0%

深入理解ARM汇编LDR命令使用Label加载数据

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:
4000b0 81000058 200040f9 00040091 c0035fd6 ...X .@......._.
4000c0 d0004100 00000000 d8004100 00000000 ..A.......A.....

Contents of section .data:
4100d0 03000000 00000000 07000000 00000000 ................
//地址4100d0处内容为3 地址4100d8处内容为7
//myvar_a代表的地址是4100d0
//myvar_b代表的地址是4100d8

Disassembly of section .text:

00000000004000b0 <_start>:
4000b0: 58000081 ldr x1, 4000c0 <addr_myvar_a>
4000b4: f9400020 ldr x0, [x1]
4000b8: 91000400 add x0, x0, #0x1
4000bc: d65f03c0 ret

//addr_myvar_a处的内容为004100d0,等于myvar_a代表的地址
00000000004000c0 <addr_myvar_a>:
4000c0: 004100d0 .word 0x004100d0
4000c4: 00000000 .word 0x00000000

//addr_myvar_b处的内容为004100d8,等于myvar_b代表的地址
00000000004000c8 <addr_myvar_b>:
4000c8: 004100d8 .word 0x004100d8
4000cc: 00000000 .word 0x00000000

好了,关于label含义搞明白了。

这里有个疑惑了,既然我们已经有了label myvar_a代表了全局变量3的地址,为什么LDR不直接使用myvar_a,而还需要在代码段中使用addr_myvar_a保存该地址呢

简单的答案是:

【注意:这个限制只针对ARM32汇编存在,实测使用AArch64/ARM64汇编可以从text段中访问data段中的label!!】

myvar_aaddr_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:
4000b0 81000058 200040f9 00040091 c0035fd6 ...X .@......._.
4000c0 d0004100 00000000 d8004100 00000000 ..A.......A.....
Contents of section .data:
4100d0 03000000 00000000 07000000 00000000 ................

Disassembly of section .text:

00000000004000b0 <_start>:
4000b0: 58000081 ldr x1, 4000c0 <addr_myvar_a> ##加载的内存源地址也确定了
4000b4: f9400020 ldr x0, [x1]
4000b8: 91000400 add x0, x0, #0x1
4000bc: d65f03c0 ret

00000000004000c0 <addr_myvar_a>: ##验证一下addr_myvar_a运行时地址是不是4000c0
4000c0: 004100d0 .inst 0x004100d0 ; undefined
4000c4: 00000000 .inst 0x00000000 ; undefined

00000000004000c8 <addr_myvar_b>:
4000c8: 004100d8 .inst 0x004100d8 ; undefined
4000cc: 00000000 .inst 0x00000000 ; undefined

Disassembly of section .data:
00000000004100d0 <myvar_a>:
4100d0: 00000003 .inst 0x00000003 ; undefined
4100d4: 00000000 .inst 0x00000000 ; undefined

00000000004100d8 <myvar_b>:
4100d8: 00000007 .inst 0x00000007 ; undefined
4100dc: 00000000 .inst 0x00000000 ; 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 0x4000b0
gef➤ run
Starting program: /home/pi/workspace/arm64/global_var

Breakpoint 1, 0x00000000004000b0 in _start ()
...
## skip some lines
─────────────────────────────────────────────────────────────────────────────────── code:arm64:ARM ────
0x4000a4 .inst 0x00000000 ; undefined
0x4000a8 .inst 0x00010000 ; undefined
0x4000ac .inst 0x00000000 ; undefined
0x4000b0 <_start+0> ldr x1, 0x4000c0 <addr_myvar_a>
0x4000b4 <_start+4> ldr x0, [x1]
0x4000b8 <_start+8> add x0, x0, #0x1
0x4000bc <_start+12> ret
## 可以看到addr_myvar_a运行时地址为0x4000c0,与objdump结果一致
0x4000c0 <addr_myvar_a+0> .inst 0x004100d0 ; undefined
0x4000c4 <addr_myvar_a+4> .inst 0x00000000 ; undefined
───────────────────────────────────────────────────────────────────────────────────

gdb运行时的debug显示addr_myvar_a的运行时地址为0x4000c0,与上述推断一致。

1
0x4000c0 <addr_myvar_a+0> .inst  0x004100d0 ; 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