ARM64/AArch64汇编使用LDR
命令从内存加载数据到寄存器,指定加载的源内存地址有多种方式,其中一种是LDR Rx Label
的方式。对这条命令心中一直心存疑惑,所以这里对LDR
命令使用Lable加载数据的方式作一些剖析,拨开迷雾,找到答案。话不多说,首先看一段GNU ARM64汇编示例代码:
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 // global_var.s .data .balign 8 myvar_a: .dword 3 myvar_b: .dword 7 .text .global _start _start: ldr x1, addr_myvar_a // ldr x0, [x1] add x0, x0, #1 ret addr_myvar_a: .dword myvar_a // addr_myvar_b: .dword myvar_b
代码文件名为global_var.s
,使用以下命令编译,生成global_var
:
1 2 3 4 $ as global_var.s -o global_var.o $ ld global_var.o -o global_var $ file global_var global_var: ELF 64 -bit LSB executable, ARM aarch64, version 1 (SYSV), statically linked, not stripped
代码比较简单,逐个问题来看。
1.关于Label 汇编代码中lablel
就是代表地址,是地址的符号名称。
上面代码中myvar_a和myvar_b
分别代表了2个全局变量的地址,该地址处存储的内容是3和7;可以理解C语言中指向变量的指针,即 myvar_a == &var_a1
;
addr_myvar_a和add_myvar_b
分别包含了2个全局变量的地址(也就是 myvar_a和myvar_b代表的地址值);
总起来就可以理解为:在text段的addr_myvar_a
处存储了myvar_a
的地址值,label myvar_a
代表的是全局变量的地址,该地址处存储的内容是3
使用objdump
验证一下,详细见如下注释:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ objdump -s -d global_var global_var: file format elf64-littleaarch64 Contents of section .text: 4000 b0 81000058 200040 f9 00040091 c0035fd6 ...X .@......._. 4000 c0 d0004100 00000000 d8004100 00000000 ..A.......A..... Contents of section .data: 4100 d0 03000000 00000000 07000000 00000000 ................ //地址4100 d0处内容为3 地址4100 d8处内容为7 //myvar_a代表的地址是4100 d0 //myvar_b代表的地址是4100 d8 Disassembly of section .text: 00000000004000 b0 <_start>: 4000 b0: 58000081 ldr x1, 4000 c0 <addr_myvar_a> 4000 b4: f9400020 ldr x0, [x1] 4000 b8: 91000400 add x0, x0, 4000 bc: d65f03c0 ret //addr_myvar_a处的内容为004100 d0,等于myvar_a代表的地址 00000000004000 c0 <addr_myvar_a>: 4000 c0: 004100 d0 .word 0 x004100d0 4000 c4: 00000000 .word 0 x00000000 //addr_myvar_b处的内容为004100 d8,等于myvar_b代表的地址 00000000004000 c8 <addr_myvar_b>: 4000 c8: 004100 d8 .word 0 x004100d8 4000 cc: 00000000 .word 0 x00000000
好了,关于label含义搞明白了。
这里有个疑惑了,既然我们已经有了label myvar_a
代表了全局变量3的地址,为什么LDR
不直接使用myvar_a
,而还需要在代码段中使用addr_myvar_a
保存该地址呢 ?
简单的答案是:
【注意:这个限制只针对ARM32汇编存在,实测使用AArch64/ARM64汇编可以从text段中访问data段中的label!!】
myvar_a
和addr_myvar_a
在不同的段中,前者在data段中,而后者在text 段中;我们无法从代码段中直接访问另一个段中符号,因此需要在代码段中增加一个特殊的label,指向数据段中某个目标的地址。(We cannot directly access a symbol from one section to another one. Thus, we need a special label in .code
which refers to the address of an entity in .data
section.)
2.关于Literal Pool 1 2 addr_myvar_a: .dword myvar_a addr_myvar_b: .dword myvar_b
addr_myvar_a和addr_myvar_b所在的位置在text段的底部 ,称为Litera Pool
:和代码位于同一个段中,用来保存字面常量,字符串或者在position-independent中可以被引用的偏移量。既然是Literal Pool,那么此处保存的内容值都是字面常量,即在代码运行前而非运行时就确定的值 。
那么问题来了,label addr_myvar_a
处存储的是变量的内存地址,但是内存的地址值如何在运行前就能确定下来成为常量呢?
答案要从编译链接时的的ld中寻找。
在Linux GNU工具链中,ld负责链接生成最终的目标文件;ld使用链接脚本ld scrip指定目标文件中各个段应该被链接到的基地址。因此,ld决定了目标文件在执行时,应该被加载到内存的目标地址(当然是虚拟地址)。这就容易解释了,ld在链接时刻就指定了每个目标在运行时的虚拟内存地址,运行时加载器根据ld的决定乖乖将程序加载到目标地址执行就好了。
所以,在链接时刻就能确定addr_myvar_a处保存的myvar_a的地址值,也能确定ldr x1, addr_myvar_a
中addr_myvar_a的内存地址。
我们通过objdump
和运行时的gdb调试来验证一下上述理解。
首先,看一下默认的ld script中定义的链接基地址,我们只关注定义段的基地址部分。可以看到默认的链接脚本中定义的基地址为:0x400000
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #查看默认ld script $ ld --verbose GNU ld (GNU Binutils for Debian) 2.33.1 Supported emulations: aarch64linux aarch64elf aarch64elf32 aarch64elf32b aarch64elfb ## skip some lines SECTIONS { /* Read-only sections, merged into text segment: */ PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS; ... ## ship some lines
Objdump看一下链接生成的目标文件,可以看到各个段即lable的地址都是 400000
为基地址,与链接脚本中的定义相符。具体以 addr_myvar_a 为例,其运行时的地址应该为 0x4000c0 ,接着我们通过gdb在运行时验证一下。另外,也注意 ldr + Lable
方式加载的内存源地址在链接时也确定了,毕竟就是label addr_myvar_a代表的地址嘛。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 $ objdump -s -D global_var global_var: file format elf64-littleaarch64 Contents of section .text: 4000 b0 81000058 200040 f9 00040091 c0035fd6 ...X .@......._. 4000 c0 d0004100 00000000 d8004100 00000000 ..A.......A..... Contents of section .data: 4100 d0 03000000 00000000 07000000 00000000 ................ Disassembly of section .text: 00000000004000 b0 <_start>: 4000 b0: 58000081 ldr x1, 4000 c0 <addr_myvar_a> 4000 b4: f9400020 ldr x0, [x1] 4000 b8: 91000400 add x0, x0, 4000 bc: d65f03c0 ret 00000000004000 c0 <addr_myvar_a>: 4000 c0: 004100 d0 .inst 0 x004100d0 ; undefined 4000 c4: 00000000 .inst 0 x00000000 ; undefined 00000000004000 c8 <addr_myvar_b>: 4000 c8: 004100 d8 .inst 0 x004100d8 ; undefined 4000 cc: 00000000 .inst 0 x00000000 ; undefined Disassembly of section .data: 00000000004100 d0 <myvar_a>: 4100 d0: 00000003 .inst 0 x00000003 ; undefined 4100 d4: 00000000 .inst 0 x00000000 ; undefined 00000000004100 d8 <myvar_b>: 4100 d8: 00000007 .inst 0 x00000007 ; undefined 4100 dc: 00000000 .inst 0 x00000000 ; undefined
gdb验证一下运行时 addr_myvar_a 的虚拟内存地址:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ gdb -q ./global_var gef➤ br _start Breakpoint 1 at 0 x4000b0 gef➤ run Starting program: /home/pi/workspace/arm64/global_var Breakpoint 1 , 0 x00000000004000b0 in _start () ... ─────────────────────────────────────────────────────────────────────────────────── code:arm64:ARM ──── 0 x4000a4 .inst 0 x00000000 ; undefined 0 x4000a8 .inst 0 x00010000 ; undefined 0 x4000ac .inst 0 x00000000 ; undefined → 0 x4000b0 <_start+0 > ldr x1, 0 x4000c0 <addr_myvar_a> 0 x4000b4 <_start+4 > ldr x0, [x1] 0 x4000b8 <_start+8 > add x0, x0, 0 x4000bc <_start+12 > ret 0 x4000c0 <addr_myvar_a+0 > .inst 0 x004100d0 ; undefined 0 x4000c4 <addr_myvar_a+4 > .inst 0 x00000000 ; undefined ───────────────────────────────────────────────────────────────────────────────────
gdb运行时的debug显示addr_myvar_a的运行时地址为0x4000c0 ,与上述推断一致。
1 0 x4000c0 <addr_myvar_a+0 > .inst 0 x004100d0 ; undefined
3.ARM64中的写法 前面提到,上述无法从text段中直接访问data段中label的限制只针对旧的ARM32位汇编存在,而AArch64/ARM64则不存在,因此ARM64汇编可以就可以采用更简单的写法了。如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 myvar_a: .dword 3 myvar_b: .dword 7 .text .global _start _start: ldr x0, myvar_a ldr x1, myvar_b mov x1, x0 //不需要手动在literal pool中添加label地址了
有一些限制:
要加载的数据的地址(也就是label代表的地址)与当前指令地址的偏移量必须在 1M 以内
4.关于PC-relative地址 TODO