ADR
ADR 是一个简单的 PC 相对地址计算:您给它一个立即偏移量,它在寄存器中存储相对于当前 PC 的地址。
例如,如果以下 ADR 指令放置在内存中的位置 0x4000 处:
adr x0, #1
然后执行该指令后x0
现在包含值 0x4001。在 GitHub 上,带有可运行断言 https://github.com/cirosantilli/linux-kernel-module-cheat/blob/059a7ef9d9c378a6d1d327ae97d90b78183680b2/userland/arch/aarch64/adr.S#L19.
我们可以尝试这样做:
mov x0, #0x4001
但相对PC寻址有以下优点:
-
所有 ARMv7 / ARMv8 指令都是 4 字节长。这与指令宽度可变的 x86 形成鲜明对比。
这简化了很多事情,但它有一个不幸的含义:您无法在一条指令中编码完整地址(4 / 8 字节),因为我们需要一些位来对指令本身进行编码。
尽管我们无法存储完整的地址,但我们可以通过 PC 的相对地址来引用其中的一些地址(适合编码的地址),这对于许多应用程序来说通常就足够了,因为我们通常只跳转到附近的代码位置。
这里的基本原理类似于ldr =
伪指令:为什么在 ARM 汇编中使用 LDR 而不是 MOV(反之亦然)? https://stackoverflow.com/questions/14046686/why-use-ldr-over-mov-or-vice-versa-in-arm-assembly
-
它允许位置独立的代码,这对于避免共享库在内存中发生冲突至关重要,而且对于主文本段也很有用,以启用ASLR https://en.wikipedia.org/wiki/Address_space_layout_randomization, 也可以看看:gcc 和 ld 中与位置无关的可执行文件的 -fPIE 选项是什么? https://stackoverflow.com/questions/2463150/what-is-the-fpie-option-for-position-independent-executables-in-gcc-and-ld/51308031#51308031
-
生成的代码更小
ADR 指令使用 21 位立即数作为偏移量,允许 +-1MiB 跳转(20 位 + 1 符号)。
在 ARmv7/aarch32 中,ADR 有时可以通过 PC 上的 ADD 和 SUB 来实现,如ARMv7 DDI 0406C.d 手册 https://static.docs.arm.com/ddi0406/cd/DDI0406C_d_armv7ar_arm.pdfD9.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 https://sourceware.org/binutils/docs-2.31/as/ARM-Opcodes.html#ARM-Opcodes
该指令将标签的地址加载到指定的寄存器中。该指令将根据标签所在的位置评估为 PC 相关的 ADD 或 SUB 指令。如果标签超出范围,或者没有在与 ADR 指令相同的文件(和节)中定义,则会生成错误。该指令不会使用文字池。
然而,在 ARMv8 aarch64 中,PC 不能像通用寄存器一样在每条指令中使用,因此 ADR 实际上在那里很重要,并且具有单独的编码:如何在arm asm上编写PC相对寻址? https://stackoverflow.com/questions/28638981/howto-write-pc-relative-adressing-on-arm-asm/54480999#54480999
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 上,带有可运行断言 https://github.com/cirosantilli/arm-assembly-cheat/blob/https://github.com/cirosantilli/linux-kernel-module-cheat/blob/059a7ef9d9c378a6d1d327ae97d90b78183680b2/userland/arch/aarch64/adrp.S#L7/v8/adrp.S.
请注意,:lo12:
只提取低12位myvariable
立即而言,链接器生成的最终指令只是一条add x0, x0, #<immediate>
, 也可以看看:AArch64 重定位前缀 https://stackoverflow.com/questions/38570495/aarch64-relocation-prefixes/38608738#38608738 and 链接器有什么作用? https://stackoverflow.com/questions/3322911/what-do-linkers-do/33690144#33690144.
ADRP 相对于 ADR 的优点是我们可以跳得更远(+-4GiB),但代价是需要在 ADRP 之后执行额外的 ADD 来设置较低的 12 位。 ARMv8手册说:
ADR 指令将带符号的 21 位立即数与获取该指令的程序计数器的值相加,然后将结果写入通用寄存器。这允许计算当前 PC ±1MB 范围内的任何字节地址。
ADRP 指令将带符号的 21 位立即数左移 12 位,将其与程序计数器的值相加,并将底部 12 位清零,然后将结果写入通用寄存器。这允许计算 4KB 对齐内存区域的地址。与 ADD(立即数)指令或具有 12 位立即数偏移量的加载/存储指令结合使用,可以计算或访问当前 PC ±4GB 内的任何地址。
ADRP 的另一个限制是,与 ADR 不同,如果您将代码加载到内存中相对于原始链接器偏移量未偏移 4K 倍数的位置(例如由于 ASLR),它就会中断。例如,如果向上移动一点,目标地址可能会落在下一页上,而 PC 位置仍保留在旧页面上,从而使 ADRP 指向错误的页面。然而,依赖ADRP的可执行文件仍然被认为是PIE,并且诸如动态链接器/ASLR之类的系统只能在内存中重定位4K的倍数,相关:Linux 中 PIE 可执行文件的文本部分的地址是如何确定的? https://stackoverflow.com/questions/51343596/how-is-the-address-of-the-text-section-of-a-pie-executable-determined-in-linux
ADRP 仅存在于 ARMv8 中,不存在于 ARMv7 中。
The ARMv8 DDI 0487C.a 手册 https://static.docs.arm.com/ddi0487/ca/DDI0487C_a_armv8_arm.pdf表示Page只是4KB的助记符,并不反映实际的页面大小,可以配置为其他大小。 C3.3.5“PC相对地址计算”:
ADRP 描述中使用的术语“页”是 4KB 内存区域的简写,与虚拟内存区域无关。
记忆翻译颗粒大小。
ADRL
ADRL 不是一条实际指令,只是一条“伪指令”,即发出真实指令的汇编程序快捷方式。
因此,v7 手册中没有提到这一点,v8 手册中只在“读取 PC 的说明”中提到过一次,但我在手册中找不到解释它的任何地方,所以也许它只是文档错误?
因此,我将重点关注 GNU AS 的实现,其记录在https://sourceware.org/binutils/docs-2.31/as/ARM-Opcodes.html#ARM-Opcodes https://sourceware.org/binutils/docs-2.31/as/ARM-Opcodes.html#ARM-OpcodesARM 特定功能:
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
at https://patchwork.kernel.org/patch/9883301/ https://patchwork.kernel.org/patch/9883301/TODO 理解原理。
备择方案
当 PC 偏移量太长而无法编码到指令中时,一种主要的替代方法是使用 movk / movw / movt,请参阅:ARMv6 汇编中=label(等号)和[label](括号)有什么区别? https://stackoverflow.com/questions/17214962/what-is-the-difference-between-label-equals-sign-and-label-brackets-in-ar/54043398#54043398