二进制

发布: (2025年12月29日 GMT+8 13:35)
9 min read

请提供您希望翻译的完整文本内容,我会按照要求保留源链接并进行简体中文翻译。

2 GiB “重定位屏障”——为何巨型二进制在 x86‑64 上会失效

在攻读博士并提交学术文章的过程中,我遇到的一个问题是,我已经为需要大规模才能有效且有价值的问题构建了解决方案。
对我的论文投稿的回复常常声称此类问题并不存在;然而,我在业界(例如在 Google)期间确实观察到了这些问题,只是无法在论文中引用它们!

唯一出现在这些超大代码库中的一个问题是巨型二进制文件
你见过最大的 ELF 二进制文件有多大?我曾见过超过 25 GiB的二进制文件(包括调试符号)。这怎么可能?

这些公司倾向于静态链接它们的服务,以加快启动速度并简化部署。把所有代码静态包含在世界上最大的一些代码库中,必然会产生巨型二进制。

类似于音障,代码大小达到一定程度后会变得成问题,我们必须重新思考链接和构建代码的方式。对于 x86‑64,这个临界点就是 2 GiB “重定位屏障”。

为什么是 2 GiB? 🤔

让我们看看位置无关代码是如何组合的。

一个简单示例

/* simple-relocation.c */
extern void far_function();

int main(void) {
    far_function();
    return 0;
}

编译它:

gcc -c simple-relocation.c -o simple-relocation.o

使用 objdump 检查目标文件:

> objdump -dr simple-relocation.o

0000000000000000 :
   0: 55                    push   %rbp
   1: 48 89 e5              mov    %rsp,%rbp
   4: b8 00 00 00 00        mov    $0x0,%eax
   9: e8 00 00 00 00        call   e 
        a: R_X86_64_PLT32   far_function-0x4
   e: b8 00 00 00 00        mov    $0x0,%eax
  13: 5d                    pop    %rbp
  14: c3                    ret
  • e8CALL 操作码(使用 32 位有符号相对偏移)。
  • 操作数目前是 00 00 00 00,因为实际地址尚未确定。
  • objdump 显示了链接器稍后必须修正的重定位条目。

注意
-0x4 是必需的,因为偏移是相对于 指令指针在读取完 4 字节操作数后 的位置计算的。

使用 readelf 显示重定位信息:

readelf -r simple-relocation.o -d
Relocation section '.rela.text' at offset 0x170 contains 1 entry:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000a  000400000004 R_X86_64_PLT32    0000000000000000 far_function - 4

该条目告诉链接器,位于偏移 0x0a(CALL 的立即数开始处)的 4 字节操作数必须被 far_function 的地址所填充。

添加被调用函数

/* far-function.c */
void far_function(void) {
}

编译并链接:

gcc -c far-function.c -o far-function.o
gcc simple-relocation.o far-function.o -o simple-relocation

检查最终可执行文件:

> objdump -dr simple-relocation

0000000000401106 :
 401106: 55                    push   %rbp
 401107: 48 89 e5              mov    %rsp,%rbp
 40110a: b8 00 00 00 00        mov    $0x0,%eax
 40110f: e8 07 00 00 00        call   40111b 
 401114: b8 00 00 00 00        mov    $0x0,%eax
 401119: 5d                    pop    %rbp
 40111a: c3                    ret

000000000040111b :
 40111b: 55                    push   %rbp
 40111c: 48 89 e5              mov    %rsp,%rbp
 40111f: 90                    nop
 401120: 5d                    pop    %rbp
 401121: c3                    ret

链接器已经计算出相对偏移 (0x07) 并修补了 CALL 指令。

2 GiB 屏障

CALL 操作码 (e8) 只接受 32 位有符号 位移,即它的可达范围是 ±2 GiB(‑2³¹ … +2³¹‑1)。
因此,一个调用点最多只能向前或向后跳约 2 GiB。这就是 “2 GiB 屏障”。

当目标位置更远时会怎样?

我们可以强制链接器把 far_function 放得离 main 很远,从而触发该限制……

Source: https://maskray.me/blog/2023-05-14-relocation-overflow-and-code-models

一个链接脚本。

/* overflow.lds */
SECTIONS
{
    /* 1. Standard low‑address sections */
    . = 0x400000;

    .text : {
        simple-relocation.o(.text.*)
    }
    .rodata : { *(.rodata .rodata.*) }
    .data   : { *(.data .data.*) }
    .bss    : { *(.bss .bss.*) }

    /* 2. Move the location counter far away for the “far” island */
    . = 0x120000000;   /* ≈ 4.5 GiB */

    .text.far : {
        far-function.o(.text*)
    }
}

现在使用 LLVM 的 lld 进行链接(它的错误信息更清晰):

gcc simple-relocation.o far-function.o -T overflow.lds -o simple-relocation-overflow -fuse-ld=lld

