为什么使用GCC编译的C程序需要.eh_frame节?

71

测试在使用gcc 4.6.3编译的32位x86 Linux上进行。

当使用gcc编译C程序并使用readelf检查节信息时,可以看到.eh_frame和.eh_frame_hdr节。

例如,这是二进制程序Perlbench的节信息。

readelf -S perlbench

There are 28 section headers, starting at offset 0x102e48:

Section Headers:
[Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
[ 0]                   NULL            00000000 000000 000000 00      0   0  0
[ 1] .interp           PROGBITS        08048154 000154 000013 00   A  0   0  1
[ 2] .note.ABI-tag     NOTE            08048168 000168 000020 00   A  0   0  4
[ 3] .note.gnu.build-i NOTE            08048188 000188 000024 00   A  0   0  4
[ 4] .gnu.hash         GNU_HASH        080481ac 0001ac 000044 04   A  5   0  4
[ 5] .dynsym           DYNSYM          080481f0 0001f0 0007b0 10   A  6   1  4
[ 6] .dynstr           STRTAB          080489a0 0009a0 0003d6 00   A  0   0  1
[ 7] .gnu.version      VERSYM          08048d76 000d76 0000f6 02   A  5   0  2
[ 8] .gnu.version_r    VERNEED         08048e6c 000e6c 0000a0 00   A  6   2  4
[ 9] .rel.dyn          REL             08048f0c 000f0c 000028 08   A  5   0  4
[10] .rel.plt          REL             08048f34 000f34 000388 08   A  5  12  4
[11] .init             PROGBITS        080492bc 0012bc 00002e 00  AX  0   0  4
[12] .plt              PROGBITS        080492f0 0012f0 000720 04  AX  0   0 16
[13] .text             PROGBITS        08049a10 001a10 0cf86c 00  AX  0   0 16
[14] .fini             PROGBITS        0811927c 0d127c 00001a 00  AX  0   0  4
[15] .rodata           PROGBITS        081192a0 0d12a0 017960 00   A  0   0 32
[16] .eh_frame_hdr     PROGBITS        08130c00 0e8c00 003604 00   A  0   0  4
[17] .eh_frame         PROGBITS        08134204 0ec204 01377c 00   A  0   0  4
[18] .ctors            PROGBITS        08148f0c 0fff0c 000008 00  WA  0   0  4
[19] .dtors            PROGBITS        08148f14 0fff14 000008 00  WA  0   0  4
[20] .jcr              PROGBITS        08148f1c 0fff1c 000004 00  WA  0   0  4
[21] .dynamic          DYNAMIC         08148f20 0fff20 0000d0 08  WA  6   0  4
[22] .got              PROGBITS        08148ff0 0ffff0 000004 04  WA  0   0  4
[23] .got.plt          PROGBITS        08148ff4 0ffff4 0001d0 04  WA  0   0  4
[24] .data             PROGBITS        081491e0 1001e0 002b50 00  WA  0   0 32
[25] .bss              NOBITS          0814bd40 102d30 002b60 00  WA  0   0 32
[26] .comment          PROGBITS        00000000 102d30 00002a 01  MS  0   0  1
[27] .shstrtab         STRTAB          00000000 102d5a 0000ec 00      0   0  1

据我理解,这两个部分用于处理异常,它们生成描述如何展开堆栈的表格。

但这是为了C++程序而设计的,它们使用eh_framegcc_exception_table部分管理异常,那么编译器为什么会将eh_frameeh_frame_hdr部分放在从C程序编译的ELF中呢?


9
x86-64 ABI规定必须处处使用.eh frames。这是为了允许在整个系统中进行堆栈展开,因为在某些情况下,比如在分析工具中,这是系统范围内所需的。因此,在x86_64上,GCC默认生成EH frame并尝试使其处处精确,而在大多数其他ABI上,默认仅在需要EH frame且可能触发EH和展开的程序点才生成EH frame,并且只在这些程序点处精确。 - askmish
我怀疑gcc也使用它们来实现__attribute__((cleanup(..))) - gsg
可能是这个问题的重复 - Ciro Santilli OurBigBook.com
“那个问题中的参考?”我不明白你的意思。你是指评论吗?为什么问题不同?我可能错了。另一种可能是反过来关闭:这个问题稍微更好一些,但另一个问题更旧,选择很艰难。关于用户名,让我们在元数据或Twitter上进行讨论。干杯。 - Ciro Santilli OurBigBook.com
1个回答

88
首先,添加基于DWARF的堆栈回溯(.eh_frame)的最初原因在很大程度上是出于政治考虑 - 这些人希望它成为一个始终存在的功能,以便用于实现除了C++异常之外的各种东西,包括:

  • backtrace()
  • __attribute__((__cleanup__(f)))
  • __builtin_return_address(n), 对于n>0
  • pthread_cleanup_push,使用__attribute__((__cleanup__(f)))实现
  • ...

然而,如果您不需要这些功能中的任何一项,则 .eh_frame 就像是 .text 大小增加了15-30%,没有任何好处。您可以使用-fno-asynchronous-unwind-tables来禁用单个翻译单元中 .eh_frame 的生成,并且这样基本上消除了大小的成本,尽管仍然有一些来自 crtbegin.o 等的残留。您不能使用strip命令剥离它们;因为.eh_frame 是生存于程序的加载部分的一个区段(这就是整个意义),剥离它会以打破程序运行为代价进行修改。请参见https://sourceware.org/bugzilla/show_bug.cgi?id=14037,其中展示了一些可能导致问题的示例。

请注意,DWARF表还用于调试,但是出于此目的,它们不需要在程序的可加载部分中。使用 -fno-asynchronous-unwind-tables 不会破坏调试,因为只要编译器也传递了 -g,则仍会生成这些表; 它们只是存储在二进制文件的一个单独、不可加载和可以剥离的部分中,即.debug_frame


2
一个小修正:crtbegin.o 在 RedHat 和 Ubuntu 上至少不包含 CFI。剩下的很可能来自于 __libc_csu_init__libc_csu_fini 的 CFI。它们不在 crtbegin.o 中,而是在 GLIBC 的非共享部分 libc_nonshared.a 中。 - Hristo Iliev
2
你好,我想知道eh_frame的使用是否是GCC特定的功能?我尝试使用clanggcc编译一段C代码。由GCC生成的eh_frame部分大约为0x5cfc,而对于clang来说只有a4 - lllllllllllll
1
值得一提的是,异常展开可能需要遍历 C 函数的堆栈帧。例如,如果一个 C 库接受一个函数指针作为回调函数,或者类似的东西,它可能会再次调用 C++ 函数。顺便说一下,-fno-omit-frame-pointer 可以显著缩小 eh_frame 的大小,gcc 开发人员已经考虑尝试让 gcc 在不会减慢速度的函数中使用 rbp 作为帧指针。 - Peter Cordes
2
@PeterCordes:这是未定义行为,即使不是也几乎肯定会违反C库的契约 - 大多数使用回调的C库代码都假定回调返回而不是longjmp,将异常传播出去等同于longjmp。如果从C中调用使用异常的C++代码作为回调,则回调需要捕获所有异常并将它们转换为错误返回给C调用者。 - R.. GitHub STOP HELPING ICE
4
@PeterCordes: “C++异常应该能够在C中传播,尽管这是未定义的行为”这是一个政治立场(依我之见是无可辩驳的,因为如上所述,即使它被定义了,它也只能用于非常纯净的C函数,而且不需要在错误时进行清理)。” - R.. GitHub STOP HELPING ICE
显示剩余7条评论

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