3.4 ARM伪指令
什么是ARM伪指令?顾名思义,ARM伪指令并不是ARM指令集中定义的标准指令,而是为了编程方便,各家编译器厂商自定义的一些辅助指令。伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个:ADR、ADRL、LDR、NOP,它们的使用示例如下。
NOP伪指令比较简单,其实就相当于MOV R0,R0。在以后的学习和工作中,大家在ARM汇编程序中经常看到的就是LDR伪指令。
3.4.1 LDR伪指令
LDR伪指令通常会让很多朋友感到迷惑,容易和加载指令LDR混淆。通过上面的学习,我们已经知道,ARM属于RISC架构,不能对内存中的数据直接操作,ARM通常会使用LDR/STR这对加载/存储指令,先将内存中的数据加载到寄存器,然后才能对寄存器中的数据进行操作,最后把寄存器中的处理结果存储到内存中。LDR伪指令的主要用途是将一个32位的内存地址保存到寄存器中。
在寄存器之间传递数据可以使用MOV指令,但是当传递的一个内存地址是32位的立即数时,MOV指令就应付不了了,如下面的第2条指令。
当我们往寄存器传递的地址是一个32位的常数时,为什么不能使用MOV,而要使用LDR伪指令呢?这还得从ARM指令的编码格式说起。RISC指令的特点是单周期指令,指令的长度一般都是固定的。在一个32位的系统中,一条指令通常是32位的,指令中包括操作码和操作数,如图3-5所示。
图3-5 ARM指令的编码格式
指令中的操作码和操作数共享32位的存储空间:一般前面的操作码要占据几个比特位,剩下来的留给操作数的编码空间就小于32位了。当编译器遇到MOV R0,#0x30008000这条指令时,因为后面的操作数是32位,编译器就无法对这条指令进行编码了。为了解决这个难题,编译器提供了一个LDR伪指令来完成上面的功能。
在上面的示例代码中,LDR不是普通的ARM加载指令,而是一个伪指令。为了与ARM指令集中的加载指令LDR区别开来,LDR伪指令中的操作数前一般会有一个等于号=,用来表示该指令是个伪指令。通过LDR伪指令,编译器就解决了向一个寄存器传送32位的立即数时指令无法编码的难题。
因为伪指令并不是ARM指令集中定义的标准指令,所以CPU硬件译码电路并不支持直接运行这些伪指令。在程序编译期间,这些伪指令会被标准的ARM指令替代。编译器在处理伪指令时,根据伪指令中的操作数大小,会使用不同的ARM标准指令替代。如当LDR伪指令中的操作数小于8位时,LDR伪指令一般会被MOV指令替代。下面的两行汇编指令其实是等价的。
当LDR伪指令中的操作数大于8位时,LDR伪指令会被编译器转换为LDR标准指令+文字池的形式。
在上面的示例代码中,当LDR伪指令中的操作数为一个32位的立即数时,编译器会首先在内存中分配一个4字节大小的存储单元,然后将这个32位的地址0x30008000存放到该存储单元中,该存储单元通常也叫作文字池(literal pool)。接着编译器计算出该存储单元到LDR伪指令之间的偏移OFFSET,然后使用寄存器相对寻址,就可以将这个32位的立即数送到R0寄存器中。偏移量OFFSET的大小一般要小于4KB,所以在分析汇编代码时你会看到,存放这些32位地址常量的文字池一般紧挨着当前指令的代码段,直接放置在当前代码段的后面。
搞清楚了LDR指令和LDR伪指令之间的区别和各自的用途,以后在汇编代码中再遇到这两个指令,我们就可以不慌不忙、从容应对了。不要不在意这些细节,当你阅读代码时,这些模棱两可的细节问题往往更容易成为阅读障碍和拦路虎。
3.4.2 ADR伪指令
ADR伪指令的功能与LDR伪指令类似,将基于PC相对偏移的地址值读取到寄存器中。ADR为小范围的地址读取伪指令,底层使用相对寻址来实现,因此可以做到代码与位置无关。ADR伪指令的使用示例代码如下。
在上面的示例代码中,ADR伪指令的作用是将标号LOOP表征的内存地址送到寄存器R0中。编译器在编译ADR伪指令时,会首先计算出当前正在执行的ADR伪指令地址与标号LOOP之间的地址偏移OFFSET,然后使用ARM指令集中的一条标准指令代替之,如使用ADD指令将标号表征的地址送到寄存器R0中。
ADR伪指令和LDR伪指令的相似之处在于:两者都是为了加载一个地址到指定的寄存器中。两者的不同之处在于:LDR伪指令通常被翻译为ARM指令集中的LDR或MOV指令,而ADR伪指令则通常会被ADD或SUB指令代替。在用途上,LDR伪指令主要用来操作外部设备的寄存器,而ADR伪指令主要用来通过相对寻址,生成与位置无关的代码。在一个程序中,只要各个标号之间的相对位置不变,使用ADR伪指令就可以做到与位置无关,将指令代码加载到内存中的任何位置都可以正常运行。在寻址方式上,LDR使用绝对地址,而ADR则使用相对地址,LDR和ADR伪指令的地址适用范围也不同,LDR伪指令适用的地址范围为[0,32GB],而ADR伪指令则要求当前指令和标号必须在同一个段中,地址偏移范围也较小,地址对齐时偏移范围为[0,1020],地址未对齐时偏移范围为[0,4096]。