计算机发明最初是用于处理数字的,但在进入商业应用时,计算机已经能够处理文字了。今天计算机基本都使用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 占用内存大小分析
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
小测验:
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中分支和跳转的地址范围
本节描述了将一个存储在外存(磁盘或闪存)某文件中的C程序转换为计算机上可执行程序的4个步骤。
C语言的翻译层次。用高级语言编写的程序首先被编译为汇编语言程序,然后被汇编为机器语言组成的目标模块。链接器将多个模块和库例程组合在一起解析所有的引用。加载器将机器代码加载到内存的适当位置供处理器执行。为了加速翻译过程,某些步骤被跳过或和其他步骤结合在一起。
为了识别文件类型,UNIX使用文件的后缀,x.c代表C源文件,x.s表示汇编文件,x.o代表目标文件,x.a代表静态链接库,x.so代表动态链接库,a.out默认情况下表示可执行文件。
MS-DOS使用后缀.C表示C源文件,.ASM代表汇编文件,.OBJ代表目标文件,.LIB代表静态链接库,.DLL代表动态链接库,.EXE代表可执行文件。
Java程序并不会被编译成目标计算机的汇编语言,而是首先被编译成易于解释的指令序列—Java字节码(Java bytecode)指令集。 该指令集被设计得与Java语言接近,因此编译步骤相对简单。事实上,并不需要进行任何优化。就像C语言编译器那样,Java编译器检查数据类型并且为每种数据类型生成正确的操作。Java程序最终将转化成这些字节码的二进制形式。
图片来自:《计算机组成与设计(ARM版)》英文版
一种称为Java虚拟机(Java Virtual Machine,JVM)的软件解释器能够执行Java字节码。解释器是一个用来模拟一种指令集体系结构的程序。例如,这门书提供下载的DS-5,ARMv8模拟器就是一种解释器。由于翻译非常简单,地址可以由编译器填写或在运行时被JVM发现,因此不需要单独的汇编步骤。
解释的优势是可移植性。
解释的不足是性能较差。 和传统方式编译的C程序相比,相差10倍的性能。
为了既能够保持可移植性又提高执行速度,Java发展的下一阶段是实现能够在程序执行同时进行翻译的编译器。这种即时编译器(Just In Time complier,JIT)通过记录运行的程序来找到“热点”(“热点”指程序中运行特别频繁的代码块),然后将它们编译成Java虚拟机所运行于的宿主机上的本地指令。编译过的部分被保存起来供下次程序运行时使用,从而使以后每次运行变得更快。
小测验: 和 翻译器相比,对Java开发者来说,解释器的哪些优点是最重要的?
回答: 机器独立性。即与机器无关。
一个将内存中两个不同位置的内容进行交换的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语言手动翻译成汇编语言:
在LEGv8中,使用寄存器X0到X7进行参数传递。由于swap只有两个参数v和k,因此可以被分配给寄存器X0和X1.仅剩的另一个变量是tmp,由于swap是一个叶过程,我们将其分配给寄存器X9.
寄存器分配如下表:
数组v | k | tmp |
---|---|---|
X0 | X1 | X9 |
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是一个叶过程)中的保留寄存器,因此没有需要保留的东西。
现在得到完整的例程,包括过程标签以及返回的跳转指令。
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 //过程返回
该例中,我们将编写一个调用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]
}
}
寄存器X0和X1分配给过程sort的两个参数v和n,寄存器X19和X20分配给变量i和j。
寄存器分配如下表:
数组v | n | i | j |
---|---|---|---|
X0 | X1 | X19 | X20 |
过程体包含两个嵌套的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 循环最后仅仅跳回循环条件判断的地方: 第一个for循环的框架代码如下: 第二个for循环的C语句如下: 这个循环的初始化部分仍然只需要一条指令: 循环末尾j的递减也只需一条指令: 循环条件测试由两部分组成,任何一个条件为假就退出循环。因此,如果第一个条件测试为假(j<0),循环就要退出: 这条跳转指令将跳过第二个条件测试。如果没有跳过,则j≥0. 下面来看第二个条件,当v[j]≤v[j+1]时为假。 现在取出v[j] (放在寄存器X12中): 因为第二个元素恰好是顺序下一个双字,因此将寄存器X11中的地址值加8就可以取出v[j+1] (存放在X13中): 测试v[j]≤v[j+1],以判断是否跳出循环: 循环末尾跳转到内存循环测试处: 将这些代码片组合起来就可以得到第二个for循环的框架: 下一步处理第二个for循坏体: 调用swap足够简单(一条BL指令即可实现): 当我们想传递参数时问题出现了,因为sort过程需要使用寄存器X0和X1中的值,而swap过程需要将其参数放入相同的寄存器中。一种解决办法是在过程执行的早期就将sort的参数复制到其他寄存器中,让出X0和X1寄存器供swap过程使用。(这种复制要比使用栈进行保存和恢复快得多。)在过程中,首先将寄存器X0和X1的值用如下方法复制到X21和X22中: 然后用下面两条指令将参数传递给swap: swap(v,j); 剩下的代码保存和恢复寄存器的值。显然,我们必须将返回地址保存在寄存器LR中,因为sort是一个过程并且本身也被调用。sort过程还使用了由被调用者保存的寄存器X19,X20,X21和X22,这些寄存器的值也必须被保存。因此,sort过程开头的代码如下: 过程末尾只需要简单地反向执行这些指令,最后加入一条BR指令以实现返回。 注意for循环中对寄存器X0和X1的引用被替换成对寄存器X21和X22的引用。for1tst: CMP X19,X1 //比较X19和X1(也就是i和n)
B.GE exit1 //如果(i≥n)则跳转 到exit1
B for1tst // branch to test of outer loop
exit1:
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(j=i-1;j>=0&&v[j]>v[j+1];j--)//从小到大排序
SUBI X20,X10,#1 //j=i-1
SUBI X20,X20,#1//J-=1;
for2tst:CMP X20,XZR
B.LT exit2 //if (j<0) go to exit2
获取v[j]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 //小于等于则跳转
B for2tst
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 中的过程调用
swap(v,j);
BL swap
4 sort中的参数传递
MOV X21,X0 // COPY PARAMETER X0 INTO X21
MOVE X22,X1 //COPY PARAMETER X1 INTO X22
MOV X0,X21 // swap的第一个参数v
MOV X1,X20//swap的第二个参数j
5 在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
6 完整的sort过程
//保存寄存器的值
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