本文整理于博文ARM指令ldr和adr的区别_ldr adr,有修改。
我们先弄清楚ldr与adr的使用格式。adr是伪指令,而ldr根据使用的格式可分为ldr指令与ldr伪指令。
格式:adr{cond} Rd,addr
功能:将基于PC相对偏移的地址值或者基于寄存器相对偏移的地址值,读取到目标寄存器Rd中。
格式:ldr{cond} Rd,addr
功能:将addr这个存储器地址对应的存储单元中的数据,加载到目标寄存器Rd中。
格式:ldr{cond} Rd,=addr
功能:将一个32位的立即数,或者一个地址值,加载到目标寄存器Rd中。
从上面的描述中可以看出,ldr作为加载指令时,Rd这个寄存器中存储的是addr这个地址对应的存储单元中的数据(指针所指向的内容)。而ldr作为伪指令时,它与adr伪指令一样,Rd这个寄存器中存储的就是addr这个地址(即指针)。这也是ldr伪指令、adr伪指令又叫作(大范围/小范围)地址读取伪指令的原因,因为它们就是用来获取一个地址(至于为什么有前缀“大范围”“小范围”,是因为它们获取地址的方式不一样,导致能够获取的地址范围不同。这有点废话,不过确实如此。见接下来的描述)。
比如有一个汇编文件test_adr.S,其内容如下:
.text //伪操作
.globl _start //伪操作,将_start这个标号导出,即作为全局变量_start: //这是一个标号,这个标号表示一个地址,就好比门牌号ldr r0,test @ 指令 ,表示r0寄存器存储的是test这个地址对应的存储单元中的数据adr r0,test //伪指令,表示r0寄存器存储的是test这个地址(或者说标号,标号即地址) ldr r0,=test //伪指令,表示r0寄存器存储的是test这个地址(或者说标号,标号即地址)nop
test:nop
另外有一个Makefile文件,其内部对test_adr.S进行汇编、链接与反汇编:
all:test_adr.S#汇编:将汇编文件test_adr.S转换成目标文件test_adr.o@ arm-linux-gcc -c -o test_adr.o test_adr.S #链接:将目标文件的集合,组合成可执行程序test_adr.elf@ arm-linux-ld -Ttext 0x00000000 -g test_adr.o -o test_adr.elf#复制:将可执行程序test_adr.elf从一种二进制格式(elf)转换成另外一种格式(bin)@ arm-linux-objcopy -O binary -S test_adr.elf test_adr.bin #反汇编:将可执行文件test_adr.elf反汇编,并将结果输出到test_adr.dis文件(否则输出至终端)@ arm-linux-objdump -d -m arm test_adr.elf > test_adr.disclean:@ rm -f test_adr.dis test_adr.bin test_adr.elf *.o
注意到 adr r0,test 与 ldr r0,=test 都是将test这个标号所在的地址存储到r0寄存器中,它们有什么区别呢?
我们看一下反汇编生成的test_adr.dis文件的内容:
xjh@ubuntu:~/iot/tmp$ cat test_adr.dis test_adr.elf: file format elf32-littlearmDisassembly of section .text:
//因为链接地址是00000000,所以这里显示为00000000
00000000 <_start>:0: e59f0008 ldr r0, [pc, #8] ; 10 4: e28f0004 add r0, pc, #48: e59f0004 ldr r0, [pc, #4] ; 14 c: e1a00000 nop ; (mov r0, r0)00000010 :10: e1a00000 nop ; (mov r0, r0)14: 00000010 .word 0x00000010
xjh@ubuntu:~/iot/tmp$
首先要明白,为了提高处理的效率,ARM采用了流水线技术,使用最广泛的是三级流水线,这意味着取指操作与执行操作相差两个周期,因为每个周期4个字节,所以差8个字节。因为PC总是指向取指的指令,而非指向正在执行的指令或者正在译码的指令,所以PC值=当前程序执行位置+8。
(1)由反编译结果可知,代码 ldr r0,test 等价于 ldr r0, [pc, #8] 指令。ldr r0, [pc, #8] 这条指令对应的二进制码e59f0008,它存放在地址0对应的存储单元中,也就是执行到这指令时,当前程序执行位置是0,则PC值=0+8(即指向了指令ldr r0, [pc, #4])。而[pc,#8]表示PC的值再加上8,所以[pc,#8]表示0+8+8=0d16=0x10这个地址所对应的存储单元中的数据([ ]表示间接寻址)。我们到0x10这个地址,看到它对应的存储单元中的数据是e1a00000,这个数据其实是指令nop对应的二进制码。
(2)由反编译结果可知,代码 adr r0,test 等价于 add r0, pc, #4 指令。注意这条指令的位置是4,也就是执行到这条指令时,当前执行位置是4,那么PC值=4+8(即指向了第一个指令nop)。add r0, pc, #4 指令表示将PC的值再加上4,然后存储到r0寄存器中,那么r0中存储的内容就是4+8+4=0d16=0x10。这个0x10表示的是一个地址,也就是test这个标号所在的地址。总结之,adr r0,test 就是把标号test所在的地址取出来,存储到r0寄存器中。
(3)由反编译结果可知,代码 ldr r0,=test 等价于 ldr r0, [pc, #4] 指令。注意这条指令的位置是8,也就是执行到这条指令时,当前执行位置是8,那么PC值=8+8。[pc, #4]表示PC的值再加上4,所以[pc, #4]表示8+8+4=0d20=0x14这个地址所对应的存储单元中的数据。我们到0x14这个地址,看到它对应的存储单元中的数据是00000010,这个数据表示test这个标号所在的地址。总结之,ldr r0,=test就是把标号test所在的地址取出来,存储到r0寄存器中。
从上面的描述可知,adr伪指令中r0寄存器所存储的数据,我们(根据反汇编代码)可以明确地知道它是一个地址。标号所在的地址=(标号所在的地址-当前执行指令的地址)+当前执行指令的地址。当前执行指令的地址是PC的值-8,虽然不直接就是PC,但依然是一个与PC有关的值。也就是说,使用adr伪指令获取某个标号的地址时,这个标号的地址是与运行地址(体现为PC)有关的,或者说,它是基于PC相对偏移的地址值。
ldr伪指令获取某个标号的地址时,这个标号的地是与链接地址有关的。链接脚本写好并执行链接之后,可执行文件中各条指令、各个标号的地址就已经定下来了。
当链接地址与运行地址一致时,使用adr伪指令与使用ldr伪指令,它们获取的标号地址是一样的。
当链接地址与运行地址不同时,使用adr伪指令获取的是程序实际运行时标号所在的地址,而使用ldr伪指令获取的是链接脚本指导下的该标号所在的地址。
我们可以利用这两个伪指令取址时的差异,判断是否需要重定位。见博客https://xiefor100.blog.csdn.net/article/details/70135880。