3.3 ARM寻址方式
ARM属于RISC体系架构,一个ARM汇编程序中的大部分汇编指令,基本上都和数据传输有关:在内存-寄存器、内存-内存、寄存器-寄存器之间来回传输数据。不同的ARM指令又有不同的寻址方式,比较常见的寻址方式有寄存器寻址、立即寻址、寄存器偏移寻址、寄存器间接寻址、基址寻址、多寄存器寻址、相对寻址等。
3.3.1 寄存器寻址
寄存器寻址比较简单,操作数保存在寄存器中,通过寄存器名就可以直接对寄存器中的数据进行读写。
3.3.2 立即数寻址
在立即数寻址中,ARM指令中的操作数为一个常数。立即数以#为前缀,0x前缀表示该立即数为十六进制,不加前缀默认是十进制。
3.3.3 寄存器偏移寻址
寄存器偏移寻址可以看作寄存器寻址的一种特例,通过第二个操作数operand2的灵活配置,我们可以将第二个操作数做各种左移和右移操作,作为新的操作数使用。
常见的移位操作有逻辑移位和算术移位,两者的区别是:逻辑移位无论是左移还是右移,空缺位一律补0;而算术移位则不同,左移时空缺位补0,右移时空缺位使用符号位填充。
3.3.4 寄存器间接寻址
寄存器间接寻址主要用来在内存和寄存器之间传输数据。寄存器中保存的是数据在内存中的存储地址,我们通过这个地址就可以在寄存器和内存之间传输数据。C语言中的指针操作,在汇编层次其实就是使用寄存器间接寻址实现的。寄存器间接寻址的使用示例及说明如下所示。
3.3.5 基址寻址
基址寻址其实也属于寄存器间接寻址。两者的不同之处在于,基址寻址将寄存器中的地址与一个偏移量相加,生成一个新地址,然后基于这个新地址去访问内存。
基址寻址一般用在查表、数组访问、函数的栈帧管理等场合。根据偏移量的正负,基址寻址又可以分为向前索引寻址和向后索引寻址,如上面的第1条和第3条指令,就是向后索引寻址,而第6条指令则为向前索引寻址。
3.3.6 多寄存器寻址
STM/LDM指令就属于多寄存器寻址,一次可以传输多个寄存器的值。
在多寄存器寻址中,用大括号{}括起来的是寄存器列表,寄存器之间用逗号隔开,如果是连续的寄存器,还可以使用连接符-连接,如R0-R3,就表示R0、R1、R2、R3这4个寄存器。LDM/STM指令一般和IA、IB、DA、DB组合使用,分别表示Increase After、Increase Before、Decrease After、Decrease Before。
LDM/STM指令也可以和FD、ED、FA、EA组合使用,用于堆栈操作。栈是程序运行过程中非常重要的一段内存空间,栈是C语言运行的基础,函数内的局部变量、函数调用过程中要传递的参数、函数的返回值一般都是保存在栈中的。可以这么说,没有栈,C语言就无法运行。在嵌入式系统的一些启动代码中,你会看到,在运行C语言程序之前,必须要先运行一段汇编代码初始化内存和栈指针SP,然后才能跳到C语言程序中运行。
ARM没有专门的入栈和出栈指令,ARM中的栈操作其实就是通过上面所讲的STM/LDM指令和栈指针SP配合操作完成的。栈一般可以分为以下4类。
● 递增栈A:入栈时,SP栈指针从低地址往高地址方向增长。
● 递减栈D:入栈时,SP栈指针从高地址往低地址方向增长。
● 满栈F:SP栈指针总是指向栈顶元素。
● 空栈E:SP栈指针总是指向栈顶元素的下一个空闲存储单元。
ARM默认使用满递减堆栈,通过STMFD/LDMFD指令配对使用,完成堆栈的入栈和出栈操作。ARM中的PUSH和POP指令其实就是LDM/STM的同义词,是LDMFD和STMFD组合指令的助记符。PUSH指令和POP指令的使用示例如下。
3.3.7 相对寻址
相对寻址其实也属于基址寻址,只不过它是基址寻址的一种特殊情况。特殊在什么地方呢?它是以PC指针作为基地址进行寻址的,以指令中的地址差作为偏移,两者相加后得到的就是一个新地址,然后可以对这个地址进行读写操作。ARM中的B、BL、ADR指令其实都是采用相对寻址的。
在上面的示例代码中,B LOOP指令其实就等价于:
其中OFFSET为B LOOP这条当前正在执行的指令地址与地址标号LOOP之间的地址偏移。B指令的前后跳转范围为[0,32MB],如果你编写的程序生成的二进制文件小于32MB,基本上就可以随意地使用B指令跳转了,放心吧,不会出现什么问题的。
除此之外,很多与位置无关的代码,如动态链接共享库,其在汇编代码层次的实现其实也是采用相对寻址的。程序中使用相对寻址访问的好处是不需要重定位,将代码加载到内存中的任何地址都可以直接运行。