ARM汇编中ADRP和ADRL指令的语义是什么?

23

ADRP

ADRP指令可以得到PC地址加上一个偏移量所在的4KB页的地址。

ADRL

ADRL指令将PC相对地址加载进寄存器中。它类似于ADR指令。与ADR不同的是,ADRL可以加载更广泛的地址范围,因为它生成两个数据处理指令。

具体来说,

ADRL汇编成两个指令,一个ADRP加一个ADD。如果汇编器无法用两条指令构建地址,就会生成一个重定位(Relocation)。链接器随后会生成正确的偏移量。ADRL产生的代码是位置无关的,因为其地址相对于PC计算。

ADRPADRL指令是用于获取特定地址的指令,ADRP使用PC地址和一个偏移量得到所在的4KB页的地址,而ADRL可以将PC相对地址加载进寄存器中。具体而言,ADRL指令汇编成两条指令,一个ADRP加一个ADD,用于构建PC相对地址。


1
ADRP计算页面地址。页面大小为4KB,并且按4KB对齐。这意味着页面地址的低12位始终为0。ADD指令提供了低12位,因此您可以形成不位于页面开头的内容的地址。 - Ross Ridge
adr_l和adrl是相同的指令吗? - Chan Kim
1个回答

46

ADR

ADR是一种简单的PC相对地址计算方法:您提供一个立即偏移量,它将存储相对于当前PC的地址到寄存器中。

例如,如果以下ADR指令放置在内存位置0x4000:

adr x0, #1

执行完这条指令后,x0 中现在包含值 0x4001。在 GitHub 上可运行的断言

我们也可以尝试执行以下操作:

mov x0, #0x4001

但是PC相对寻址有以下优点:
  • 所有ARMv7 / ARMv8指令都是4字节长。这与x86指令宽度可变形成了鲜明对比。

    这简化了很多事情,但有一个不幸的影响:您无法在单个指令中编码完整地址(4/8字节),因为我们需要一些位来编码指令本身。

    尽管我们无法存储完整地址,但我们可以通过相对于PC的相对地址引用其中一些地址(适合编码的那些地址),这通常对于许多应用程序已经足够,因为我们通常只跳转到附近的代码位置。

    这里的原理类似于存在ldr =伪指令的原理:在ARM汇编中为什么使用LDR而不是MOV(反之亦然)?

  • 它允许生成位置无关代码,这是避免共享库在内存中冲突的基础,但也对主文本段非常有用,以启用ASLR,另请参见:gcc和ld中面向位置的可执行文件的-fPIE选项是什么?

  • 生成的代码更小

ADR指令使用21位立即数作为偏移量,允许进行+-1MiB的跳转(20位+1位符号)。
在ARmv7/aarch32中,ADR有时可以通过文档ARMv7 DDI 0406C.d manual D9.4 "ARM指令中显式使用PC" 中记录的ADD和SUB来实现:
一些形式的ADR指令可以被表示为以PC为Rn的ADD或SUB形式。这些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在那里实际上很重要,并且具有单独的编码:如何在arm汇编中编写PC相对寻址? ADRP类似于ADR,但它:
  • 相对于当前页面移动页面(4KiB,ADRP中的P代表Page)而不是字节
  • 将低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: 只提取 myvariable 的低 12 位到一个立即数中,链接器生成的最终指令只是一个 add x0, x0, #<immediate>,另请参见:AArch64 relocation prefixesWhat do linkers do?

ADRP 相对于 ADR 的优势在于我们可以跳得更远(+-4GiB),但需要在 ADRP 后再执行一次 ADD 来设置低 12 位。ARMv8 手册上写道:

ADR指令将带符号的21位立即数加到获取该指令的程序计数器的值上,然后将结果写入通用寄存器。这允许计算当前PC±1MB范围内的任何字节地址。
ADRP指令将带符号的21位立即数左移12位,将其添加到程序计数器的值中,并清除底部12位,然后将结果写入通用寄存器。这允许计算4KB对齐的内存区域的地址。与ADD(立即数)指令或带有12位立即偏移量的Load/Store指令结合使用,可以计算或访问当前PC±4GB范围内的任何地址。
ADRP的另一个限制是,与ADR不同,如果将代码加载到相对于原始链接器偏移量的4K倍数之外的位置(例如由于ASLR),它将会出错。例如,如果稍微向上移动,目标地址可能会落在下一页,而PC位置仍停留在旧页面上,使ADRP指向错误的页面。但是,依赖ADRP的可执行文件仍被认为是PIE,而像动态链接器/ASLR这样的系统只能以4K的倍数在内存中重定位,相关:在Linux中如何确定PIE可执行文件的文本段地址? ADRP仅存在于ARMv8中,不存在于ARMv7中。

ARMv8 DDI 0487C.a manual指出Page只是4KB的助记符,不反映实际页面大小,其可配置为其他大小。C3.3.5“PC-relative address calculation”:

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>

This instruction will load the address of label into the indicated register. The instruction will evaluate to one or two PC relative ADD or SUB instructions depending upon where the label is located. If a second instruction is not needed a NOP instruction will be generated in its place, so that this instruction is always 8 bytes long.

因此,它似乎能够扩展到多个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的两倍吗?

尝试在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,请参见:What is the difference between =label (equals sign) and [label] (brackets) in ARMv6 assembly?


3
例如,如果将以下ADRP指令放置在内存的位置0x4050处:\ adrp x0,#1\ 那么在执行此指令后,x0现在包含值0x8000(+ 1 * 4k并清除前12位)。" -- 后面一位是“1”,接下来的12位都是“0”,所以我认为这个例子应该得到的结果是5000h,而不是8000h。 - ecm
换句话说,假设我们的PC是0x20e0,我们看到:adrp x0,#0x9000add x0,x0,#0x70。我们可以立即看到加载的地址在0xb070处。另一种选择可能是类似于adr x0,#0x8f90,这并没有告诉您太多信息。 - Asherah
当添加0x10000x4050时,得到0x5050。但是,如果将这条指令“ADRP X1, 0x101000”放置在内存位置“00100e48”,则需要采取哪些步骤才能获得0x5000?那么x1包含什么呢?谢谢。 - hanan
@hanan 如果指令位于 00100e48,而您想让 x1 等于 0x5050,我认为您只需要忽略低 12 位,即从 0x00100000 到 0x5000,因此我会使用负偏移量 ADRP x1,(0x5000 - 0x00100000) - Ciro Santilli
1
@hanan 好的,那么就是取出 00100e48,去掉 12 位,得到 00100000,然后加上 0x101000 得到 0x201000 - Ciro Santilli
显示剩余11条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接