.text 与 .data 需要对齐

Required alignment of .text versus .data

我一直在研究 ELFIO library. One of the examples,特别是它允许人们从头开始创建 ELF 文件——定义部分、段、入口点,并为相关部分提供二进制内容。

I noticed that a program created this way segfaults when the code segment alignment is chosen less than the page size (0x1000):

// Create a loadable segment
segment* text_seg = writer.segments.add();
text_seg->set_type( PT_LOAD );
text_seg->set_virtual_address( 0x08048000 );
text_seg->set_physical_address( 0x08048000 );
text_seg->set_flags( PF_X | PF_R );
text_seg->set_align( 0x1000 ); // can't change this

注意 .text 部分 在同一示例中仅与 0x10 的倍数对齐:

section* text_sec = writer.sections.add( ".text" );
text_sec->set_type( SHT_PROGBITS );
text_sec->set_flags( SHF_ALLOC | SHF_EXECINSTR );
text_sec->set_addr_align( 0x10 );

但是数据段虽然通过同样的机制单独加载,但是没有这个问题:

segment* data_seg = writer.segments.add();
data_seg->set_type( PT_LOAD );
data_seg->set_virtual_address( 0x08048020 );
data_seg->set_physical_address( 0x08048020 );
data_seg->set_flags( PF_W | PF_R );
data_seg->set_align( 0x10 ); // note here!

现在,在这种特定情况下,数据按设计适合已分配的页面。不确定这是否有任何区别,但我将其虚拟地址更改为 0x8148020,结果仍然正常。

这是 readelf 的输出:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000001000 0x0000000008048000 0x0000000008048000
                 0x000000000000001d 0x000000000000001d  R E    1000
  LOAD           0x0000000000001020 0x0000000008148020 0x0000000008148020
                 0x000000000000000e 0x000000000000000e  RW     10

为什么可执行段的对齐不是0x1000的倍数,但是对于数据0x10没问题,为什么程序执行失败?


更新: 不知怎的,第二次尝试 text_seg->set_align( 0x100 ); 也可以,text_seg->set_align( 0x10 ); 失败了。页面大小为 0x1000,有趣的是,工作程序的 VirtAddr 在任何一个段中都没有遵守它:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000100 0x08048100 0x08048100 0x0001d 0x0001d R E 0x100
  LOAD           0x000120 0x08148120 0x08148120 0x0000e 0x0000e RW  0x10

SIGSEGV'ing 之一:

Program Headers:
  Type           Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
  LOAD           0x000080 0x08048100 0x08048100 0x0001d 0x0001d R E 0x10
  LOAD           0x0000a0 0x08148120 0x08148120 0x0000e 0x0000e RW  0x10

生成的 ELF 是 here

(抱歉,更多的是扩展评论而不是答案)

有一些关于 ELF 可执行文件应该是什么的规范。特别阅读elf(5) and most importantly the relevant ABI specification (see also this question), e.g. on https://github.com/hjl-tools/x86-psABI/wiki/X86-psABI

AFAIU,这些规范要求代码和数据段都是页面对齐的,但您需要检查一下,特别是在 ABI 规范的第 5 章(程序加载和动态链接)中。

当前生成 ELF 可执行文件的工具(特别是 binutils)正在努力遵守这些规范。如果你编写一些 ELF 生成器,你也应该努力遵守这些规范(因此测试生成的 ELF 显然有效是不够)。

内核正在实现 execve(2), and dynamic loading is done also with ld-linux(8) using mmap(2)。由于某些原因(可能是性能),它不会检查可执行文件是否符合所有规范。

(当然,内核人员希望通常生成的 ELF 可执行文件能够成功 execve-d)

在某些极端情况下(比如您观察到的情况),内核不会出现故障 execve 并且不会对构造不当的 ELF 文件执行某些操作。

但恕我直言,对此无法保证。未来的内核和未来的 x86-64 处理器可能会在此类构造不当的 ELF 文件上失败。

我的感觉是您处于某个灰色区域,有点 execve 的 "undefined behavior"。如果碰巧成功了,那就是运气不好。

Why does the program fail to execute when the alignment of the executable segment is not a multiple of 0x1000 but for data 0x10 is no problem?

要准确理解原因,您需要深入研究特定内核的源代码(与 execve 相关)。而且我相信它可能会在未来发生变化(内核的未来版本)。

内核社区或多或少地承诺过去的兼容性,但这是与 规范 相关的。某些格式错误的 ELF 可执行文件可能会被 Linux 3.10 execve-d 而不是 Linux 4.13 或某些未来的 Linux 5.

(我确实读到一些过去的内核已经能够 execve-d 一些格式错误的 ELF 可执行文件,但我忘记了细节,也许与 16 字节对齐有关堆栈指针)

ELF ABI 不要求 VirtAddrPhysAddr 页面对齐。 (我相信)它只需要

({Virt,Phys}Addr - Offset) % PageSize == 0

对于两个工作二进制文件都是正确的,对于非工作二进制文件是错误的。

更新:

I don't see how this fails for the latter.

我们有:VirtAddr == 0x08048100Offset == 0x80(和 PageSize == 4096 == 0x1000)。

(0x08048100 - 0x80) % 0x1000 == 0x80 != 0

has to agree when align == 0x10, doesn't it?

否:它必须与页面大小一致(正如我之前所说),否则内核将无法 mmap 该段。