结果:

ld.lld: error: :(.eh_frame+0x6c):
relocation R_X86_64_PC32 out of range:
5364513724 is not in [-2147483648, 2147483647]; references section '.text'
ld.lld: error: simple-relocation.o:(function main: .text+0xa):
relocation R_X86_64_PLT32 out of range:
5364514572 is not in [-2147483648, 2147483647]; references 'far_function'
>>> referenced by simple-relocation.c
>>> defined in far-function.o

链接器报告 重定位溢出,因为所需的位移无法装入有符号 32 位字段。

如何处理这个障碍?

这是一整个涉及 代码模型(small、kernel、medium、large)以及 代码数据 引用区别的主题。
简而言之:

情形常见解决方案
调用或跳转距离 > 2 GiB使用 间接 调用/跳转(例如通过寄存器或 PLT 条目),或使用 large 代码模型编译(-mcmodel=large)。
访问静态数据距离 > 2 GiB使用 RIP‑相对 寻址并配合 large 代码模型,或先将地址加载到寄存器中。
混合静态链接和动态链接依赖动态链接器的 PLT/GOT 机制,自动生成间接引用。

关于这些主题的深入讨论可以在 @maskray 的博客文章 “Relocation overflow and code models” 中找到:

https://maskray.me/blog/2023-05-14-relocation-overflow-and-code-models

(如果链接失效,请搜索标题和作者。)

要点

  • 对巨大的代码库进行 Static linking 很容易生成超过单个相对跳转 2 GiB 范围的二进制文件。
  • x86-64 CALL/JMP 指令使用 signed 32‑bit displacement,因此直接跳转的范围被限制在 ±2 GiB。
  • 当链接器无法在该范围内放入位移时,就会出现 relocation overflow
  • 常见的解决办法包括 indirect calls/jumpsdifferent code models,或 dynamic linking(PLT/GOT)。

理解这种 “relocation barrier” 对于为 mega‑binaries(数十 GB)设计构建系统,以及决定静态链接是否真的适合特定组织,都是至关重要的。

使用 -mcmodel=large 避免重定位溢出

最简单的解决方案是使用 -mcmodel=large 编译,它会把所有相对 CALL 指令改为绝对跳转 (JMP)。

# 构建可执行文件
gcc simple-relocation.o far-function.o -T overflow.lds -o simple-relocation-overflow

# 使用大代码模型编译
gcc -c simple-relocation.c -o simple-relocation.o -mcmodel=large -fno-asynchronous-unwind-tables

# 再次链接(同上)
gcc simple-relocation.o far-function.o -T overflow.lds -o simple-relocation-overflow

# 运行
./simple-relocation-overflow

注意
-fno-asynchronous-unwind-tables 用于禁用额外的 unwind‑table 数据,否则在本示例中可能导致溢出。

切换到 -mcmodel=large 后的反汇编

objdump -dr simple-relocation-overflow
0000000120000000 :
  120000000: 55                    push   %rbp
  120000001: 48 89 e5              mov    %rsp,%rbp
  120000004: 90                    nop
  120000005: 5d                    pop    %rbp
  120000006: c3                    ret

00000000004000e6 :
  4000e6: 55                    push   %rbp
  4000e7: 48 89 e5              mov    %rsp,%rbp
  4000ea: b8 00 00 00 00        mov    $0x0,%eax
  4000ef: 48 ba 00 00 00 20 01  movabs $0x120000000,%rdx
  4000f6: 00 00 00 
  4000f9: ff d2                 call   *%rdx
  4000fb: b8 00 00 00 00        mov    $0x0,%eax
  400100: 5d                    pop    %rbp
  400101: c3                    ret

单个 CALL 指令已被 MOVABSCALL 替代——指令大小从 5 字节(opcode + 4‑字节相对偏移)增长到 12 字节(2‑字节 MOVABS opcode + 8‑字节绝对地址 + 2‑字节 CALL)。

大代码模型的缺点

  • 指令膨胀 – 每次调用现在占用 12 字节,而不是 5 字节。在调用点很多的二进制中,这会显著增加代码体积。
  • 寄存器压力 – 需要额外的通用寄存器(示例中的 %rdx)来保存绝对地址。

警告
我在构造基准测试时很难得到大 mcmodel 对 IPC(每周期指令数)产生可测量下降的结果。请相信我,这种影响并非微不足道。 🤷

保持小代码模型

如果想继续使用小代码模型,就需要探索其他策略(例如重新组织段、使用跳板或拆分二进制)。更多思路将在后续文章中讨论。

Back to Blog

相关文章

阅读更多 »

部署计算游戏应用

我把我的计算游戏应用部署到了 Vercel——专为不喜欢数学的日本孩子设计!哈哈! https://flush-calc.vercel.app/https://flush-...