ARM汇编编程基础之六 —— 其它寻址模式与其它指令
现在我们已经掌握了所有知识,可以编写简单的ARM汇编程序,但如果要编写较为复杂的ARM程序,就必须掌握更多的寻址模式和指令,这就是本文的重点所在。
我们在“基本寻址模式与基本指令”一文中学习了最常用的3种寻址方式。下面介绍其它寻址方式。
1、基址寻址
基址寻址就是将基址寄存器的内容与指令中给出的偏移量相加,形成操作数的有效地址。基址寻址用于访问基址附近的存储单元,常用于查表、数组操作、功能部件寄存器访问等。基址寻址指令举例如下:
LDR R1,[R2,#0x0C]
R2的值+0x0C形成内存地址,读取内存中该地址上的内容,放入R1
其它额外需要了解的内容:
§零偏移。 如:LDR R0,[R1]
§前索引偏移。 如:LDR R0,[R1,#0x04]!,表示将R1的值加上4后作为内存地址,并且指令执行结束时,R1本身的值也要加4。这里!表示要回写
§程序相对偏移。 如:LDR R0,labe1,表示将标号label所代表的地址处存放的内容放入R0,相当于LDR R0, [PC, #某个常数]
§后索引偏移。 如:LDR R0,[R1],#0x04,表示将R1的值作为内存地址,并且指令执行结束时,R1本身的值要加4
2、多寄存器寻址
多寄存器寻址一次可传送几个寄存器值,允许一条指令传送16个寄存器的任何子集或所有寄存器。多寄存器寻址指令举例如下:
LDMIA R1!,{R2-R4,R6} ,它是ldr的多寄存版本,将内存中的4个字放入寄存器R2,R3,R4,R6中
指令执行前 指令执行后
两点说明:
1)、R1!中的!号表示在指令执行完成后,要改变(回写)基址寄存器(R1)的值
2)、寄存器列表{R2-R4, R6}中的顺序并不要紧。最终寄存器与内存地址的对应关系是:编号小的寄存器与内存的低地址相对应
两点问题:
1)、为什么内存起地址是0x40000000,而不是0x40000004
2)、为什么内存地址是从0x40000000 ---- 0x4000000C,而不是从0x3FFFFFF4 ---- 0x40000000
要解释上面2个问题,其实也很简单。其实多寄存加载指令ldm总共有4个:ldmia、 ldmib、 ldmda、 ldmdb。ia的意思是increase after,ib的意思是increase before,da的意思是decrease after,db的意思是decrease before。以LDMIA R1!, {R2-R4, R6}为例子,这里的ia是指办事(将内存中的数加载到寄存器)之后增加基址寄存器(R1)的值。这条指令的执行过程从逻辑上看,如下:
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条类似的多寄存器存储指令,分别是stria、 strib、 strda、 strdb
3、堆栈寻址
由于ARM指令集没有专门的出栈和入栈指令,所以ARM汇编程序是采用SP作为栈指针,以stm指令完成入栈操作,以ldm指令完成出栈操作。
以入栈后SP的值是增加还是减少为依据,可将堆栈类型划分为递增堆栈(向上生长)和递减堆栈(向下生长);
以SP所指向的内存处存放的是栈顶元素还是下一次要入栈的元素,可将堆栈类型划分为满堆栈和空堆栈
那么当堆栈类型为空递减堆栈时候,入栈操作应该使用什么指令?出栈操作应该使用什么指令?进一步,如果堆栈类型为空递增、满递增、满递减堆栈,又将如何呢?如果你不看下面的答案,我相信你一定会让这几个问题折磨得做很多的脑力体操,然后感叹ARM指令集的设计者太不为你这样的程序员考虑了,给了你本不应该由你承担的负荷。但事实上正相反,ARM指令集的设计者充分理解了你作为程序员的苦恼,请看下面的答案。
数据块传送 | 堆栈操作 | 说明 |
存储 | 压栈 | |
STMDA | STMED | 空递减 |
STMIA | STMEA | 空递增 |
STMDB | STMFD | 满递减 |
STMIB | STMFA | 满递增 |
数据块传送 | 堆栈操作 | 说明 |
加载 | 出栈 | |
LDMDA | LDMFA | 满递增 |
LDMIA | LDMFD | 满递减 |
LDMDB | LDMEA | 空递增 |
LDMIB | LDMED | 空递减 |
这2张表的第一、三列回答了前面你绞尽脑汁回答的问题。而第二列则体现了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。
移位的方式有以下几种:
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。
我们在“基本寻址模式与基本指令”一文中学习了最常用的指令。下面介绍其它较为常用的指令。
1、访存指令
LDRH(半字加载);LDRSH (有符号半字加载);STRH(半字存储)
交换指令
助记符 |
说明 |
操作 |
SWP Rd,Rm,[Rn] |
寄存器和存储器字数据交换 |
Rd←[Rn],[Rn]←Rm (Rn≠Rd或Rm) |
SWPB Rd,Rm,[Rn] |
寄存器和存储器字节数据交换 |
Rd←[Rn],[Rn]←Rm (Rn≠Rd或Rm) |
2、数据处理指令
助记符 |
说明 |
操作 |
MVN Rd,operand2 |
数据非传送 |
Rd←(~operand2) |
助记符 |
说明 |
操作 |
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位整数加法,详情参见“杂项解释”一文
助记符 |
说明 |
操作 |
BIC Rd, Rn, operand2 |
按位清除指令 |
Rd←Rn & (~operand2) |
其实现功能是:将Rn中对应于operand2中为1的bit位全部清0,然后将结果保存到Rd中
助记符 |
说明 |
操作 |
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、乘法指令
助记符 |
说明 |
操作 |
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与system call的实现”一文
MRS、MSR:程序状态寄存器操作指令,参见“ARM异常处理”一文