0. 概览
我们知道在 MacOS 的 as 汇编器中有一条 ldr 伪指令,使用它我们可以非常方便的将立即数加载到寄存器中。
不过,当 ldr 的源操作数是一个标签(Label)时,就会导致在运行时发生崩溃:
如上图所示,第 21 行指令会导致运行时崩溃。
这是什么原因?又该如何解决呢?
闲言少叙,Let‘s go!!!😉
1. ldr 伪指令探究
在 MacOS 的 as 汇编器(以下简称 as )中, ldr 指令根据不同使用情境有着不同的功能。
当 ldr 源操作数不以 = 号开头时,它被转换成一条货真价实的 ARM64 机器指令:
ldr x0, [x0]
比如,上面的 ldr 指令表示将 x0 寄存器中的值加载到 x0 寄存器中。
当 ldr 源操作数以 = 号开头时,as 会将它变成一条伪指令。它的意思是将 = 号后面的值加载到目的寄存器中:
ldr x1, =0x12345678aabbccdd
比如,上面的 ldr 伪指令表示将 0x12345678aabbccdd 放入 x1 寄存器。
为什么要有 ldr =伪指令呢?因为在 ARM64 汇编中将立即数放入寄存器是一件“力气活”。
正常情况下,我们可以使用 mov 指令做这件事:
mov x0, #0xfff9 // 未超过 16 位
mov x0, #0xff000000 // 超过 16 位,但可以用左移来表示
不过,当立即数长度超过 16 位且不能用左移(LSL)来表示时,用 mov 指令会出错:
mov x0, #0x12345
以上指令在编译时会妥妥的报错:
arm64.s:22:13: error: expected compatible register or logical immediate
mov x0, #0x12345
^
这是因为:ARM64 机器指令编码本身是定长的 32 位,里面无法再放入一个超长的立即数了(intel x64 汇编就无此问题,因为其指令编码是变长的)。
所以,ldr = 伪指令就应运而生了:编译器会帮助我们处理好赋值超长立即数这件事!
还拿上面的例子来说:
ldr x1,=0x12345678aabbccdd
它从功能上实际等于下面这条 mov 指令:
mov x1, 0x12345678aabbccdd
按常理 0x12345678aabbccdd 对于 mov 指令来说绝对是超长了,不过编译器在这里使用了一个小伎俩:它不是直接把 0x12345678aabbccdd 放到 x1 中,而是在 TEXT 段放入 0x12345678aabbccdd 值,然后将其地址当做 ldr 指令的源操作数:
这就是 ldr = 伪指令可以间接助攻 mov 指令的根本原因。
需要注意的是,如果 ldr = 伪指令的源操作数可以编码为 mov 指令许可的值,那么 as 汇编器会直接使用 mov 而不是 ldr 指令:
ldr x2,=0x1234
比如,上面的 0x1234 满足 mov 指令的长度限制,所以 as 会生成如下机器指令:
_main:
0000000100003f9c mov x2, #0x1234
2. 错误原因
了解了ldr 伪指令,现在我们来看看它的另一种形式:=Label。
.text
.globl _main
.p2align 2
_main:
func_constructor
ldr x2,=my_data
func_destructor
ret
.p2align 3
my_data: .quad 0x12345678aabbccdd
上面的 ldr 伪指令实际做了神马?
它其实是将 my_data 的地址放入 x2 寄存器:
如上图所示:ldr 指令将 0x100003FB0 地址中的值放入 x2 寄存器,而 0x100003FB0 地址存放的就是 0x12345678aabbccdd 的地址!
以上代码编译链接均无问题,不过在运行时会立即崩溃。
% ./arm64
zsh: bus error ./arm64
看来是一种 bus error,我们使用 lldb 调试器看一下详细情况:
% lldb arm64
(lldb) target create "arm64"
Current executable set to '/Users/hopy/src/asm/arm64' (arm64).
(lldb) r
Process 3194 launched: '/Users/hopy/src/asm/arm64' (arm64)
Process 3194 stopped
* thread #1, stop reason = EXC_BAD_ACCESS (code=2, address=0x100003fb0)
frame #0: 0x0000000191597ed8 dyld`dyld4::fixupPage64(void*, mwl_info_hdr const*, dyld_chained_starts_in_segment const*, unsigned int, bool) + 132
dyld`dyld4::fixupPage64:
-> 0x191597ed8 <+132>: str x12, [x9]
0x191597edc <+136>: add x9, x9, x11, lsl #2
0x191597ee0 <+140>: cbnz x11, 0x191597e98 ; <+68>
0x191597ee4 <+144>: b 0x191597f04 ; <+176>
Target 0: (arm64) stopped.
可以看到,实际崩溃发生在一个名为 fixupPage64() 的“陌生”函数里。
这个函数是做什么的呢?它实际是在程序运行前来修正程序中绝对链接地址的。
再回过头看一下前面一张图:
ldr 指令中 0x100003FB0 指向的内容在链接时为 0x3FA8,这不是一个运行时有效的地址,所以 Mach-O 可执行文件加载器会在运行时根据实际基地址将其修正为有效的内存地址。
我们再来看看 fixupPage64() 函数中发生崩溃的指令 str x12, [x9]
,用 lldb 命令查看一下此时 x12 和 x9 寄存器中的内容到底是啥:
Process 3227 launched: '/Users/hopy/src/asm/arm64' (arm64)
Process 3227 stopped
* thread #1, stop reason = EXC_BAD_ACCESS (code=2, address=0x100003fb0)
frame #0: 0x0000000191597ed8 dyld`dyld4::fixupPage64(void*, mwl_info_hdr const*, dyld_chained_starts_in_segment const*, unsigned int, bool) + 132
dyld`dyld4::fixupPage64:
-> 0x191597ed8 <+132>: str x12, [x9]
0x191597edc <+136>: add x9, x9, x11, lsl #2
0x191597ee0 <+140>: cbnz x11, 0x191597e98 ; <+68>
0x191597ee4 <+144>: b 0x191597f04 ; <+176>
Target 0: (arm64) stopped.
(lldb) register read x12 x9
x12 = 0x0000000100003fa8 arm64`my_data
x9 = 0x0000000100003fb0 arm64`my_data + 8
(lldb) memory read $x12
0x100003fa8: dd cc bb aa 78 56 34 12 a8 3f 00 00 00 00 00 00 ....xV4..?......
0x100003fb8: 01 00 00 00 1c 00 00 00 00 00 00 00 1c 00 00 00 ................
(lldb) memory read $x12+8
0x100003fb0: a8 3f 00 00 00 00 00 00 01 00 00 00 1c 00 00 00 .?..............
0x100003fc0: 00 00 00 00 1c 00 00 00 00 00 00 00 1c 00 00 00 ................
看到了吗?fixupPage64() 函数实际上想要把 0x3fa8 修正为 100003fa8。
但这又为什么会出错呢?
原来,0x3fa8 是放在代码段(TEXT)中的,而代码段本身只有可读 + 可执行权限,是没有可写权限的,这就是程序崩溃的根本原因!
3. 解决之道
由此看来,as 中 ldr 的本意是让我们操作立即数,而不是标签(Label)。
解决方法有几种,首先我们可以为 TEXT 段添加可写属性,不过这样费事又不安全。
其实,我们的本意是获得 my_data 的地址,这可以通过 adr 指令来完成:
_main:
func_constructor
//ldr x2,=my_data
adr x2, my_data
func_destructor
ret
.p2align 3
my_data: .quad 0x12345678aabbccdd
生成的机器指令如下所示:
综上所述,为了获得标签(my_data)的地址,我们完全可以跳过 ldr = 伪指令,而直接使用 adr 指令;只在需要立即数(尤其是超长立即数)赋值时借助 ldr = 的“神秘力量”。
4. 总结
在本篇博文中,我们详细讨论了 MacOS as 汇编器(ARM64)中 ldr = 伪指令的功能,并介绍了为什么不能用它来获得标签(Label)的地址,并提供解决方法。
感谢观赏,再会!😎