了解 ARM 重定位(示例:str x0, [tmp, #:lo12:zbi_paddr])

Understanding ARM relocation (example: str x0, [tmp, #:lo12:zbi_paddr])

我在 zircon kernel start.S

中找到了这行汇编
str     x0, [tmp, #:lo12:zbi_paddr]

用于 ARM64。我还发现 zbi_paddr 在 C++ 中定义:

extern paddr_t zbi_paddr;

所以我开始研究 #:lo12: 是什么意思。

我发现 这看起来是一个很好的解释,但它没有解释最基本的东西:什么是重定位以及为什么需要一些东西。

我猜想因为 zbi_paddrr 是在 start.S 中定义的并且在 C++ 代码中使用,因为 start.S 在目标文件 start.o 上生成地址从 0 开始,链接过程必须将那里的所有地址重新分配给最终可执行文件中的地址。

为了跟踪需要重定位的符号,ELF 存储这些结构,如答案中所述:

typedef struct
{
    Elf64_Addr r_offset;    /* Address of reference */
    Elf64_Xword r_info;     /* Symbol index and type of relocation */
} Elf64_Rel;

typedef struct
{
    Elf64_Addr r_offset;    /* Address of reference */
    Elf64_Xword r_info;     /* Symbol index and type of relocation */
    Elf64_Sxword r_addend;  /* Constant part of expression */
} Elf64_Rela;

因此,例如,r_offset 会将 zbi_paddr 的地址存储在最终的可执行文件中。然后,当加载程序时,加载器查看这些结构,然后从 C++ 代码中填充 zbi_paddr 的地址。

在那之后,我完全没有需要像 SAPXabs_g0_s 和 [=28= 这样的东西].他说这与无法将 64 位插入寄存器的指令有关。有人可以给我更多背景信息吗?我无法理解,已经有方法可以将 64 位插入寄存器。这与重新分配有何关系?

潜在的问题是 ARM64 指令的大小都是 32 位,这限制了可以在任何一条指令中编码的立即数据的位数。您当然不能编码 64 位地址,甚至 32 位。

内核的代码和静态数据预计在4GB以下,所以为了在静态变量zbi_paddr中存储数据,程序员可以编写如下两条指令(包括前一个你省略但很重要的)。注意tmp是上面定义为x9的宏,所以代码展开为:

adrp    x9, zbi_paddr
str     x0, [x9, #:lo12:zbi_paddr]

现在当链接发生时,链接器将知道整个内核的布局,以及所有符号的相对位置。该方案支持位置无关代码,所以不需要知道绝对地址,但是我们肯定会知道上面zbi_paddradrp指令之间的位移,它将适合带符号的 32 位值,以及 zbi_paddr 在其 4KB 页面内的偏移量(因为内核必须加载到页面对齐的地址)。

因此,此位移的第 12 位和更高位将被编码到 adrp 指令中,该指令具有 21 位立即数字段。 adrp对其进行符号扩展,将其与程序计数器的相应位相加,并将结果放入x9。然后 x9 将包含 zbi_paddr 的绝对地址的第 63-12 位,低 12 位被清零。

zbi_paddr在其页内的12位偏移量将被编码到str指令的12位立即数字段中。它将这个立即数添加到 x9 中的值,然后它将产生 zbi_paddr 的地址,并将 x0 存储在该地址。所以我们只用两条指令就成功地将一个值存储在 zbi_paddr 中。

为了支持这一点,通过汇编我们的代码生成的目标文件需要指示链接器需要将位移的第 32-12 位插入到 adrp 指令中,而位移的第 11-0 位zbi_paddr 的地址需要插入到 str 指令中。这些给链接器的指令就是重定位;它们将包含对其地址要被编码的符号的引用(此处 zbi_paddr)以及具体要用它做什么。 ELF 支持专门为这些指令设计的重定位,将正确的位放在指令字的正确位置。

确实有其他方法可以将 64 位值存入寄存器。例如,它可以放在文字池中,这是一个足够接近相应代码的数据区域,可以通过单个 ldr 指令(使用 PC 相对位移)到达它。您可以进行重定位,告诉链接器在文字池中插入 zbi_paddr 的绝对地址。但是加载它需要额外的内存访问,这比 adrp 慢;此外,8 个字节的字面值,加上 ldr,再加上实际进行存储的 str,总共需要 16 个字节的内存。 adrp/str 方法只需要 8 个,它在与位置无关的代码中工作得更好,在这种情况下链接器可能实际上不知道 zbi_paddr.

的绝对地址

如果您不喜欢从内存中加载,您可以使用最多四个 mov/movk 指令将 zbi_paddr 的绝对地址放入一个寄存器中,一次加载 16 位。也有搬迁。但是对于最后的 str,我们最多使用了 20 个字节的代码;执行 5 条指令比执行 2 条指令需要更多的时钟周期;而且与位置无关的代码仍然存在问题。

因此,adrp/str:lo12: 是访问全局或静态变量的标准接受方法。如果你想加载而不是存储,你可以使用 adrp/ldr。如果你想在寄存器中找到 zbi_paddr 的地址,你可以

adrp x9, zbi_paddr
add x9, x9, #:lo12:zbi_paddr

add指令也支持12位立即数,正是为了这个目的。

这些功能在 GNU assembler manual 中有解释。