我有一个小的x86-64汇编程序,我在2018年编译并链接了它。我现在正试图复制构建,但在链接时,我在最终的二进制文件中得到了不同的结果。
这两个文件都是使用以下命令组装和链接的:
$ nasm -f elf64 prng.asm; ld -s -o prng prng.o
我在2018年创建的原始ELF名为prng
。我今天创建的版本名为prng2
。我已经验证了中间对象文件prng.o
是相同的,所以我排除了源代码或nasm是我看到的差异的原因。下面我展示了objdump
在每个ELF上的输出,无论是旧的还是新的:
原件:
$ 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, ALLOC, LOAD, DATA
SYMBOL TABLE:
no symbols
最新:
$ objdump -x prng2
prng2: file format elf64-x86-64
prng2
architecture: i386:x86-64, flags 0x00000102:
EXEC_P, 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, ALLOC, LOAD, READONLY, CODE
1 .data 00000008 0000000000402000 0000000000402000 00002000 2**2
CONTENTS, ALLOC, LOAD, DATA
SYMBOL TABLE:
no symbols
我可以看出,这种差异似乎是由于不同的路线造成的。但是,我无法确定是什么原因导致使用不同的路线。
- 我今天使用的是Ubuntu 20.04.1,而2018年我使用的是Ubuntu16.04
- 我今天使用的是AMD Ryzen 3700X CPU,而在2018年,我使用的是英特尔酷睿i7-860
我相信ld
的版本会在Ubuntu的两个版本之间发生变化。在那段时间里,ld的对齐行为是否可能发生了变化,例如使用了不同的默认链接器脚本?
或者CPU是否会影响对齐值的选择?
为什么现在程序头有三个部分,而以前只有两个?
Modernld
将.rodata
部分放在一个单独的read without exec页面中。这需要将它放在一个单独的ELF段(程序头条目,由加载程序读取(中。术语:ELF节是在程序标题列表之后的"节"列表中列出的内容。
较旧的ld
将.rodata
放在与.text
相同的段中,使用exec只读。这在过去几年里确实发生了变化,比如2018年?(大约从2017年开始,我就一直在使用Arch GNU/Linux,这是一个滚动发行版,主要使用未经修改的上游源代码,大约在IIRC的某个时候发生了变化。(
较旧的ld
在与.text
开头相同的磁盘页中也有ELF头和.data
的初始化器。(对于.data和.text总计小于4k的小文件(。此磁盘页以两种不同的方式映射:文本段的Read+Exec,位于用于代码和只读数据的虚拟地址;数据段的Read+Write,用于.data
。
请注意0x00000000004000b0
的入口点地址(在ELF标题+数据之后,从页面开始的某个小偏移量(与新可执行文件中对齐的0x0000000000401000
页面通过对齐磁盘上的数据,可以映射到虚拟内存中,而不会将任何内容重叠到不需要执行的可执行段中这样做的自然结果是页面对齐的内存地址,但这是副作用,而不是目标。
您的可执行文件没有.rodata
部分(您的输入也没有(,但ELF标头本身仍然映射在具有LOAD属性的段中(映射到内存中(。
BTW,更喜欢使用readelf
而不是objdump
来检查ELF报头。
这一变化通过不使恒定数据作为"可用"来帮助防止ROP和Spectre攻击;小工具";跳转到。(现在大多数程序通过确保W^X使代码注入变得不可能,更复杂的攻击必须寻找现有的可执行字节序列。因此,强化的下一步是尽可能少地制作不需要的可执行页面。(
它与您运行的CPU或构建的CPU无关。正如@old_timer所指出的,您不应该期望来自不同版本的工具链的相同二进制文件。出于这个或其他原因,或者甚至为了将工具版本签名嵌入到元数据中的工具,对默认值进行这样的更改当然是可能的。(像GCC这样的编译器会这样做,可能NASM和ld
不会。(
您可以从源代码构建GNU binutils的旧版本,或者从二进制包中获取旧ld。
或者可以编写自己的链接器脚本,将.rodata
与.text
放在同一程序段中。(我认为ld
的工作原理是有一个默认的链接器脚本;如果你能在旧的ld源中找到默认的链接程序脚本,你可能可以将它与你安装的当前ld一起使用。(