问题描述
我有一个小的x86-64汇编程序,我在2018年进行了编译和链接。我现在正在尝试重现该版本,但是在链接时,最终的二进制文件得到了不同的结果。
两个文件都是使用以下命令组装和链接的:
$ nasm -f elf64 prng.asm; ld -s -o prng prng.o
我在2018年创建的原始ELF被命名为prng
。我今天创建的版本名为prng2
。我已经验证了中间目标文件prng.o
是相同的,所以我排除了源代码或nasm是造成我所看到的差异的原因。下面显示了每个新旧ELF上objdump
的输出:
原文:
$ objdump -x prng
prng: file format elf64-x86-64
prng
architecture: i386:x86-64,flags 0x00000102:
EXEC_P,D_PAGED
start address 0x00000000004000b0
Program Header:
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**21
filesz 0x0000000000000150 memsz 0x0000000000000150 flags r-x
LOAD off 0x0000000000000150 vaddr 0x0000000000600150 paddr 0x0000000000600150 align 2**21
filesz 0x0000000000000008 memsz 0x0000000000000008 flags rw-
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000a0 00000000004000b0 00000000004000b0 000000b0 2**4
CONTENTS,ALLOC,LOAD,READONLY,CODE
1 .data 00000008 0000000000600150 0000000000600150 00000150 2**2
CONTENTS,DATA
SYMBOL TABLE:
no symbols
最新:
$ objdump -x prng2
prng2: file format elf64-x86-64
prng2
architecture: i386:x86-64,D_PAGED
start address 0x0000000000401000
Program Header:
LOAD off 0x0000000000000000 vaddr 0x0000000000400000 paddr 0x0000000000400000 align 2**12
filesz 0x00000000000000e8 memsz 0x00000000000000e8 flags r--
LOAD off 0x0000000000001000 vaddr 0x0000000000401000 paddr 0x0000000000401000 align 2**12
filesz 0x00000000000000a0 memsz 0x00000000000000a0 flags r-x
LOAD off 0x0000000000002000 vaddr 0x0000000000402000 paddr 0x0000000000402000 align 2**12
filesz 0x0000000000000008 memsz 0x0000000000000008 flags rw-
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 000000a0 0000000000401000 0000000000401000 00001000 2**4
CONTENTS,CODE
1 .data 00000008 0000000000402000 0000000000402000 00002000 2**2
CONTENTS,DATA
SYMBOL TABLE:
no symbols
我可以看到差异似乎可以归结为不同的对齐方式。但是,我无法确定是什么原因导致使用了不同的对齐方式。
- 今天我正在使用Ubuntu 20.04.1,而在2018年我正在使用Ubuntu 16.04。
- 我今天正在使用AMD Ryzen 3700X CPU,而在2018年我正在使用Intel Core i7-860。
我相信ld
的版本将在两个Ubuntu版本之间进行更改。在此期间ld的对齐行为是否可能会发生变化,例如使用不同的默认链接描述文件?
还是CPU会影响对齐值的选择?
为什么现在程序头有3个部分,而以前只有2个?
解决方法
现代ld
将.rodata
部分放在单独的read-without-exec页面中。这就需要将其放在单独的ELF segment 中(程序头条目,由加载程序读取)。术语:ELF sections 是“程序标题”列表后面“ Sections”列表中列出的内容。
较旧的ld
将.rodata
与.text
放在同一段中,对exec只读。这在过去几年中发生了变化,例如2018年? (自2017年以来,我一直在使用Arch GNU / Linux,这是一个滚动发行版,主要使用未经修改的上游源代码,并且在IIRC左右的某个时间发生了变化。)
较旧的ld
在与.data
开头相同的磁盘页面中,还具有ELF标头和.text
的初始化程序。 (对于.data和.text总计小于4k的小文件)。该磁盘页以两种不同的方式映射:文本段的Read + Exec,用于代码和只读数据的虚拟地址,数据段的Read + Write,用于.data
。
请注意0x00000000004000b0
的入口点地址(在ELF标头+数据之后,距页面开头的一些小偏移)与在新的可执行文件中对齐的0x0000000000401000
页面的地址。 对齐磁盘上的数据可以映射到虚拟内存,而不会与不需要执行的可执行段重叠。这样做的自然结果是页面对齐的内存地址,但这是副作用。效果,而不是目标。
您的可执行文件没有.rodata
部分(您的输入也没有),但是ELF标头本身仍被映射为具有LOAD属性的段(映射到内存)。
顺便说一句,更喜欢使用readelf
而不是objdump
来检查ELF标头。
此更改通过不使恒定数据作为“小工具”跳转到来帮助防止ROP和Spectre攻击。 (现在,大多数程序通过确保W ^ X使得不可能进行代码注入,因此,更复杂的攻击必须寻找现有的可执行字节序列。因此,强化的下一步是使不必要的页面尽可能少地成为可执行文件。 )
它与您所运行的CPU或所构建的CPU无关。正如@old_timer指出的,您不应期望工具链的不同版本中具有相同的二进制文件。出于这种或其他原因,甚至可以对这样的默认值进行更改,或者甚至可以将某个工具将工具版本签名嵌入到元数据中的某个位置。 (像GCC这样的编译器会这样做,也许NASM和ld
不会。)
您可以从源代码构建旧版本的GNU binutils,或从二进制包中获取旧的ld。
或者编写自己的链接描述文件,将.rodata
与.text
放在同一程序段中。 (我认为ld
可以通过使用默认的链接描述文件来工作;如果可以在较旧的ld源中找到默认的链接描述文件,则可以将其与已安装的当前ld一起使用。)