《计算机组成与设计(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,c
ADD a,a,d
ADD 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,t1
2.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,X20
ADD X10,X22,X23
SUB X19,X9,X10
2.3.1 存储器操作数
在编程语⾔中,有仅含⼀个数据元素的简单变量,也有如数组和结构体那样复杂的数据结构。这些复杂数据结构中的数据元素可能远多于计算机中寄存器的个数。计算机怎样来表⽰和访问这样⼤的结构呢?
处理器只能将少量数据保存在寄存器中,但存储器可以存放数⼗亿的数据元素。因此,数据结构(如数组和结构体)存放在存储器中。
如上所述,LEGv8的算术运算指令只对寄存器进⾏操作,因此LEGv8必须包含在存储器和寄存器之间传输数据的指令。这些指令称为数据传输指令(data transfer instruction)。为了访问存储器中的字或双字,指令必须给出存储器的地址。 可以将存储器视为⼀个很⼤的⼀维数组,其地址相对于数组的索引,从0开始。
将数据从存储器复制到寄存器的数据传输指令通常称为取数(load)指令。load指令的格式是操作码后接着⽬的寄存器,再后⾯是⽤来访问存储器的寄存器和常数。 常数和第⼆个寄存器中的值加起来即得到待访问的存储器地址。 实际的LEGv8 load指令助记符为LDUR,表⽰加载寄存器。
例题
编译⼀个操作数在存储器中的赋值语句
设A是⼀个含有100个双字的数组,编译器仍然将寄存器X20,X21依次分配给变量g和h。⼜设数组A的起始地址(或称基址(base address))存放在寄存器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 4
ADD 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中。
这条指令中的各个字段也可以表⽰成⼆进制的形式:
10001011000101010000001010001001
11位5位6位5位5位
指令的布局形式叫做指令格式(instruction format)。从⼆进制位的数⽬可以看出,LEGv8指令占32位,即⼀个字或半个双字。遵循简单源于规整的原则,所有的LEGv8指令都是32位长。
2.6 逻辑操作
虽然早期的计算机仅仅对整字进⾏操作,但⼈们很快发现,对字中由若⼲位组成的字段甚⾄对单个位进⾏操作是很有⽤的。 于是,编程语⾔和指令集体系结构中增加了⼀些指令,⽤于简化对字中若⼲位进⾏打包或者拆包的操作。这些指令被称为逻辑操作。
逻辑操作C操作符Java操作符LEGv8指令
逻辑左移<<<<LSL
逻辑右移>>>>>LSR
按位与&&AND,ANDI
按位或||OR,ORI
按位取反~~EOR,EORI
LEGv8实现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 not zero)。这两条指令传统上称为条件分⽀(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-jx86架构和arm架构区别
CBNZ 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 Exit
Exit:
例题
编译C语⾔中的while循环
下⾯是⽤C语⾔编写的⼀个传统循环程序
while(save[i]==k)
i+=1;
假设i和k存放在寄存器X22和X24中,数组save的基址存放在寄存器X25中。请写出这段C程序对应的LEGv8汇编代码。
变量i k save基地址临时寄存器
寄存器X22X24X25X9,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+1
B Loop  // 指令跳转到循环开始的地⽅
Exit:
分析
第⼀步:将save[i]读⼊到⼀个临时寄存器中; save[i]和k相减,差值保存在X11中⽤于循环测试。当然,这段代码可以进⾏优化
体系结构设计师通过增加四个额外的⼆进制位来记录指令执⾏的状态信息,这些增加的位称为条件码(condition code)或标志位(flag):
负数标志位(N):若结果最⾼位位1,则设置该条件码
零标志位(Z):若结果为0,则设置该条件码
溢出标志位(V):若结果溢出,则设置该条件码
进位标志位( C):若结果向最⾼位进位或从最⾼位借位,则设置该条件码