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

文章目录

    • 2.9 人机交互
    • 2.10 LEGv8中的宽立即数和地址的寻址
      • 2.10.1 宽立即数
      • 2.10.2 分支中的寻址
      • 2.10.3 LEGv8寻址模式总结
      • 2.10.4 机器语言解码
    • 2.11 并行与指令:同步
    • 2.12 翻译并启动程序
      • 2.12.1 编译器
      • 2.12.2 汇编器
      • 2.12.3 链接器
      • 2.12.4 加载器
      • 2.12.5 动态链接库
      • 2.12.6 启动Java程序
    • 2.13 综合实例:C排序程序
      • 2.13.1 swap过程
        • 1 为swap分配寄存器
        • 2 为swap过程体生成代码
        • 3 完整的swap过程
      • 2.13.2 sort过程
        • 1 为sort分配寄存器
        • 2 为sort过程体生成代码
        • 3 sort 中的过程调用
        • 4 sort中的参数传递
        • 5 在sort中保存寄存器
        • 6 完整的sort过程

2.9 人机交互

计算机发明最初是用于处理数字的,但在进入商业应用时,计算机已经能够处理文字了。今天计算机基本都使用8位的字节来表示字符,并遵行美国信息交换标准代码(即ASCII码)。

注意,在ASCII码中,所有大写字母和对应小写字母的差均为32,这个规律可以用于快速检查和切换大小写。 另外一个有用的ASCII值是0,表示null,C语言常用它来标记字符串的结尾。

通过使用一系列指令可以从一个双字中提取出一个字节,所以load register 和 store register 足以完成字和字节的传输。然而,很多程序中经常要处理文本,因此LEGv8额外提供了字节移动指令。读字节指令LDURB(load byte)从内存中读出一个字节,并将其放在一个寄存器最右边的8位。存字节指令STURB(store byte)把一个寄存器最右边的8位(即一个字节)取出,并写到内存。这样,我们可以通过以下指令序列复制一个字节:

