ADR
ADR 是一个简单的 PC 相对地址计算:你给它一个直接的偏移量,它在寄存器中存储相对于当前 PC 的地址。
例如,如果将以下 ADR 指令放置在内存中的位置 0x4000:
adr x0, #1
那么在这条指令执行后x0
现在包含值 0x4001。在带有可运行断言的 GitHub 上。
我们可以尝试这样做:
mov x0, #0x4001
但是PC相对寻址有以下优点:
ADR 指令使用 21 位立即数作为偏移量,允许 +-1MiB 跳转(符号为 20 位 + 1)。
在 ARMv7/aarch32 中,ADR 有时可以通过 ADD 和 SUB 与 PC 实现,如ARMv7 DDI 0406C.d 手册D9.4“在 ARM 指令中显式使用 PC”中所述:
ADR 指令的某些形式可以表示为 ADD 或 SUB 的形式,PC 为 Rn。这些形式的 ADD 和 SUB 是允许的,并且不被弃用。
TODO 什么时候不能实现ADD
?GNU GAS 建议 ADR 只是一个伪操作,总是组装成 ADD 或 SUB:https ://sourceware.org/binutils/docs-2.31/as/ARM-Opcodes.html#ARM-Opcodes
该指令将标签的地址加载到指定的寄存器中。根据标签所在的位置,该指令将评估为 PC 相关的 ADD 或 SUB 指令。如果标签超出范围,或者它没有在与 ADR 指令相同的文件(和节)中定义,则会产生错误。该指令不会使用文字池。
然而,在 ARMv8 aarch64 中,PC 不能像通用寄存器那样在每条指令中使用,因此 ADR 在那里实际上很重要,并且有一个单独的编码:Howto write PC relative addressing on arm asm?
ADRP
ADRP 类似于 ADR,但它:
- 相对于当前页面而不是字节移动页面(4KiB,ADRP 中的 P 代表页面)
- 清零低 12 位
例如,如果将以下 ADRP 指令放置在内存中的位置 0x4050:
adrp x0, #0x1000
然后在执行此指令后,x0
现在包含值 0x5000(+ 0x1000 并将前 12 位清零)。
但是请注意,上述语法仅具有教育意义,因为 GNU GAS 似乎不接受文字整数常量作为参数,只接受符号。(或者它将 0x1000 视为符号并且链接失败,沿着这些思路,现在没有时间完全理解它 TODO)。
由于低 12 位被清零,为了计算完整地址,ADRP 通常与 ADD +:lo12:
重定位一起使用,如下所示:
adrp x0, myvariable
add x0, x0, :lo12:myvariable
在带有可运行断言的 GitHub 上。
请注意,:lo12:
只是将 的低 12 位提取myvariable
到立即数,链接器生成的最终指令只是一个add x0, x0, #<immediate>
,另请参见:AArch64 重定位前缀和链接器做什么?.
ADRP 优于 ADR 的优势在于我们可以跳得更远(+-4GiB),代价是需要在 ADRP 之后进行额外的 ADD 以设置低 12 位。ARMv8 手册说:
ADR 指令将带符号的 21 位立即数加到获取该指令的程序计数器的值上,然后将结果写入通用寄存器。这允许计算当前 PC 的 ±1MB 范围内的任何字节地址。
ADRP 指令将带符号的 21 位立即数左移 12 位,将其与程序计数器的值相加,低 12 位清零,然后将结果写入通用寄存器。这允许在 4KB 对齐的内存区域计算地址。结合 ADD(立即数)指令或具有 12 位立即数偏移量的加载/存储指令,这允许计算或访问当前 PC ±4GB 范围内的任何地址。
ADRP 的另一个限制是,与 ADR 不同的是,如果您将代码加载到内存中相对于原始链接器偏移量(例如,由于 ASLR)未偏移 4K 倍数的位置,它会中断。例如,如果您稍微上移,目标地址可能会落在下一页,而 PC 位置会停留在旧地址上,从而使 ADRP 指向错误的页面。但是,依赖ADRP的可执行文件仍然被认为是PIE,而动态链接器/ASLR等系统在内存中只能重定位4K的倍数,相关:Linux中PIE可执行文件的文本部分的地址是如何确定的?
ADRP 仅存在于 ARMv8 中,不存在于 ARMv7 中。
ARMv8 DDI 0487C.a手册说Page只是4KB的助记词,并不反映实际的页面大小,可以配置成其他大小。C3.3.5“PC相对地址计算”:
ADRP 描述中使用的术语 page 是 4KB 内存区域的简写,与虚拟内存转换粒度大小无关。
ADRL
ADRL 不是实际指令,只是“伪指令”,即发出真实指令的汇编快捷方式。
因此,在 v7 手册中没有提到它,并且在 v8 手册中的“阅读 PC 的说明”中只提到了一次,但我在手册中找不到任何解释它的地方,所以也许它只是文档错误?
因此,我将专注于 GNU AS 实现,该实现在 ARM 特定功能下的https://sourceware.org/binutils/docs-2.31/as/ARM-Opcodes.html#ARM-Opcodes中记录了它:
adrl <register> <label>
该指令将标签的地址加载到指定的寄存器中。根据标签所在的位置,该指令将评估为一个或两个 PC 相关的 ADD 或 SUB 指令。如果不需要第二条指令,则会在其位置生成一条 NOP 指令,因此该指令始终为 8 字节长。
因此它似乎能够扩展到多个 ADD/SUB,大概是为了允许从 PC 进行更大的跳跃。
Objdump 确认了 GNU 手册对短地址的说明:
adr r0, label
10478: e28f0008 add r0, pc, #8
adrl r2, label
10480: e28f2000 add r2, pc, #0
10484: e1a00000 nop ; (mov r0, r0)
TODO:长地址的例子。最大长度是多少?只是 ADD/ADR 的 2 倍?
尝试在 aarch64 上使用它失败了,因为根据 GNU GAS 手册,它是 ARMv7 特定的功能。GNU GAS 2.29.1 上的错误消息是:
Error: unknown mnemonic `adrl' -- `adrl r6,.Llabel'
Linux 内核还定义了一个宏adr_l
,称为https://patchwork.kernel.org/patch/9883301/ TODO 了解基本原理。
备择方案
当 PC 偏移量太长而无法编码到指令中时,一种主要替代方法是使用 movk / movw / movt,请参阅:在 ARMv6 程序集中 =label(等号)和 [label](括号)有什么区别?