And*_*zos 16 linux x86 x86-64 elf ld
我在Linux x86_64上试验ELF可执行文件和gnu工具链:
我已经链接并剥离(手工)"Hello World"测试.:
.global _start
.text
_start:
mov $1, %rax
...
Run Code Online (Sandbox Code Playgroud)
到一个267字节的ELF64可执行文件...
0000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............
0000010: 0200 3e00 0100 0000 d400 4000 0000 0000 ..>.......@.....
0000020: 4000 0000 0000 0000 0000 0000 0000 0000 @...............
0000030: 0000 0000 4000 3800 0100 4000 0000 0000 ....@.8...@.....
0000040: 0100 0000 0500 0000 0000 0000 0000 0000 ................
0000050: 0000 4000 0000 0000 0000 4000 0000 0000 ..@.......@.....
0000060: 0b01 0000 0000 0000 0b01 0000 0000 0000 ................
0000070: 0000 2000 0000 0000 0000 0000 0000 0000 .. .............
0000080: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000090: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000a0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00000b0: 0400 0000 1400 0000 0300 0000 474e 5500 ............GNU.
00000c0: c3b0 cbbd 0abf a73c 26ef e960 fc64 4026 .......<&..`.d@&
00000d0: e242 8bc7 48c7 c001 0000 0048 c7c7 0100 .B..H......H....
00000e0: 0000 48c7 c6fe 0040 0048 c7c2 0d00 0000 ..H....@.H......
00000f0: 0f05 48c7 c03c 0000 0048 31ff 0f05 4865 ..H..<...H1...He
0000100: 6c6c 6f2c 2057 6f72 6c64 0a llo, World.
Run Code Online (Sandbox Code Playgroud)
它有一个程序头(LOAD),没有部分:
There are 1 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000010b 0x000000000000010b R E 200000
Run Code Online (Sandbox Code Playgroud)
这似乎加载整个文件(文件偏移0到0x10b - elf标题和所有)在地址0x400000.
切入点是:
Entry point address: 0x4000d4
Run Code Online (Sandbox Code Playgroud)
这对应于文件中的0xd4偏移量,因为我们可以看到地址是机器代码的开头(mov $1, %rax1)
我的问题是为什么(如何)gnu链接器选择地址0x400000来映射文件?
jko*_*shy 11
起始地址通常由链接描述文件设置.
例如,在GNU/Linux上,/usr/lib/ldscripts/elf_x86_64.x我们看到:
...
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); \
. = SEGMENT_START("text-segment", 0x400000) + SIZEOF_HEADERS;
Run Code Online (Sandbox Code Playgroud)
该值0x400000是SEGMENT_START()此平台上函数的默认值.
您可以通过浏览链接器手册找到有关链接器脚本的更多信息:
% info ld Scripts
Run Code Online (Sandbox Code Playgroud)
Pet*_*des 10
ld的默认链接描述文件已0x400000为非 PIE 可执行文件嵌入该值。
PIE(位置无关的可执行文件)没有默认基地址;它们总是由内核重新定位,内核的默认值是0x0000555...加上一些 ASLR 偏移量,除非为此进程或系统范围禁用 ASLR。 ld对此无法控制。请注意,大多数现代系统将 GCC 配置-fPIE -pie为默认使用,因此它传递-pie给ld,并将 C 转换为与位置无关的 asm。如果您以这种方式链接手写汇编,则必须遵循相同的规则。
0x400000(4 MiB) 成为一个很好的默认值呢?mmap_min_addr= 65536 = 64K。远离 0 可以提供更多的空间来防止 NULL deref 和偏移读取.text或.data/.bss内存(array[i]其中arrayNULL)。即使不增加mmap_min_addr(这会在不破坏可执行文件的情况下留出空间),通常mmap也会随机选择高地址,因此在实践中我们至少有 4MiB 的空间来防止 NULL deref。
这将其置于页表的下一级页目录的开头,意味着相同数量的 4K 页表条目将被分割为更少的 2M 页目录条目,从而节省内核页表内存并帮助页面-走硬件缓存更好。对于大型静态数组,靠近下一级 1G 子树的开头也很好。
我不知道为什么是 4MiB 而不是 2MiB,或者开发人员的推理是什么。 4MiB 是没有 PAE 的 32 位大页面大小(4 字节 PTE,因此每级 10 位而不是 9 位),但 CPU 必须使用 x86-64 页表才能处于 64 位模式。
(如果不使用更大的代码模型,至少必须以有时效率较低的方式处理大型数组。有关代码模型的详细信息,请参阅x86-64 System V ABI 文档中的 3.5.1 架构约束部分。)
非 PIE 可执行文件(“小”)的默认代码模型让程序假设任何静态地址都位于虚拟地址空间的低 2GiB 中。.text因此/ .rodata、 、中.data的任何绝对地址都.bss可以用作机器代码中的 32 位符号扩展立即数,这样效率更高。
(PIE 或共享库中的情况并非如此:请参阅 x86-64 Linux 中不再允许的 32 位绝对地址?对于您/编译器因此无法在 x86-64 asm 中执行的操作,特别addss xmm0, [foo + rdi*4]是需要 RIP 相对 LEA 将数组起始地址放入寄存器。x86-64 唯一的 RIP 相对寻址模式是 [RIP+rel32],没有任何通用寄存器。)
在虚拟地址空间底部附近启动可执行文件的节/段,几乎使整个 2GiB 可用于文本+数据+bss 这么大。 (可能有一个更高的默认值,并且有大型可执行文件使 ld 选择一个较低的地址以使它们适合,但这将是一个更复杂的链接器脚本。)
这包括 .bss 中的零初始化数组,这些数组不会使可执行文件变大,而只会使内存中的进程映像变大。实际上,Fortran 程序员比 C 和 C++ 更容易遇到这种情况,因为静态数组在那里很流行。例如傻瓜式的 gfortran: mcmodel=medium 到底是做什么的?对默认small模型的构建错误以及由此产生的 x86-64 asm 差异有一个很好的解释medium(其中超过特定大小阈值的对象不假定位于代码的低 2G 或 +-2G 内。但是代码并且较小的静态数据仍然如此,因此速度损失很小。)
例如static float arr[1UL<<28];1 GiB 阵列。如果您有 3 个,它们无法全部在低 2 GiB 内启动(这可能是您手写汇编所需的全部),更不用说让每个元素都可访问了。
gcc -fno-pie期望能够编译float *p = &arr[size-1];为mov $arr+1073741820, %edi5 字节的mov $imm32. 如果目标地址距生成地址的代码超过 2GiB(或使用 加载),则 RIP 相对将不起作用movss arr+1073741820(%rip), %xmm0;即使在非 PIE 中,RIP 相对也是加载/存储静态数据的正常方式,当没有运行时变量索引时。)这就是为什么小 PIC 模型对文本+数据+bss(加上段之间的间隙)也有 2GiB 大小限制:所有静态数据和代码都需要在 2GiB 之内。想要达到它。
如果您的代码仅通过运行时变量索引访问高位元素或其地址,则只需要每个数组的开头(符号本身)位于低位 2 GiB 中。我忘记链接器是否强制将 end-of-bss 控制在低 2GiB 内;可能是因为链接描述文件在那里放置了一些 CRT 启动代码可能引用的符号。
脚注 1:对于小于 2GiB 的代码模型,没有任何有用的较小尺寸。x86-64 机器代码使用 8 位或 32 位用于立即数和寻址模式。8 位(256 字节)太小而无法使用,而且许多重要指令(例如call rel32、mov r32, imm32和[rip+rel32]寻址)只能使用 4 字节常量,而不是 1 字节常量。
限制为低 2 GiB(而不是 4)意味着地址可以安全地进行零扩展(如 )mov edi, OFFSET arr,或符号扩展(如 )mov eax, [arr + rdi*4]。[reg + disp32]请记住,地址并不是寻址模式的唯一用例;[rbp - 256]通常可以有意义,因此 x86-64 机器代码将 disp8 和 disp32 符号扩展为 64 位(而不是零扩展)是件好事。
写入 32 位寄存器时,会发生隐式零扩展至 64 位,就像使用mov-immediate 将地址放入寄存器一样,其中 32 位操作数大小是比 64 位操作数大小更小的机器代码指令。请参阅如何将函数或标签的地址加载到寄存器中(其中还涵盖了 RIP 相关的 LEA)。
Raymond Chen 写了一篇文章,介绍为什么32 位 Windows0x400000默认使用相同的基址。
他提到 DLL 默认情况下会加载到高地址,而低地址则远非如此。x86-64 SysV 共享对象可以在任何有足够大的地址空间间隙的地方加载,内核默认位于用户空间虚拟地址空间的顶部附近,即规范范围的顶部。但 ELF 共享对象需要完全可重定位,这样才能在任何地方正常工作。
32 位 Windows 选择 4MiB 的原因还在于避免低 64K(NULL deref),并为传统 32 位页表选择页目录的开头。(其中“大页”大小为 4M,对于 x86-64 或 PAE 来说不是 2M。)由于一堆 Win95 和 Win3.1 遗留内存映射原因,为什么至少 1MiB 或 4MiB 是部分必要的,以及诸如围绕 CPU 进行工作之类的东西错误。
| 归档时间: |
|
| 查看次数: |
3347 次 |
| 最近记录: |