C语言 在实践中是否有可能将数百万个小函数编译成静态二进制文件?



我已经创建了一个包含大约 200 万个小函数的静态库,但我在 Linux x86_64 下使用 GCC(测试 4.8.5 或 7.3.0(将其链接到我的主函数时遇到了问题。

链接器抱怨重定位截断,非常类似于此问题中的截断。

我已经尝试过使用-mcmodel=large,但正如同一问题的答案所说,我会 "需要一个可以处理完整 64 位地址的 CRT1.o"。然后我尝试编译一个,按照这个答案,但最近的 glibc 不会在-mcmodel=large下编译,即使 libgcc 这样做,也没有任何作用。

我也尝试添加标志-fPIC和/或-fPIE无济于事。我得到的最好的是这个唯一的错误:

ld:无法转换 GOTPCREL 重定位;重新链接 --no-relax

添加该标志也无济于事。

我已经在互联网上搜索了几个小时,但大多数帖子都非常旧,我找不到一种方法来做到这一点。

我知道这不是一件常见的事情,但我认为应该可以做到这一点。我在 HPC 环境中工作,因此内存或时间限制不是这里的问题。

有没有人成功地用最近的编译器和工具链完成了类似的事情?

要么不要使用标准库,要么修补它。至于 2.34 版本,Glibc 不支持大代码模型。(另见Glibc邮件列表和Redhat Bugzilla(

解释

让我们检查一下 Glibc 源代码,以了解为什么使用-mcmodel=large重新编译一无所获。它替换了源自 C 文件的重定位。但是 Glibc 在原始程序集文件中包含硬编码的 32 位重定位,例如在start.S(sysdeps/x86_64/start.S中(。

call *__libc_start_main@GOTPCREL(%rip)

start.S发出R_X86_64_GOTPCREL用于使用相对寻址的__libc_start_main。 x86_64CALL指令不支持超过 32 位位移的相对跳转,请参阅 AMD64 手册 3。因此,ld无法抵消重新定位R_X86_64_GOTPCREL,因为代码大小超过 2GB。

由于相同的 ISA 约束,添加-fPIC没有帮助。对于与位置无关的代码,编译器仍会生成相对跳转。

修补

简而言之,您必须替换程序集代码中的 32 位重定位。有关实现 64 位重定位的详细信息,请参阅 System V 应用程序二进制接口 AMD64 体系结构进程补充。另请参阅此处,了解有关代码模型的更深入说明。

为什么 32 位重定位不足以满足大型代码模型?因为我们不能依赖其他符号在 2GB 的范围内。所有调用都必须是绝对的。与小型 PIC 代码模型相反,在小型 PIC 代码模型中,编译器尽可能生成相对跳转。

让我们仔细看看搬迁R_X86_64_GOTPCREL。它包含 RIP 和符号的 GOT 条目地址之间的 32 位差异。它有一个 64 位替代品 —R_X86_64_GOTPCREL64,但我找不到在汇编中使用它的方法。

因此,要替换GOTPCREL,我们必须计算符号条目GOT基本偏移量GOT地址本身。我们可以在函数序言中计算一次 GOT 位置,因为它不会改变。

首先,让我们获取 GOT 基础(从 ABI 补充中批量提升的代码(。GLOBAL_OFFSET_TABLE重定位指定相对于当前位置的偏移:

leaq 1f(%rip), %r11
1: movabs $_GLOBAL_OFFSET_TABLE_, %r15
leaq (%r11, %r15), %r15

由于GOT基位于%r15寄存器上,现在我们必须找到交易品种的GOT条目偏移量。R_X86_64_GOT64重新定位正是指定了这一点。有了这个,我们可以将__libc_start_main调用重写为:

movabs $__libc_start_main@GOT, %r11
call *(%r11, %r15)

我们用GLOBAL_OFFSET_TABLER_X86_64_GOT64替换了R_X86_64_GOTPCREL.以同样的方式替换其他人。

注意:将动态链接可执行文件中的函数的R_X86_64_GOT64替换为R_X86_64_PLTOFF64

测试

使用以下需要大型代码模型的测试验证修补程序的正确性。它不包含一百万个小函数,而是有一个大函数和一个小函数。

编译器必须支持大型代码模型。如果您使用 GCC,则需要使用标志-mcmodel=large从源代码构建它。启动文件不应包含 32 位重定位。

foo函数占用超过 2GB,导致 32 位重定位不可用。因此,如果在没有-mcmodel=large的情况下编译,测试将失败并出现溢出错误。另外,添加标志-O0 -fPIC -static,用黄金链接。

extern int foo();
extern int bar();
int foo(){
bar();
// Call sys_exit
asm( "mov $0x3c, %%rax n"
"xor %%rdi, %%rdi n"
"syscall n"
".zero 1 << 32 n"
: : : "rax", "rdx");
return 0;
}
int bar(){
return 0;
}
int __libc_start_main(){
foo();
return 0;
}
int main(){
return 0;
}

:注:我使用了没有标准库本身的修补 Glibc 启动文件,所以我必须同时定义_libc_start_mainmain