《计算机组成与设计(ARM版)》读书笔记-第二章指令1

《计算机组成与设计(ARM版)》读书笔记-第二章指令1

2023年7月13日发(作者:)

《计算机组成与设计(ARM版)》读书笔记-第⼆章指令1《计算机组成与设计ARM版》⽹页:Youtube上⾯ ARM DS-5 教程:⽂章⽬录2.1 引⾔想要命令计算机,就必须使⽤计算机的语⾔。计算机语⾔中的基本单词称为指令,⽽⼀台计算机的全部指令(即词汇库)称为该计算机的指令集。通过理解如何表述指令,我们也可以发现计算的秘密:存储程序思想(stored-program concept)。什么是存储程序思想呢?存储程序思想指的是多种类型的指令和数据均以数字形式存储于存储器中,该思想导致了存储程序型计算机的诞⽣。2.2.计算机硬件的操作任何计算机都必须能够执⾏算术运算。LEGv8的汇编语句ADD a,b,c命令计算机将两个变量b和c相加,并将结果放⼊变量a中。这种助记符表⽰是固定的:每条LEGv8算数指令只执⾏⼀个操作,并且有且仅有三个变量。下⾯的指令序列将实现4个变量的相加:ADD a,b,cADD a,a,dADD a,a,e因此对四个变量求和需要三条指令。注意与其他编程语⾔不同的是,LEGv8语⾔的每⼀⾏最多只有⼀条指令。另⼀点与C语⾔不同的是,注释总是在⼀⾏的末尾结束。例题将⼀条复杂的C语⾔语句编译成为LEGv8语句f=(g+h)-(i+j)C编译器会产⽣什么样的LEGv8汇编代码呢?答案因为⼀条LEGv8指令仅执⾏⼀个操作,所以编译器必须将这条C语句编译成多条汇编指令。若第⼀条指令计算g和h的和,其结果暂存在某⼀个地⽅。因此,编译器需要创建⼀个临时变量t0:ADD t0,t,h下⼀条指令计算i和j的和ADD t1,i,j最后,⽤⼀条减法指令将t0和t1中的值相减,结果存⼊变量f,完成编译:SUB f,t0,t12.3 计算机硬件的操作数与⾼级语⾔不同的是,LEGv8算数运算指令的操作数有严格的限制—必须来⾃寄存器。 寄存器直接由硬件构建,且数量有限,是计算机硬件设计的基本元素。 在LEGv8体系结构中每个寄存器的⼤⼩为64位。LEGv8体系结构中将64位称为双字,32位称为字。⾼级语⾔的变量与寄存器 的⼀个主要区别在于寄存器的数量是有限的,现代计算机(如LEGv8中)⼀般有32个寄存器。例题使⽤寄存器编译C赋值语句将程序变量和寄存器对应起来是编译器的⼯作之⼀。以前⾯提到的C赋值语句为例:f=(g+h)-(i+j)寄存器X19,X20,X21,X22,X23⼀次分配给f,g,h,i,j。请写出编译后的LEGv8代码。答案除了将变量⽤上述寄存器代替,将两个临时变量⽤X9和X10代替外,编译后⽣成的代码和前⾯的⾮常类似ADD X9,X19,X20ADD X10,X22,X23SUB X19,X9,X102.3.1 存储器操作数在编程语⾔中,有仅含⼀个数据元素的简单变量,也有如数组和结构体那样复杂的数据结构。这些复杂数据结构中的数据元素可能远多于计算机中寄存器的个数。计算机怎样来表⽰和访问这样⼤的结构呢?处理器只能将少量数据保存在寄存器中,但存储器可以存放数⼗亿的数据元素。因此,数据结构(如数组和结构体)存放在存储器中。如上所述,LEGv8的算术运算指令只对寄存器进⾏操作,因此LEGv8必须包含在存储器和寄存器之间传输数据的指令。这些指令称为数据传输指令(data transfer instruction)。为了访问存储器中的字或双字,指令必须给出存储器的地址。 可以将存储器视为⼀个很⼤的⼀维数组,其地址相对于数组的索引,从0开始。将数据从存储器复制到寄存器的数据传输指令通常称为取数(load)指令。load指令的格式是操作码后接着⽬的寄存器,再后⾯是⽤来访问存储器的寄存器和常数。 常数和第⼆个寄存器中的值加起来即得到待访问的存储器地址。 实际的LEGv8 load指令助记符为LDUR,表⽰加载寄存器。例题编译⼀个操作数在存储器中的赋值语句设A是⼀个含有100个双字的数组,编译器仍然将寄存器X20,X21依次分配给变量g和h。⼜设数组A的起始地址(或称基址(baseaddress))存放在寄存器X22中。试编译下⾯的C赋值语句:g=h+A[8];答案虽然该C赋值语句只有⼀个简单操作,但其中⼀个操作数在存储器中,所以⾸先必须将A[8]传输到寄存器中。 该数组元素的地址由A的基址(X22中)加上该元素序号8构成。 取出的数据放在⼀个临时寄存器中供下⼀条指令使⽤。编译后⽣成的第⼀条指令为(这⾥是⼀种简化,后⾯会对这条指令做细微的调整):LDUR X9,[X22,#8] //寄存器X9得到A[8]下⼀条指令可对X9(其值等于A[8])进⾏操作。ADD X20,x21,x9 // g=h+A[8]⽤于计算机访存地址的寄存器(本例中为X22)称为基址寄存器(base register),数据传输指令中的常数(本例中为8)称为偏移量。很多程序经常⽤到8⽐特的字节类型,事实上⽬前的体系结构都按字节编址。因此,双字的地址和其所包括的8字节中某个字节的地址相匹配,且相邻双字的地址相差8.图⽚来源:计算机组成与设计(ARM版)英⽂版字节寻址对数组索引有影响。在上⾯的代码中,为了得到正确的字节地址,与基址寄存器X22相加的偏移量必须是8*8(即64),这样才能得到正确的A[8]。与取数(load)指令相对应的指令通常叫做存数(store)指令,将数据从寄存器复制到存储器中。存数指令的格式和取数指令类似:⾸先是操作码,接着是包含待存储数据的寄存器,然后是基址寄存器,最后是选择具体数组元素的偏移量。同样,访存地址由常数和基址寄存器共同决定。LEGv8的存数指令为STUR,表⽰将寄存器内容存储到存储器中。例题⽤load/store进⾏编译假设变量h存放在寄存器X21中,数组A的基址放在X22中。那么下⾯C赋值语句的LEGv8汇编代码是怎样的?A[12]=h+A[8];答案虽然该C语句只有⼀个操作,但两个操作数都在存储器中,因此需要更多的LEGv8指令。前两条指令与上个例题相同,但本例按照字节寻址,load指令使⽤偏移量64来选择A[8],并且加法指令将结果放在寄存器X9中:LDUR X9,[X22,#64]ADD X9,X21,X9最后⼀条指令将加法结果存放到存储器单元A[12]中,使⽤96(12*8)作为偏移量,X22作为基址寄存器。STUR X9,[X22,#96]//把h+A[8]存回A[12]LDUR和STUR是ARMv8体系结构中在存储器和寄存器之间复制双字的指令。有些计算机采⽤其他的指令来传输数据,如Intel x86体系结构。2.3.2 常数或⽴即数操作数程序中经常会在某个操作中使⽤到常数,例如,将数组的索引递增,以指向下⼀个数组元素。实际上,有很多(甚⾄超过⼀半)的LEGv8算术运算指令会⽤到常数作为操作数。如果仅使⽤⽬前已介绍过的指令,使⽤常数时必须先将其从存储器中取出(常数可能是在程序被加载进主存时放⼊存储器的)。例如,要使寄存器X22加4,可以使⽤以下代码LDUR X9,[X20,AddrConstant4] // X9= consant 4ADD X22 ,X22,X9假设X20+AddrConsant4 是常量4在存储器中的地址。避免使⽤load指令的另⼀种⽅法是,增加⼀种算术运算指令,并令其中⼀个操作数是常数。这种⼀个操作数是常数的快速加法指令称为⽴即数加(add immediate),或写成ADDI。因此上述操作可以写成:ADDI X22,X22,#4常数操作数出现频率很⾼,⽽且相对于从存储器中取常数,包含常数的算数运算指令执⾏速度更快,并且能耗更低。常数0还有另外的作⽤,即通过提供有⽤的变量简化指令集。例如 ,数据移动指令MOV等价于⼀个常数操作数为0的加法。因此。LEGv8将寄存器XZR(寄存器编号为31)通过硬件连线恒置为0.2.4 有符号数和⽆符号数2.5 计算机中指令的表⽰指令在计算机内部是以⼀系列或⾼或低的电信号表⽰的,形式上和数的表⽰相同。实际上,指令的各部分都可以看成⼀个独⽴的数,将这些数拼接在⼀起就形成了指令。LEGv8中的32个寄存器⽤编号0~31表⽰。例题将LEGv8汇编语⾔指令翻译成机器指令下⾯以LEGv8汇编语⾔为例。对于符号表⽰的LEGv8指令ADD X9,X20,X21⾸先表⽰为⼗进制数的组合,然后表⽰为⼆进制数的组合。答案其⼗进制数表⽰为1112210209指令分为若⼲字段(field)。第⼀个字段(本例中包含1112的字段)告诉LEGv8计算机该指令要执⾏加法运算。第⼆个字段指明加法操作中第⼆个源操作数的寄存器编号(即X21的编号21),第四个四段指出另⼀个源操作数的寄存器编号(X20的20).第五个字段表⽰存放运算结果的⽬的寄存器编号(X9的9).第三个字段在这条指令中没有⽤到,故置为0.这条指令将寄存器X20和寄存器X21的内容相加,并将和放在寄存器X9中。这条指令中的各个字段也可以表⽰成⼆进制的形式:1位101015位0000006位101005位010015位指令的布局形式叫做指令格式(instruction format)。从⼆进制位的数⽬可以看出,LEGv8指令占32位,即⼀个字或半个双字。遵循简单源于规整的原则,所有的LEGv8指令都是32位长。2.6 逻辑操作虽然早期的计算机仅仅对整字进⾏操作,但⼈们很快发现,对字中由若⼲位组成的字段甚⾄对单个位进⾏操作是很有⽤的。 于是,编程语⾔和指令集体系结构中增加了⼀些指令,⽤于简化对字中若⼲位进⾏打包或者拆包的操作。这些指令被称为逻辑操作。逻辑操作逻辑左移逻辑右移按位与按位或按位取反C操作符<<>>&|~Java操作符<<>>>&|~LEGv8指令LSLLSRAND,ANDIOR,ORIEOR,EORILEGv8实现NOT(取反)操作的⼀种⽅式是与全1数做异或处理。2.7 决策指令LEGv8汇编语⾔中有两条决策指令,和if以及go to语句类似。第⼀条是:CBZ register,L1该指令表⽰:如果register的数值为0,则转到标签为L1的语句执⾏。助记符CBZ表⽰⽐较为0分⽀(compare and branch if zero)。第⼆条指令是CBNZ register,L1该指令表⽰:如果register的数值不为0,则转到标签为L1的语句执⾏。助记符CBNZ表⽰⽐较不为0分⽀(compare and branch if notzero)。这两条指令传统上称为条件分⽀(conditional branch) 指令。例题将if-then-else语句编译成条件分⽀指令在下⾯这段代码中,f,g,h,i,j都是变量,假设这五个变量依次对应于五个寄存器X19到X23.请写出这条C语⾔编写的if语句编译后形成的LEGv8代码。if (i==j) f=g+h;else f=g-h;答案前⾯介绍的条件分⽀指令只能判断⼀个寄存器的值是否为0,因此第⼀步要将i和j相减,检查结果是否为0.接下来要做的似乎是如果结果为0,则进⾏分⽀,即使⽤CBZ指令。通常,通过测试分⽀的相反条件来跳过⽐较不相等要执⾏的代码,这样的代码效率会更⾼。故这⾥使⽤CBNZ指令。SUB X9,X22,X23 //X9=i-jCBNZ X9,Else // go to Else if i≠ j(X9≠0)ADD X19,X20,X21 //f=g+h (skipped if i≠ j)Else: SUB X19,X20,X21 //f=g-h(skipped if i=j)B ExitExit:例题编译C语⾔中的while循环下⾯是⽤C语⾔编写的⼀个传统循环程序while(save[i]==k) i+=1;假设i和k存放在寄存器X22和X24中,数组save的基址存放在寄存器X25中。请写出这段C程序对应的LEGv8汇编代码。变量寄存器iX22kX24save基地址X25临时寄存器X9,X10,X11答案Loop: LSL X10,X22,#3 //临时寄存器存放偏移量,i左移3位,因为按字节编址ADD X10,X10,X25 //save[i]的地址,在寄存器X10中,此时save[i]

还在

存储器中LDUR X9,[X10,#0] //save[i]读⼊到临时寄存器X9中SUB X11,X9,X24 //作差,当作循环判断条件CBNZ X11,Exit //不为0,跳出循环ADDI X22,X22,#1 //i+1B Loop //

指令跳转到循环开始的地⽅Exit:分析第⼀步:将save[i]读⼊到⼀个临时寄存器中; save[i]和k相减,差值保存在X11中⽤于循环测试。当然,这段代码可以进⾏优化体系结构设计师通过增加四个额外的⼆进制位来记录指令执⾏的状态信息,这些增加的位称为条件码(condition code)或标志位(flag):负数标志位(N):若结果最⾼位位1,则设置该条件码零标志位(Z):若结果为0,则设置该条件码溢出标志位(V):若结果溢出,则设置该条件码进位标志位( C):若结果向最⾼位进位或从最⾼位借位,则设置该条件码条件分⽀指令通过组合使⽤这些条件码完成条件判断。在LEGv8指令集中,这种条件分⽀指令是,cond可以⽤于任意有符号数的⽐较指令中,⽐如EQ(等于),NE(不等于),LT(⼩于),LE(⼩于等于),GT(⼤于),GE(⼤于等于).cond也可以⽤于⽆符号数的⽐较指令,⽐如LO(低于),LS(低于或相同),HI(⾼于)或者HS(⾼于或相同)。条件码的⼀个缺点是,如果许多指令频繁设置条件码,就可能造成依赖性问题,使得指令很难流⽔执⾏。LEGv8规定只有少数指令—ADD,ADDI,AND,ANDI,SUB和SUBI–能设置条件码,并且条件码的设置是可选择的。 在LEGv8汇编语⾔中,如果想设置条件码,只需要在相应指令的尾部追加S,如ADDS,ADDIS,ANDS,ANDIS,SUBS,SUBIS.指令名称中实际上使⽤了术语“flag”,因此ADDS的正确解释应该是“add and set flags”,即加并设置标志位。边界检查的简便⽅法将有符号数作为⽆符号数来处理,是⼀种检验0≤x

IndexOutOfBounds翻阅博客⽂章,找到类似的汇编语句cmp x11, #4; //

相当于 subs xzr, x11, #4.

//

如果 x11 - 4 == 0,

那么状态寄存器NZCV.Z = 1 //

如果 x11 - 4 < 0,

那么 NZCV.N = 1来源:备注零寄存器XZR零寄存器忽略所有对它的写操作,并且所有对它的读操作都返回0.您可以在⼤多数(但不是全部)指令中使⽤零寄存器。2.8 计算机硬件对过程的⽀持过程(procedure)或函数是程序员进⾏结构化编程的⼯具,其定义为:根据提供的参数完成⼀定任务的⼦程序。在过程执⾏时,程序必须遵守以下6个步骤:1 将参数放到过程可以访问的地⽅2 将执⾏的控制权转交给过程3 获得过程执⾏所需的存储资源4 执⾏需要的任务5 将结果值放在调⽤程序可以访问的地⽅6 将控制返回调⽤点,⼀个过程可能在程序中的多个点被调⽤。由于寄存器是计算机中保存数据最快的地⽅,所以我们希望尽可能多地利⽤寄存器。LEGv8软件在位过程调⽤分配寄存器时遵循以下约定:X0~X7:作为参数寄存器(8个),⽤于传递参数或返回结果。LR(X30):作为返回地址寄存器(存放过程调⽤的返回地址),⽤于返回原始调⽤点。除了分配寄存器外,LEGv8汇编语⾔还为过程调⽤提供了⼀条指令:该指令在跳转到某个地址的同时,将下⼀条指令的地址保存在寄存器LR(X30)中。这条分⽀和链接指令(branch-and-link instruction)BL可简单表⽰为:BL ProcedureAddress指令名中的链接代表指向调⽤者的地址或链接,以允许过程返回到合适的地址。存储在寄存器LR中的“链接”称为返回地址,返回地址是必需的,因为同⼀过程可能在程序的不同地⽅被调⽤。为了⽀持从过程调⽤返回,计算机(如LEGv8)使⽤了寄存器跳转( branch register)指令BR,表⽰⽆条件跳转到寄存器所指定的地址:BR LR寄存器跳转指令跳转到存储在LR寄存器中的地址。因此,调⽤程序或称为调⽤者(caller),将参数值放在X0~X7中,然后使⽤BL X 跳转到过程X(有时被称为被调⽤者(callee))。被调⽤者执⾏运算,将结果放在相同的参数寄存器中,然后通过BR LR 指令将控制返回给调⽤者。程序计数器(PC):存放正在执⾏指令的地址的寄存器。BL指令实际上将PC+4保存在寄存器LR中,从⽽链接到下⼀条指令的字节地址,为过程返回做好准备。2.8.1 使⽤更多的寄存器假设对于⼀个过程,编译器需要的寄存器超过了8个参数寄存器的数量。由于在任务完成后必须消除过程产⽣的踪迹,因此调⽤者原先使⽤的寄存器都必须恢复到过程调⽤前的状态。这种情况可以看作是需要将寄存器溢出(换出)到存储器的⼀个例⼦。换出寄存器的最理想的数据结构是栈,⼀种后进先出的队列。 栈需要⼀个指针指向栈中最新分配的地址,以指⽰下⼀个过程放置换出寄存器的位置,或是寄存器旧址存放的位置。按照历史惯例,栈从⾼地址向低地址“增长”。这意味着数据压栈时,栈指针值减⼩;⽽数据弹栈时,栈指针增加,(空闲栈)空间缩⼩。例题编译⼀个不调⽤其他过程的C过程将2.2节的例⼦转化为⼀个C过程:long long int leaf_example( long long int g,long long int h, long long int i,long long int j){ long long int f; f=(g+h)-(i+j); return f;}编译后的LEGv8汇编代码是什么?答案参数变量 g,h,i,j分别对应参数寄存器X0,X1,X2,X3,f对应X19.编译后的程序是以⼀个过程标号开始的:leaf_example:下⼀步是保存过程中使⽤的寄存器。过程实体中的C赋值语句使⽤了两个临时寄存器X9和X10.因此,需要保存三个寄存器:X19,X9和X10.我们将旧值压栈,即在栈中建⽴三个双字的空间(24个字节),并将三个寄存器的值存⼊:SUBI SP,SP,#24 //

调整堆栈指针,预留空间STUR X10,[SP,#16]//保存寄存器X10

STUR X9,[SP,#8]//保存寄存器X9STUR X19,[SP,#0]//保存寄存器X19从上⾯四句汇编可以看出,堆栈从⾼地址开始使⽤,⼀直到低地址。接下来三条语句对应 函数体ADD X9,X0,X1ADD X10,X2,X3SUB X19,X9,X10为了返回f的值,我们将它复制到⼀个参数寄存器中:ADD X0,X19,XZR // return f (X0=X19+0)在返回前,还要通过“弹栈”恢复三个寄存器的旧值:LDUR X19,[SP,#0]//

为调⽤者恢复X19的旧值LDUR X9,[SP,#8]//为调⽤者恢复X9的旧值LDUR X10,[SP,#16]//为调⽤者恢复X10的旧值ADDI SP,SP,#24 //调整堆栈指针,删除三个过程最后通过⼀条寄存器分⽀指令跳转到返回地址:BR LR // branch back to calling routine上例中,我们使⽤了临时寄存器(X9和X10),并假设它们的旧值必须保存和恢复。为了避免保存和恢复⼀个从未被使⽤过的寄存器(通常是临时寄存器),LEGv8软件将其中19个寄存器分为两组:X9~X17:在过程调⽤中,不需要由被调⽤者(被调⽤的过程)保存的临时寄存器。X19~X28:在过程调⽤中必须被保存(⼀旦被使⽤,由被调⽤者保存和恢复)的保存寄存器。这⼀简单约定减少了寄存器的换出。在上⾯的例⼦中,因为调⽤者不期望在过程调⽤时保留寄存器X9和X10,所以我们可以去掉代码中的两次保存和两次载⼊操作。但仍然需要保存和恢复X19,因为被调⽤者只能假设调⽤者需要该值。2.8.2 过程嵌套不调⽤其他过程的过程称为叶(leaf)过程。如果所有过程都是叶过程,那么情况就很简单,但实际并⾮如此。实际上过程嵌套⽐较多,如果没有⼀套机制,就容易相互冲突。⼀种解决⽅法是将其他所有必须保留的寄存器压栈,就像把保存寄存器压栈⼀样。调⽤者将所有调⽤后需要的参数寄存器(X0~ X7)或临时寄存器( X9~ X17)压栈。被调⽤者将返回地址寄存器LR和被调⽤者使⽤的保存寄存器(X19~X25)压栈。栈指针SP随着压⼊栈中的寄存器个数进⾏调整。返回时,寄存器从存储器中恢复,栈指针也随之重新调整。例题编译⼀个递归的C过程,演⽰嵌套过程的链接下⾯是⼀个计算阶乘的递归过程:long long int fact(long long int n){ if(n<1) return (1); else return (n*fact(n-1));}请写出对应的LEGv8汇编代码。答案参变量n对应参数寄存器X0.编译后的程序从过程标签开始,然后将两个寄存器保存在栈中,⼀个是返回地址LR,另⼀个是X0:fact: SUBI SP,SP,#16//调整堆栈指针,预留两个 STUR LR,[SP,#8]//

保存返回地址寄存器 STUR X0,[SP,#0] //保存参变量n第⼀次调⽤fact时,STUR保存程序中调⽤fact 的地址。下⾯两条指令测试n是否⼩于1,如果n≥1则跳转到 ZXR,X0,#1 // TEST FOR n<1

(这⾥是测试X0和1的差值) L1 //IF n≥ 1,go to L1如果n⼩于1,fact将1 放⼊⼀个参数寄存器X1并返回,具体步骤是:在0上加1,它们的和存⼊X1,然后从栈中弹出两个已保存的值并跳转到返回地址:ADDI X1,XZR,#1 // return 1ADDI SP,SP,#16// pop 2 items off stackBR LR //跳转到返回地址在从栈中弹出两项之前,本应该加载(加载使⽤STUR)X0和LR。但是由于n⼩于1时,X0和LR没有变化,所以跳过了这些指令。如果n不⼩于1,参数n减1,然后使⽤减1后的值再次调⽤fact:L1: SUBI X0,X0,#1 //参数减⼀ BL fact //call fact(n-1)BL指令(分⽀和链接指令):跳转到某⼀个地址的同时将下⼀条指令的地址保存到寄存器中(LEGv8中为寄存器LR,即X30).BR指令(寄存器跳转指令):⽆条件跳转到寄存器所指定的地址下⼀条指令是fact的返回位置。下⾯恢复旧的参数、旧的返回地址,以及栈指针:LDUR X0,[SP,#0] //

从BL返回:恢复参数nLDUR LR,[SP,#8] //

恢复

返回地址ADDI SP,SP,#16 //调整堆栈指针

弹出2个位置接下来,值寄存器X1得到旧参数X0和当前值寄存器的乘积MUL X1,X0,X1 //return n*fact(n-1)最后 ,fact再次跳转到返回地址:BR LR // return to the callerC语⾔中的⼀个变量通常对应存储器中的⼀个位置,其解释取决于类型和存储⽅式。典型的类型包括整型和字符型。C语⾔提供两种存储⽅式:动态地和静态的。动态变量位于过程中,当过程退出时失效。静态变量在进⼊和退出过程时始终存在。在所有过程之外声明的C变量,以及声明时使⽤关键字static的变量都视为静态的,其余的变量都视为动态的。为了简化对静态数据的访问,LEGv8编译器保留了⼀个寄存器,称为全局指针(global pointer),即GP。例如X27寄存器可以保留全局指针。2.8.3 为栈中新数据分配空间栈的另⼀点复杂性在于,栈还⽤来保存过程的局部变量,⽽这些变量可能不适⽤于寄存器,例如局部数组或结构体。栈中包含过程所保存的寄存器和局部变量的⽚段称为过程帧(procedure frame)或活动记录(activation record).有些LEGv8编译器使⽤帧指针(Frame Pointer,FP)指向过程帧的第⼀个双字。在过程中栈指针(SP)可能会发⽣改变。 因此,在过程中的不同位置对存储器中局部变量的引⽤可能会具有不同的偏移量,这使得过程更加难以理解。另⼀种⽅案是,帧指针(FP)在⼀个过程中为本地存储器引⽤提供⼀个固定的基址寄存器。注意,⽆论是否使⽤显⽰的帧指针,活动记录都出现在栈中。图⽚来源:英⽂版计算机组成与设计ARM版对定义再明确⼀下过程帧:也称为活动记录,栈中包含过程所保存的寄存器以及局部变量的⽚段。帧指针:指向给定过程中保存的寄存器和局部变量的值(指针)上图表⽰过程调⽤前(a),调⽤时(b),调⽤后(c)栈的分配情况。帧指针(FP或者X29)指向该帧的第⼀个双字(⼀般是保存的参数寄存器 ,saved argument registers),栈指针(SP)指向栈顶。(本书中,栈顶在图⽚下⽅,因为栈从⾼地址到低地址)。栈进⾏调整,为所有的保存寄存器和驻留在内存的局部变量提供⾜够的空间。 由于栈指针(SP)可能会改变,所以对于程序员来说,虽然使⽤栈指针和少量的地址运算就可能完成对变量的引⽤,但使⽤固定的帧指针(FP)会变得更为简单。 如果在⼀个过程中栈中没有局部变量,则编译器可以不设置和不恢复帧指针以节省时间。使⽤帧指针时,在过程调⽤时使⽤SP中的地址初始化FP,⽽SP可以使⽤FP来恢复。2.8.4 在堆中为新数据分配空间除了动态变量之外,C程序员还需要再内存中为静态变量和动态数据结构提供空间。下图给出了LEGv8在运⾏Linux操作系统时内存分配的约定。栈从⽤户地址空间的⾼端(⾼地址)开始并向下增长。内存低端的第⼀部分是保留的,接着是LEGv8机器代码,通常称为代码段(textsegment),即下图中的Text。代码段之上是静态数据段,是存储常量和其他静态变量的空间。数组通常具有固定长度,因⽽能与静态数据段很好地匹配。但类似于链表这样的数据结构通常会在⽣命周期内增长或缩短,这类数据结构对应的段通常称为堆(heap),⼀般在存储器中放在静态数据段之后。这种分配允许栈和堆相互增长(相反⽅向),从⽽在连个段此消彼长的过程中实现对内存的⾼效使⽤。图⽚来源:英⽂版计算机组成与设计ARM版⼀些递归过程可以不通过递归⽽⽤迭代⽅式实现。通过消除递归时过程调⽤产⽣的相关开销,可以显著提⾼迭代性能。例如,考虑下⾯的求和过程:long long int sum(long long int n,long long int acc){ if(n>0)

return sum(n-1,acc+n); else return acc;}考虑过程调⽤sum(3,0).该过程递归调⽤sum(2,3),sum(1,5),sum(0,6),结果6通过4次返回得到。这种求和的递归调⽤称为尾调⽤,这个尾递归的例⼦可以⾼效地实现(假设X0=n,X1=acc,结果在X2中):sum: SUBS XZR,X0,XZR // COMPARE N TO 0 sum_exit //go to sum_exit if n≤0 ADD X1,X1,X0 // add n to acc SUBI X0,X0,#1 // n-1 B sum // go to sumsum_exit: ADD X2,X1,XZR // return value acc BR LR // return to caller

发布者:admin,转转请注明出处:http://www.yc00.com/news/1689199119a219872.html

相关推荐

发表回复

评论列表(0条)

  • 暂无评论

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信