LDURB X9,[X0,#0] // 读取字节  
STURB X9,[X1,#0] // 向目的地写 字节

字符通常被组合为字符数目可变的字符串。表示一个字符串有三种方法:(1) 保留字符串的第一个位置用于给出字符串的长度;(Java采用该种方法)(2)附加一个指明字符串长度的变量(如再结构体中)。(3)字符串最后的位置用一个字符来标识其结尾。C语言使用第三种方法,用一个值为零(ASCII码中的null)的字节来结束字符串。因此,在C语言中,字符串“Cal” 由4字节表示,由十进制分别表示为67,97,108,0.

例题
编译一个字符串复制过程,体会如何使用C语言的字符串

过程strcpy利用C语言中以null字节结束字符串的规定,将字符串y赋值给字符串x:

void strcpy( char x[],char y[]){
     
	size_t i;
	i=0;
	while((x[i]=y[i])!='\0')  //赋值并且判断是否符合条件
		i+=1;

}

请给出编译后的LEGv8汇编代码。

下面是在DS-5中 反汇编得到的结果,值得研究

strcpy
        0x0000000080001218:    SUB      sp, sp, #0x20
        0x000000008000121C:    STR      x0, [sp, #0x8]
        0x0000000080001220:    STR      x1, [sp]
        0x0000000080001224:    STR      xzr, [sp, #0x18]
        0x0000000080001228:    B        0x80001238
        0x000000008000122C:    LDR      x0, [sp, #0x18]
        0x0000000080001230:    ADD      x0, x0, #0x1
        0x0000000080001234:    STR      x0, [sp, #0x18]
        0x0000000080001238:    LDR      x1, [sp]
        0x000000008000123C:    LDR      x0, [sp, #0x18]
        0x0000000080001240:    ADD      x1, x1, x0
        0x0000000080001244:    LDR      x2, [sp, #0x8]
        0x0000000080001248:    LDR      x0, [sp, #0x18]
        0x000000008000124C:    ADD      x0, x2, x0
        0x0000000080001250:    LDRB     w1, [x1]
        0x0000000080001254:    STRB     w1, [x0]
        0x0000000080001258:    LDRB     w0, [x0]
        0x000000008000125C:    CMP      w0, #0x0
        0x0000000080001260:    B.NE     0x8000122c
        0x0000000080001264:    NOP
        0x0000000080001268:    ADD      sp, sp, #0x20
        0x000000008000126C:    RET

答案

下面是基本的LEGv8代码片段。假设数组x和y的基址在X0和X1中,而i在X19中。strcpy调整栈指针,然后将保存寄存器X19保存在栈中。

strcpy:
	SUBI SP,SP,#8   //调整堆栈指针,为一个元素留出空地
	STUR X19, [SP,#0]//SAVE X19

Java中的字符和字符串

Java对字符采用Unicode编码,默认情况下,Unicode使用16位来表示一个字符。ASCII使用8位表示一个字符。

LEGv8指令集中的一些指令能够显示地读取和存储16位量,即半字。读半字指令LDURH(load half) 从存储器中读出一个半字,然后将其放到寄存器的最右边16位。与读字节类似,读半字指令LDURH也将半字看作有符号苏,并进行符号扩展以填充寄存器中剩余的48位。存半字指令STRUH(store half)将寄存器最右边的16位写入寄存器。 下面的指令可以复制半字

LDURH X19,[X0,#0]//从源地址读取半字(16bits)
STURH X9,[X1,#0]//向目的写入半字

下面哪种类型的变量存放1000 000 000占用的内存空间最大?
A C语言中的long long int
long long在win32中存在,长度为8个字节;定义为LONG64。
B C语言中的 string
C Java中的string
答案是C

参考:Java String 占用内存大小分析

2.10 LEGv8中的宽立即数和地址的寻址

2.10.1 宽立即数

2.10.2 分支中的寻址

LEGv8跳转指令(无条件分支指令)采用最简单的寻址方式,使用B型LEGv8指令格式,操作码为6位,其余为都是地址段。

B 10000 // go to location  10000(Decimal)

可以汇编成下面的格式(实际中更为复杂):

5 1000 0 t e n 10000_{ten} 10000ten
6位 26位

其中,跳转指令的操作码值为5,分支地址为 1000 0 t e n 10000_{ten} 10000ten

和跳转指令不同,条件分支指令除了分支地址之外还可以指定一个操作数。
因此:

CBNZ X19,Exit  //go to Exit if X19≠0

被汇编成下面的指令,其中只有19位用于指定分支地址:

181 Exit 19
8位 19位 5位

对于条件分支指令,这种格式叫作CB型。

如果程序的地址只能放在19位的字段中,这意味着没有程序能大于 2 19 2^{19} 219,这在今天来说实在太小,因此是一种很不现实的选择。

另一种方法是指定一个寄存器,该寄存器的值用于和分支地址的偏移量相加以得到最终地址,这样分支指令的地址可按下面的方式计算:
程 序 计 数 器 = 寄 存 器 内 容 + 分 支 地 址 偏 移 量 程序计数器=寄存器内容+分支地址偏移量 =+

这个求和结果允许程序的大小得到 2 64 2^{64} 264,并且仍能使用条件分支,从而解决了分支地址大小的问题。但随之而来的问题是,使用哪个寄存器?

答案取决于条件分支是如何使用的。 条件分支在循环和if语句中都可以找到,它们倾向于转向附近的指令。例如,在SPEC基准测试程序中,大概一半的条件分支转移范围都在16条指令以内。因为程序计数器(PC)包含当前指令的地址,所以如果我们使用PC作为计算地址的寄存器,就可转移到距当前指令 ± 2 18 ±2^{18} ±218个字(1个字=32位)的地方。 几乎所有的循环和if语句都远远小于 ± 2 18 ±2^{18} ±218个字,因此PC是一个理想的选择。这种分支地址的寻址方式称为PC相对寻址(PC-relative addressing).

PC相对寻址:一种寻址方式,将PC和指令中的常数相加作为地址。

像近期大多数计算机一样,LEGv8对所有条件分支使用PC相对寻址,因为这些指令的跳转目标一般都比较接近分支指令本身。 另一方面,分支和链接(branch-and-link)指令引发的过程则并不一定总是靠近调用者,所以通常使用其他寻址方式。 因此,LEGv8 体系结构通过对分支指令以及分支和链接指令采用B型指令格式,为过程调用提供长地址。

因为LEGv8的所有指令都是4字节长,所以将PC相对寻址的地址设计成字地址(1个字=32位)而不是字节地址,从而可以扩展分支转移的范围。 通过将字段解释成相对字地址而不是相对字节地址,19位的地址字段所指示的转移范围扩大了4倍:当前PC±1MB。同样,分支指令的26位字段 也是字地址,即表示28位字节地址。

这里的1MB是怎么算出来的呢? 19位地址提供 2 18 2^{18} 218种寻址方式,又因为是字地址,还需要乘以4倍(即 2 2 2^2 22)所以 寻址大小为 2 18 × 2 2 = 2 20 = 1 M B 2^{18}\times 2^2=2^{20} =1MB 218×22=220=1MB

无条件分支(即跳转指令)也采用PC相对寻址,这意味着转移范围是当前PC值±128MB。

这里的128MB是怎么算出来的呢? 26位地址提供 2 25 2^{25} 225种寻址方式,又因为是字地址,还需要乘以4倍(即 2 2 2^2 22)所以 寻址大小为 2 25 × 2 2 = 2 27 = 2 20 × 2 7 = 128 M B 2^{25}\times 2^2=2^{27}=2^{20} \times 2^7 =128MB 225×22=227=220×27=128MB

2.10.3 LEGv8寻址模式总结

2.10.4 机器语言解码

小测验:

LEGv8中条件分支的地址范围多大(K=1024)?

答案: 分支前后大约1024K(即1MB)的地址范围

LEGv8中条件分支的指令格式为

opcode Address Rt
8位 19位 5位

其中19位用于指定分支地址。

这里的1MB是怎么算出来的呢? 19位地址提供 2 18 2^{18} 218种寻址方式,又因为是字地址,还需要乘以4倍(即 2 2 2^2 22)所以 寻址大小为 2 18 × 2 2 = 2 20 = 1 M B 2^{18}\times 2^2=2^{20} =1MB 218×22=220=1MB

LEGv8中跳转和跳转链接指令的地址范围(M=1024K)是多大?

答案:分支前后大约128M的地址范围

opcode a d d r e s s address address
6位 26位

这里的128MB是怎么算出来的呢? 26位地址提供 2 25 2^{25} 225种寻址方式,又因为是字地址,还需要乘以4倍(即 2 2 2^2 22)所以 寻址大小为 2 25 × 2 2 = 2 27 = 2 20 × 2 7 = 128 M B 2^{25}\times 2^2=2^{27}=2^{20} \times 2^7 =128MB 225×22=227=220×27=128MB

另外关于MIPS指令跳转的地址范围可参考下面这篇文章:MIPS中分支和跳转的地址范围

2.11 并行与指令:同步

2.12 翻译并启动程序

本节描述了将一个存储在外存(磁盘或闪存)某文件中的C程序转换为计算机上可执行程序的4个步骤。

C语言的翻译层次。用高级语言编写的程序首先被编译为汇编语言程序,然后被汇编为机器语言组成的目标模块。链接器将多个模块和库例程组合在一起解析所有的引用。加载器将机器代码加载到内存的适当位置供处理器执行。为了加速翻译过程,某些步骤被跳过或和其他步骤结合在一起。

《计算机组成与设计(ARM版)》读书笔记-第二章指令2_第1张图片
图片来自:《计算机组成与设计(ARM版)》英文版

为了识别文件类型,UNIX使用文件的后缀,x.c代表C源文件,x.s表示汇编文件,x.o代表目标文件,x.a代表静态链接库,x.so代表动态链接库,a.out默认情况下表示可执行文件。

MS-DOS使用后缀.C表示C源文件,.ASM代表汇编文件,.OBJ代表目标文件,.LIB代表静态链接库,.DLL代表动态链接库,.EXE代表可执行文件。

2.12.1 编译器

2.12.2 汇编器

2.12.3 链接器

2.12.4 加载器

2.12.5 动态链接库

2.12.6 启动Java程序

Java程序并不会被编译成目标计算机的汇编语言,而是首先被编译成易于解释的指令序列—Java字节码(Java bytecode)指令集。 该指令集被设计得与Java语言接近,因此编译步骤相对简单。事实上,并不需要进行任何优化。就像C语言编译器那样,Java编译器检查数据类型并且为每种数据类型生成正确的操作。Java程序最终将转化成这些字节码的二进制形式。

《计算机组成与设计(ARM版)》读书笔记-第二章指令2_第2张图片
图片来自:《计算机组成与设计(ARM版)》英文版
一种称为Java虚拟机(Java Virtual Machine,JVM)的软件解释器能够执行Java字节码。解释器是一个用来模拟一种指令集体系结构的程序。例如,这门书提供下载的DS-5,ARMv8模拟器就是一种解释器。由于翻译非常简单,地址可以由编译器填写或在运行时被JVM发现,因此不需要单独的汇编步骤。

解释的优势是可移植性。

解释的不足是性能较差。 和传统方式编译的C程序相比,相差10倍的性能。

为了既能够保持可移植性又提高执行速度,Java发展的下一阶段是实现能够在程序执行同时进行翻译的编译器。这种即时编译器(Just In Time complier,JIT)通过记录运行的程序来找到“热点”(“热点”指程序中运行特别频繁的代码块),然后将它们编译成Java虚拟机所运行于的宿主机上的本地指令。编译过的部分被保存起来供下次程序运行时使用,从而使以后每次运行变得更快。

小测验: 和 翻译器相比,对Java开发者来说,解释器的哪些优点是最重要的?
回答: 机器独立性。即与机器无关。

2.13 综合实例:C排序程序

2.13.1 swap过程

一个将内存中两个不同位置的内容进行交换的C过程。

void swap(long long int v[],size_t k){
     //数组v,位置k
//交换 v[k]和v[k+1]
	long long int tmp;
	tmp=v[k];
	v[k]=v[k+1];
	v[k+1]=tmp;
}

我们按照以下步骤把该过程从C语言手动翻译成汇编语言:

  1. 为程序变量分配寄存器
  2. 为过程体生成汇编代码
  3. 保存过程调用间的寄存器

1 为swap分配寄存器

在LEGv8中,使用寄存器X0到X7进行参数传递。由于swap只有两个参数v和k,因此可以被分配给寄存器X0和X1.仅剩的另一个变量是tmp,由于swap是一个叶过程,我们将其分配给寄存器X9.

寄存器分配如下表:

数组v k tmp
X0 X1 X9

2 为swap过程体生成代码

LEGv8存储地址按字节编址,因此双字由8个字节组成。因此索引k需先乘上8,再与地址相加。忘记连续的双字之间的地址相差8而不是1,是用汇编语言编程时常见的错误。

因此,第一步通过左移3位来将k乘以8以获得v[k]的的地址:

LSL X10,X1,#3 // X10=K*8
ADD X10,X10,X0 // X10=V+K*8

接下来根据X10取出v[k]的值,并将X10加8得到v[k+1]:(使用寄存器X9和X11)

LDUR X9,[X10,#0]  //X9=V[K]
LDUR X11,[X10,#8] //X10=V[K+1]

最后将X9和X11存储到需要交换数据的地址中

STUR X11,[X10,#0]// 交换
STUR X9,[X10,#8]

至此,我们已经为该过程分配了寄存器并获得了实现交换操作的代码。保存swap中使用的保存寄存器的代码并没有包括在其中。但由于我们并不使用叶过程(这里swap是一个叶过程)中的保留寄存器,因此没有需要保留的东西。

3 完整的swap过程

现在得到完整的例程,包括过程标签以及返回的跳转指令。

swap:LSL X10,X1,#3 //k*8
	 ADD X10,X10,X0//得到v[k]
	 LDUR X9,[X10,#0] //取出到寄存器
	 LDUR X11,[X10,#8]
	 STUR X9,[X10,#8]//交换,存储到存储器
	 STUR X11,[X10,#0]
	 
	BR LR  //过程返回

2.13.2 sort过程

该例中,我们将编写一个调用swap过程的例程,使用冒泡排序算法(这种排序算法虽然不是最快的,但确是最简单的)对数组中的整数进行排序。
一个对数组v中元素进行排序的C过程(冒泡排序):

void sort(long long int v[],size_t int n){
     //数组v和数组长度n
	size_t i ,j;
	for(i=0;i<n;i++){
     
		for(j=i-1;j>=0&&v[j]>v[j+1];j--)//从小到大排序
			swap(v,j);//交换v[j]和v[j+1]
	}

}

1 为sort分配寄存器

寄存器X0和X1分配给过程sort的两个参数v和n,寄存器X19和X20分配给变量i和j。
寄存器分配如下表:

数组v n i j
X0 X1 X19 X20

2 为sort过程体生成代码

过程体包含两个嵌套的for循环和一个带参数的swap过程。下面由外向内来展开代码。

第一步翻译第一个for循环

for(i=0;i<n;i++){
     

C语言中的for语句有三个部分:初始化、循环条件判断和迭代递增。将i初始化为0只需要一条指令,故for循环的第一部分为:

MOV X19,XZR //i=0

(注意:MOV是为了方便汇编程序员编程而由汇编器提供的伪指令)。将i递增同样也只需要一条指令实现,因此for语句的最后部分为:

ADDI X19,X19,#1 //i+=1

当i

for1tst: CMP X19,X1 //比较X19和X1(也就是i和n)
		B.GE exit1 //如果(i≥n)则跳转 到exit1

循环最后仅仅跳回循环条件判断的地方:

	B for1tst  // branch to test of outer loop
exit1:

第一个for循环的框架代码如下:

	    MOV X19,XZR
for1tst:CMP X19,X1  //循环条件判断
		B.GE exit1
		...
		(body of first  for loop)
		...
		ADDI X19,X19,#1//i+=1
		B for1tst //回到for1tst
exit1:

第二个for循环的C语句如下:

for(j=i-1;j>=0&&v[j]>v[j+1];j--)//从小到大排序

这个循环的初始化部分仍然只需要一条指令:

SUBI X20,X10,#1 //j=i-1

循环末尾j的递减也只需一条指令:

SUBI X20,X20,#1//J-=1;

循环条件测试由两部分组成,任何一个条件为假就退出循环。因此,如果第一个条件测试为假(j<0),循环就要退出:

for2tst:CMP X20,XZR
		B.LT exit2  //if (j<0)  go to exit2

这条跳转指令将跳过第二个条件测试。如果没有跳过,则j≥0.

下面来看第二个条件,当v[j]≤v[j+1]时为假。
获取v[j]

LSL X10,X20,#3
ADD X11,X0,X10// X11存放v[j]

现在取出v[j] (放在寄存器X12中):

LDUR X12,[X11,#0]  

因为第二个元素恰好是顺序下一个双字,因此将寄存器X11中的地址值加8就可以取出v[j+1] (存放在X13中):

LDUR X13,[X11,#8]

测试v[j]≤v[j+1],以判断是否跳出循环:

CMP X12,X13
B.LE exit2  //小于等于则跳转

循环末尾跳转到内存循环测试处:

 B for2tst

将这些代码片组合起来就可以得到第二个for循环的框架:

		SUBI X20,X19,#1 //J=I-1
for2tst:CMP X20,XZR  // J AND  0 进行比较
		B.LT exit2
		LSL X10,X20,#3
		ADD X11,X0,X10// X11存放v[j]
		LDUR X12,[X11,#0]  
		LDUR X13,[X11,#8]
		CMP X12,X13
		B.LE exit2  //小于等于则跳转
			...
			(body of second for loop)
			...
		SUBI X20,X20,#1//J-=1;
		B for2tst

exit2:

3 sort 中的过程调用

下一步处理第二个for循坏体:

swap(v,j);

调用swap足够简单(一条BL指令即可实现):

BL swap

4 sort中的参数传递

当我们想传递参数时问题出现了,因为sort过程需要使用寄存器X0和X1中的值,而swap过程需要将其参数放入相同的寄存器中。一种解决办法是在过程执行的早期就将sort的参数复制到其他寄存器中,让出X0和X1寄存器供swap过程使用。(这种复制要比使用栈进行保存和恢复快得多。)在过程中,首先将寄存器X0和X1的值用如下方法复制到X21和X22中:

MOV X21,X0 // COPY PARAMETER X0 INTO X21
MOVE X22,X1 //COPY PARAMETER X1 INTO X22

然后用下面两条指令将参数传递给swap:

MOV X0,X21 // swap的第一个参数v
MOV X1,X20//swap的第二个参数j

swap(v,j);

5 在sort中保存寄存器

剩下的代码保存和恢复寄存器的值。显然,我们必须将返回地址保存在寄存器LR中,因为sort是一个过程并且本身也被调用。sort过程还使用了由被调用者保存的寄存器X19,X20,X21和X22,这些寄存器的值也必须被保存。因此,sort过程开头的代码如下:

SUBI SP,SP,#40  // MAKE ROOM ON STACK FOR 5 REGS
STUR LR,[SP,#32]  //SAVE LR ON STACK
STUR X22,[SP,#24] // SAVE X22
STUR X21,[SP,#16] //SAVE X21
STUR X20,[SP,#8] //SAVE X20
STUR X19,[SP,#0] //SAVE X19

过程末尾只需要简单地反向执行这些指令,最后加入一条BR指令以实现返回。

6 完整的sort过程

注意for循环中对寄存器X0和X1的引用被替换成对寄存器X21和X22的引用。

//保存寄存器的值
sort:	SUBI SP,SP,#40  // MAKE ROOM ON STACK FOR 5 REGS
		 STUR LR,[SP,#32]  //SAVE LR ON STACK
	 	STUR X22,[SP,#24] // SAVE X22
	 	STUR X21,[SP,#16] //SAVE X21
		 STUR X20,[SP,#8] //SAVE X20
	 	STUR X19,[SP,#0] //SAVE X19
//过程体

		//传送参数
		MOV X21,X0 // COPY PARAMETER X0 INTO X21
		MOVE X22,X1 //COPY PARAMETER X1 INTO X22
	    //外部循环
		SUBI X20,X19,#1 //J=I-1
for2tst:CMP X20,XZR  // J AND  0 进行比较
		B.LT exit2
		LSL X10,X20,#3
		ADD X11,X0,X10// X11存放v[j]
		LDUR X12,[X11,#0]  
		LDUR X13,[X11,#8]
		CMP X12,X13
		B.LE exit2  //小于等于则跳转
	
	//传递参数和调用
		MOV X0,X21 // swap的第一个参数v
		MOV X1,X20//swap的第二个参数j
		BL swap
	//内部循环
		SUBI X20,X20,#1//J-=1;
		B for2tst
	//外部循环
exit2:  ADDI X19,X19,#1
		B for1tst

//恢复寄存器的值
exit1:  STUR X19,[SP,#0] //SAVE X19
		STUR X20,[SP,#8] //SAVE X20
		STUR X21,[SP,#16] //SAVE X21
		STUR X22,[SP,#24] // SAVE X22
		STUR LR,[SP,#32]  //SAVE LR ON STACK
		SUBI SP,SP,#40  // MAKE ROOM ON STACK FOR 5 REGS

//过程返回
		BR LR  //RETURN TO CALLING ROUTINE		

你可能感兴趣的:(计算机组成原理)