巨大的二进制文件

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

Source: Hacker News

在攻读博士学位并提交学术文章的过程中,我遇到的一个问题是,我构建的解决方案需要极大规模才能有效且有价值。
对我的论文投稿的回复常常声称此类问题并不存在;然而,我在业界(例如在 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

e8 字节是 CALL 操作码(它使用 32 位有符号相对偏移)。
目前偏移为 0(四个字节的 0)。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 GiB 窗口内的代码。此限制被称为 2 GiB 重定位屏障

当目标距离超过 2 GiB 时会怎样?

我们可以使用 链接脚本 强制链接器把 far_function 放得很远。

/* 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 */
    . = 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 位字段。

处理障碍

当我们遇到这个问题时,有几种选择,统称为 代码模型。合适的解决方案取决于我们访问的对象是:

  • 数据(静态变量、常量)
  • 代码(函数、跳转目标)

关于这些技术的深入讨论可参见博客文章 “Relocation overflow and code models” 作者 @maskray。

TL;DR

  • x86‑64 的 CALL/JMP 指令使用 32 位有符号相对偏移,因此直接跳转的范围限制在 ±2 GiB
  • 巨大的静态二进制文件很容易超出此限制,导致链接时出现 重定位溢出 错误。
  • 解决方案包括使用不同的 代码模型(如 small、medium、large 或 PIE)、间接跳转跳板(trampoline),或 动态链接,以确保所有调用目标都在可达范围内。

在处理产生数十 GB 大小二进制文件的 超大代码库 时,理解并绕过 2 GiB 重定位屏障至关重要。

com/maskray — lld 的作者。

最简单的办法是使用 -mcmodel=large,它会把所有相对的 CALL 指令改为绝对的 JMP

# Build the overflow example
gcc simple-relocation.o far-function.o -T overflow.lds -o simple-relocation-overflow

# Compile with the large code model
gcc -c simple-relocation.c -o simple-relocation.o -mcmodel=large -fno-asynchronous-unwind-tables

# Link again
gcc simple-relocation.o far-function.o -T overflow.lds -o simple-relocation-overflow

# Run
./simple-relocation-overflow

注意
为了演示的目的,我需要添加 -fno-asynchronous-unwind-tables 来禁用可能导致溢出的额外数据。

现在的反汇编是什么样的?

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 指令;它已经变成了 MOVABS 加上 CALL 😲。这把指令大小从 5 字节(opcode + 4‑字节相对偏移)膨胀到了惊人的 12 字节(2‑字节 ABS opcode + 8‑字节绝对地址 + 2‑字节 CALL)。

显著的缺点

  • 指令膨胀 – 每个调用从 5 字节增至 12 字节。在拥有数百万调用点的二进制文件中,这会迅速累积。
  • 寄存器压力 – 为了完成跳转,我们消耗了一个通用寄存器(%rdx)。

警告
我在构建基准测试时遇到了很多困难,未能展示 large mcmodel 在 IPC(每周期指令数)上的更差表现,所以只能请大家相信我的结论。 🤷

我们希望保持小代码模型。还有哪些策略可以采用?

后续文章将继续展开讨论。

Back to Blog

相关文章

阅读更多 »

二进制

2 GiB “Relocation Barrier” – 为什么大规模二进制在 x86‑64 上会崩溃 我在攻读 PhD 并提交学术文章时遇到的一个问题是…

真实世界代理示例与 Gemini 3

2025年12月19日 我们正进入 agentic AI 的新阶段。开发者正超越简单的 notebooks,构建复杂、production‑ready 的 agentic 工作流……