现在我们已经掌握了所有知识,可以编写简单的ARM汇编程序,但如果要编写较为复杂的ARM程序,就必须掌握更多的寻址模式和指令,这就是本节的重点所在。
我们在“基本寻址模式与基本指令”中学习了最常用的3种寻址方式。下面介绍其它寻址方式。
1.6.1其它常见寻址模式
1. 基址寻址
基址寻址就是将基址寄存器的内容与指令中给出的偏移量相加,形成操作数的有效地址。基址寻址用于访问基址附近的存储单元,常用于查表、数组操作、功能部件寄存器访问等。基址寻址指令举例如下:
LDR R1,[R2,#0x0C]
R2的值+0x0C形成内存地址,读取内存中该地址上的内容,放入R1
其它额外需要了解的内容:
l 零偏移。 如:LDR R0,[R1]
l 前索引偏移。 如:LDR R0,[R1,#0x04]!,表示将R1的值加上4后作为内存地址,将该内存处存放的数读出送给R0;并且指令执行结束时,R1本身的值也要加4。这里!表示要回写R1
l 程序相对偏移。
如:LDR R0,labe1,表示将标号label所代表的内存地址处存放的内容放入R0,相当于
LDR R0, [PC, #某个常数]
ldr r0, label
......
label DCD 0x12345678
l 后索引偏移。 如:LDR R0,[R1],#0x04,表示将R1的值作为内存地址,将该内存处存放的数读出送给R0;并且指令执行结束时,R1本身的值要加4
2. 多寄存器寻址
多寄存器寻址一次可传送几个寄存器值,允许一条指令传送16个寄存器的任何子集或所有寄存器。多寄存器寻址指令举例如下:
LDMIA R1!,{R2-R4,R6} ,它是ldr的多寄存器版本,将内存中连续存放的4个字加载到寄存器R2,R3,R4,R6中。R1中存放的是内存地址。
图1 - 64 LDMIA指令执行前 图1 - 65 LDMIA指令执行后
两点说明:
(1)R1!中的!号表示在指令执行完成后,要改变(回写)基址寄存器(R1)的值
(2)寄存器列表{R2-R4, R6}中的顺序并不要紧。最终寄存器与内存地址的对应关系是:编号小的寄存器与内存的低地址相对应
两点问题:
(1)为什么内存起地址是0x40000000,而不是0x40000004
(2)为什么内存地址是从0x40000000 ---- 0x4000000C,而不是从0x3FFFFFF4 ---- 0x40000000
要解释上面2个问题,其实也很简单。其实多寄存加载指令ldm总共有4个:ldmia、ldmib、 ldmda、ldmdb。ia的意思是increaseafter,ib的意思是increasebefore,da的意思是decreaseafter,db的意思是decreasebefore。以LDMIA R1!, {R2-R4, R6}为例子,这里的ia是指办事(将内存中的数加载到寄存器)之后增加基址寄存器(R1)的值。这条指令的执行过程从逻辑(实际上一条指令肯定是原子操作,所以从物理上看,下面8个步骤其实是同时完成的)上看,如下:
(1)先办事:将R1的值(0x40000000)作为内存地址,到该地址处取得数(0x01),加载到寄存器R2中
(2)后增加:将R1的值从0x40000000增加为0x40000004
再重复上面的操作3次,分别将内存中的数0x02、0x03、0x04放到寄存器中R3、R4、R6中,最后R1的值变为0x40000010。
这个例子中,如果将ldmia改为ldmib,则R2、R3、R4、R6中存放的是0x02、0x03、0x04、内存0x40000010处的内容,最后R1的值为0x40000010。
除了4条多寄存器加载指令外,还有4条类似的多寄存器存储指令,分别是stmia、 stmib、 stmda、 stmdb
3. 堆栈寻址
由于ARM指令集没有专门的出栈和入栈指令,所以ARM汇编程序是采用SP作为栈指针,以stm指令完成入栈操作,以ldm指令完成出栈操作。
以入栈后SP的值是增加还是减少为依据,可将堆栈类型划分为递增堆栈(向上生长)和递减堆栈(向下生长);
图1 - 66 递增堆栈与递减堆栈
以SP所指向的内存是栈顶元素所在位置,还是下一次要入栈的元素的位置,可将堆栈类型划分为满堆栈和空堆栈
图1 - 67 满堆栈与空堆栈
那么当堆栈类型为空递减堆栈时候,入栈操作应该使用什么指令?出栈操作应该使用什么指令?进一步,如果堆栈类型为空递增、满递增、满递减堆栈,又将如何呢? 如果你不看下面的答案,我相信你一定会让这几个问题折磨得做很多的脑力体操,然后感叹ARM指令集的设计者太不为你这样的程序员考虑了,给了你本不应该由你承担的负荷。但事实上正相反,ARM指令集的设计者充分理解了你作为程序员的苦恼,请看下面的答案。
表1 - 8 堆栈类型与堆栈操作
数据块传送(存储) | 堆栈操作(入栈) | 说明 |
STMDA | STMED | 空递减 |
STMIA | STMEA | 空递增 |
STMDB | STMFD | 满递减 |
STMIB | STMFA | 满递增 |
数据块传送(加载) | 堆栈操作(出栈) | 说明 |
LDMDA | LDMFA | 满递增 |
LDMIA | LDMFD | 满递减 |
LDMDB | LDMEA | 空递增 |
LDMIB | LDMED | 空递减 |
这张表的第一、三列回答了前面你绞尽脑汁回答的问题。而第二列则体现了ARM指令集的设计者对作为程序员的你的充分体贴。第二列中的ED、EA、FD、 FA分别表示empty descend(空递减)、 empty ascend(空递增)、 full descend(满递减)、 full ascend(满递增),其含义是说,如果你采用的是空递减(空递增、满递减、满递增)堆栈的话,入栈操作则使用指令STMED(STMEA、 STMFD、STMFA),出栈操作则使用指令LDMED(LDMEA、LDMFD、LDMFA)。从此你再也不会为你应该使用ia、ib、da还是db 来实现出、入栈操作而苦恼了。
STMED、STMEA、STMFD、STMFA和LDMED、LDMEA、LDMFD、LDMFA就是所谓的堆栈寻址指令。由此可见:为了对程序员体贴入微,ARM指令集的设计者设计了堆栈寻址指令,其实质就是多寄存寻址指令的快捷方式。
4. 寄存器移位寻址
寄存器移位寻址是ARM指令集特有的寻址方式。当第2个操作数是寄存器移位方式时,第2个寄存器操作数在与第1个操作数结合之前,选择进行移位操作。例如:
MOV R0,R2,LSL #3 表示将R2的值逻辑左移3位,结果放入R0,即是R0=R2×8。
移位的方式有以下几种:
图1 - 68 移位操作类型
LSL(logic shift left):逻辑左移
LSR(logic shift right):逻辑右移
ASR(arithmetic shift right):算术右移
ROR(rotate shift right):循环右移
RRX(rotate shift right with extend):带扩展的循环右移。其中的C指的是CPSR的C位
5. 相对寻址
相对寻址是基址寻址的一种变通。由程序计数器PC提供基准地址,指令中的地址码字段作为偏移量,两者相加后得到的地址即为操作数的有效地址。例如:
B LOOP
...
LOOP MOV R6,#1
该条B指令的意思是要跳转到标号LOOP所代表的指令处,其含义相当明显,但你要明白CPU根本不明白标号是个什么东西(事实上在指令的机器码中根本就没有标号这种东西),那么b loop这条指令的机器码会是什么呢?答案是:高8bit是操作码相关内容,低24bit是一个常数,表示从b指令到mov指令之间的内存地址的差值(如果不考虑流水线的影响的话)。由此可见,b loop这条指令相当于add pc, pc, #偏移量常数,典型的相对于PC(当前指令地址)的相对寻址。由于是相对于当前指令地址进行相对寻址,所以无论程序最终运行在内存的何处(即使运行的地址不是它预期的位置),这条B指令都能正确运行。关于相对寻址、程序期望的运行地址等等,将在“ARM汇编伪指令”中详细描述。
随便说一下,前面学到b指令的跳转范围是当前指令的前后32M,为什么是这个范围呢?因为24bit常数用1个比特区别正负,还剩23bit,同时由于ARM指令在内存中的地址的最低2bit一定是0(为什么?请自行思考一下),因此23bit中可以不必表示这2个0,所以23bit可以表示的范围是0 ---- 2^25,即:0 ---- 32M。
关于指令的机器码编码格式,请参阅:光盘中提供的技术文档“ARM Architecture Reference Manual.pdf”(位于\docs目录)
1.6.2其它常见指令(访存指令、数据处理指令、乘法指令)
我们在“基本寻址模式与基本指令”中学习了最常用的指令。下面介绍其它较为常用的指令。
1. 访存指令
LDRH(半字加载);
LDRSH (有符号半字加载);
STRH(半字存储);
交换指令
表1 - 9 2个交换指令
助记符 | 说明 | 操作 |
SWP Rd,Rm,[Rn] | 寄存器和存储器字进行数据交换 | 同时完成Rd←[Rn],[Rn]←Rm (Rn≠Rd或Rm) |
SWPB Rd,Rm,[Rn] | 寄存器和存储器字节进行数据交换 | 同时完成Rd←[Rn],[Rn]←Rm (Rn≠Rd或Rm) |
2. 数据处理指令
表1 - 10 数据传送指令
助记符 | 说明 | 操作 |
MVN Rd,operand2 | 数据非传送 | Rd←(~operand2) |
表1 - 11 算术运算指令
助记符 | 说明 | 操作 |
RSB Rd, Rn, operand2 | 逆向减法指令 | Rd←operand2-Rn |
ADC Rd, Rn, operand2 | 带进位加法 | Rd←Rn+operand2+Carry |
SBC Rd, Rn, operand2 | 带进位减法指令 | Rd←Rn-operand2-(NOT)Carry |
RSC Rd, Rn, operand2 | 带进位逆向减法指令 | Rd←operand2-Rn-(NOT)Carry |
这里要特别提到,ADC指令结合CPSR,可以实现64位整数加法。
表1 - 12 逻辑运算指令
助记符 | 说明 | 操作 |
BIC Rd, Rn, operand2 | 按位清除指令 | Rd←Rn & (~operand2) |
其实现功能是:将Rn中对应于operand2中为1的bit位全部清0,其它bit位保持不变,然后将结果保存到Rd中
表1 - 13 比较指令
助记符 | 说明 | 操作 |
CMN Rn, operand2 | 负数比较指令 | 标志N、Z、C、V←Rn+operand2 |
TST Rn, operand2 | 位测试指令 | 标志N、Z、C←Rn & operand2 |
TEQ Rn, operand2 | 相等测试指令 | 标志N、Z、C←Rn ^ operand2 |
TST指令测试的是:Rn中所有指定bit位是否全为0(指定的bit位是operand2中为1的所有位);
TEQ指令测试的是:Rn和operand2是否相等。这点上与CMP指令一样,区别在于CMP指令除了可以比较2个数是否相等外,也可以比较2个数谁大谁小,但TEQ不行。
3. 乘法指令
表1 - 14 乘法指令
助记符 | 说明 | 操作 |
MUL Rd,Rm,Rs | 32位乘法指令 | Rd←Rm*Rs (Rd≠Rm) |
MLA Rd,Rm,Rs,Rn | 32位乘加指令 | Rd←Rm*Rs+Rn (Rd≠Rm) |
UMULL RdLo,RdHi,Rm,Rs | 64位无符号乘法指令 | (RdLo,RdHi) ←Rm*Rs |
UMLAL RdLo,RdHi,Rm,Rs | 64位无符号乘加指令 | (RdLo,RdHi) ←Rm*Rs+(RdLo,RdHi) |
SMULL RdLo,RdHi,Rm,Rs | 64位有符号乘法指令 | (RdLo,RdHi) ←Rm*Rs |
SMLAL RdLo,RdHi,Rm,Rs | 64位有符号乘加指令 | (RdLo,RdHi) ←Rm*Rs+(RdLo,RdHi) |
4. 协处理器指令
参见“MMU与内存保护的实现”
5. 杂项指令
SWI:软中断指令,参见“swi与systemcall的实现”
MRS、MSR:程序状态寄存器操作指令,参见“ARM异常处理”