我正在编写一个ELF加载器,并研究ELF格式.我有一个简单的Hello World二进制文件,它是我在Fedora38中使用Clang 16创建的,它可以工作,而且我没有编译\链接任何特定的选项(clang hello.c -o hello).我用readelf判断了程序头,发现了一些与我对加载和动态头的理解不符的地方.

例如:


Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
…
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x00000000000004f0 0x00000000000004f0  R      0x1000
  LOAD           0x0000000000001000 0x0000000000401000 0x0000000000401000
                 0x000000000000016d 0x000000000000016d  R E    0x1000
  LOAD           0x0000000000002000 0x0000000000402000 0x0000000000402000
                 0x00000000000000dc 0x00000000000000dc  R      0x1000
  LOAD           0x0000000000002df8 0x0000000000403df8 0x0000000000403df8
                 0x0000000000000214 0x0000000000000218  RW     0x1000
  DYNAMIC        0x0000000000002e08 0x0000000000403e08 0x0000000000403e08
                 0x00000000000001d0 0x00000000000001d0  RW     0x8

有两个问题是我无法理解的.请注意前三个加载头是如何对齐虚拟地址的,这对我来说很有意义.但是最后的加载报头和动态报头重叠并且未对齐.可执行文件在Linux中加载得很好,但Linux似乎也解释了这一点.以下是前/proc/<pid>/maps名:

00400000-00401000 r--p 00000000 00:23 16905823                         /path/to/hello
00401000-00402000 r-xp 00001000 00:23 16905823                         /path/to/hello
00402000-00403000 r--p 00002000 00:23 16905823                         /path/to/hello
00403000-00404000 r--p 00002000 00:23 16905823                         /path/to/hello
00404000-00405000 rw-p 00003000 00:23 16905823                         /path/to/hello

所以我有几个问题:

  • 为什么我的链接器会产生未对齐的段?依赖操作系统来修复对齐似乎不是一个好主意.
  • 为什么我的链接器会创建重叠的线段?再说一次,依赖操作系统来修复这个问题似乎不是一个好主意.
  • Linux是否实现了特定的约定、标准或算法来纠正这一点?感觉这里有一些未记录在案的行为,或者有记录的行为,我一直无法找到.

推荐答案

为什么我的链接器会产生未对齐的段?

不是的.要求是可以直接对这LOAD个片段进行mmap个,因此,下面的must条是真的:p_vaddr % pagesize == p_offset % pagesize.对于输出中的所有LOAD个段都是如此.

为什么我的链接器会创建重叠的线段?

这没有什么错--相同的文件内容会在内存中出现两次.

另一种方法是在文件中插入一个大洞,浪费磁盘空间.

另请参见this answer.

Linux是否实现了特定的约定、标准或算法来纠正这一点?

不需要修正.仔细阅读man mmap个单词.

由于offset必须是页面对齐的,因此动态加载器向下舍入bothp_vaddrp_offset以页面对齐,并使用这两个值的页面对齐值执行mmap(..., MAP_FIXED, ...).这"映射"了一些数据,但保证了代码或数据的最终地址与其链接的地址完全相同.

Update:

为什么链接器不总是将p_offset和p_vaddr设置为运行时使用的实际值

通常不会这样做,因为静态链接器不知道运行时的页面大小.在x86_64上,pagesize可能是4KiB、2MiB或1GiB.

然而,该特定二进制与0x1000(4KiB)对齐相链接,因此(静态)链接器could have.p_vaddar.p_offset设置为它们的四舍五入值(并向上调整.p_memsize以进行补偿).静态链接器中的代码与对齐和页面大小无关,因此它没有将4KiB视为special(完全合理的方法).

C++相关问答推荐

当包含头文件时,gcc会发出隐式函数声明警告

C如何显示字符串数组中的第一个字母

常数函数指针优化

为什么STM32G474RE上没有启用RCC PLL

GCC创建应用于移动项的单独位掩码的目的是什么?

获取每个循环迭代结束时的当前时间

解决S随机内存分配问题,实现跨进程高效数据共享

为什么数组的最后一个元素丢失了?

为 struct 中的数组动态分配内存时出错

在C语言中,指针指向一个数组

C11/C17标准允许编译器清除复合文字内存吗?

GCC错误,共享内存未定义引用?

C中2个数字的加法 - 简单的人类方法

无法理解 fgets 输出

如何使用 VLA 语法使用 const 指针声明函数

cs50拼写器分配中的无限循环

与 C 相比,C++ 中无副作用的无限循环的好处是 UB?

Clang 是否为内联汇编生成了错误的代码?

将帧从相机 (/dev/video0) 复制到帧缓冲区 (/dev/fb0) 会产生意外结果

K&R 练习 1-24