./hello是c中的一个简单的echo程序。 根据objdump文件头,
$ objdump -f ./hello
./hello: file format elf32-i386
architecture: i386, flags 0x00000150:
HAS_SYMS, DYNAMIC, D_PAGED
start address 0x00000430
./hello的起始地址为0x430
现在在gdb中加载此二进制文件。
(gdb) file ./hello
Reading symbols from ./hello...(no debugging symbols found)...done.
(gdb) x/x _start
0x430 <_start>: 0x895eed31
(gdb) break _start
Breakpoint 1 at 0x430
(gdb) run
Starting program: /1/vooks/cdac/ditiss/proj/binaries/temp/hello
Breakpoint 1, 0x00400430 in _start ()
(gdb) x/x _start
0x400430 <_start>: 0x895eed31
(gdb)
在设置断点或运行二进制文件之前的上述输出中,_start的地址为0x430,但运行后,该地址变为0x400430。
$ readelf -l ./hello | grep LOAD
LOAD 0x000000 0x00000000 0x00000000 0x007b4 0x007b4 R E 0x1000
LOAD 0x000eec 0x00001eec 0x00001eec 0x00130 0x00134 RW 0x1000
这种映射如何发生?
请帮助。
基本上,在链接之后,ELF文件格式为加载器提供了将程序加载到内存并运行它的所有必要信息。
每段代码和数据都放在一个区域内的偏移区域内,如数据区段,文本区域等,并且通过向区段起始地址添加适当的偏移量来完成特定功能或全局变量的访问。
现在,ELF文件格式还包括程序头表:
可执行文件或共享目标文件的程序头表是一组结构,每个结构描述系统准备程序执行所需的段或其他信息。目标文件段包含一个或多个部分,如“段内容”中所述。
然后,OS加载程序使用这些结构将图像加载到内存中。结构:
typedef struct {
Elf32_Word p_type;
Elf32_Off p_offset;
Elf32_Addr p_vaddr;
Elf32_Addr p_paddr;
Elf32_Word p_filesz;
Elf32_Word p_memsz;
Elf32_Word p_flags;
Elf32_Word p_align;
} Elf32_Phdr;
请注意以下字段:
p_vaddr
段的第一个字节驻留在内存中的虚拟地址
p_offset
从段的第一个字节所在的文件开头的偏移量。
和p_type
此数组元素描述的段的类型或如何解释数组元素的信息。类型值及其含义在表7-35中规定。
从表7-35,注意PT_LOAD
:
指定可加载的段,由p_filesz和p_memsz描述。文件中的字节映射到内存段的开头。如果段的内存大小(p_memsz)大于文件大小(p_filesz),则定义额外字节以保持值0并跟随段的初始化区域。文件大小不能大于内存大小。程序头表中的可加载段条目以升序显示,在p_vaddr成员上排序。
因此,通过查看这些字段(以及更多),加载程序可以在ELF文件中找到段(可以包含多个部分),并将它们(PT_LOAD
)加载到给定虚拟地址的内存中。
现在,可以在运行时(加载时间)更改ELF文件段的虚拟地址吗?是:
程序头中的虚拟地址可能不代表程序内存映像的实际虚拟地址。请参阅“程序加载(特定于处理器)”。
因此,程序头包含OS加载器将加载到内存中的段(可加载段,其中包含可加载段),但加载器放置它们的虚拟地址可能与ELF文件中的地址不同。
怎么样?
要理解它,让我们先阅读有关Base Address
的内容
可执行文件和共享对象文件具有基址,该基址是与程序目标文件的内存映像关联的最低虚拟地址。基地址的一个用途是在动态链接期间重新定位程序的存储器映像。
可执行文件或共享目标文件的基址在执行期间从三个值计算:内存加载地址,最大页面大小和程序可加载段的最低虚拟地址。程序头中的虚拟地址可能不代表程序内存映像的实际虚拟地址。请参阅“程序加载(特定于处理器)”。
所以练习如下:
与位置无关的代码。此代码使段的虚拟地址从一个进程更改为另一个进程,而不会使执行行为无效。
虽然系统为各个进程选择虚拟地址,但它保持了段的相对位置。由于与位置无关的代码使用段之间的相对寻址,因此内存中虚拟地址之间的差异必须与文件中虚拟地址之间的差异相匹配。
因此,通过使用相对寻址(PIE位置无关可执行文件),实际放置可能与ELF文件中的地址不同。
来自PeterCordes
的回答:
0x400000
是Linux默认基址,用于在禁用ASLR的情况下加载PIE可执行文件(默认情况下与GDB一样)。
因此,对于您的特定情况(Linux中的PIE可执行文件),加载器选择此base address
。
当然,独立位置只是一种选择。程序可以在没有它的情况下编译,并且绝对寻址模式发生,其中ELF中的段地址与实际存储器地址段之间不得有差别被加载到:
可执行文件段通常包含绝对代码。要使进程正确执行,段必须驻留在用于创建可执行文件的虚拟地址中。系统将p_vaddr值更改为虚拟地址。
您有一个PIE可执行文件(Position Independent Executable),因此该文件只包含相对于加载地址的偏移量,操作系统选择该偏移量(并且可以随机化)。
0x400000
是Linux默认基址,用于在禁用ASLR的情况下加载PIE可执行文件(默认情况下与GDB一样)。
如果你用-m32 -fno-pie -no-pie hello.c
编译一个正常位置依赖的动态链接可执行文件,它可以用mov eax, [symname]
从静态位置加载而不必在寄存器中获取EIP,并使用它来进行PC相对寻址而不需要x86-64 RIP相对寻址模式, objdump -f
会说:
./hello-32-nopie: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x08048380 # hard-coded load address, can't be ASLRed
代替
architecture: i386, flags 0x00000150: # some different flags set
HAS_SYMS, DYNAMIC, D_PAGED # different ELF type
start address 0x000003e0
在“常规”位置相关的可执行文件中,链接器默认选择该基址,并将其嵌入可执行文件中。操作系统的程序加载器无法选择ELF可执行文件,仅适用于ELF共享对象。非PIE可执行文件不能在任何其他地址加载,因此只有它们的库可以是ASLRed,而不是可执行文件本身。这就是PIE可执行文件被发明的原因。
允许非PIE嵌入绝对地址,而不会有任何允许操作系统尝试重定位的元数据。或者它允许包含手写的asm,它利用它想要的任何关于地址的数值。
PIE是具有入口点的ELF共享对象。在发明PIE之前,ELF共享对象通常仅用于共享库。有关PIE的更多信息,请参阅32-bit absolute addresses no longer allowed in x86-64 Linux?。
它们对于32位代码效率很低,我建议不要制作32位PIE。
静态可执行文件不能是PIE,因此gcc -static
将创建一个非PIE elf可执行文件;它意味着-no-pie
。 (因此将直接与ld
连接,因为默认情况下只有gcc改为制作PIE,gcc需要将-pie
传递给ld
才能这样做。)
因此,如果您所看到的唯一动态可执行文件是PIE,那么您很容易理解为什么在标题中编写“静态与动态”。但动态链接的非PIE ELF可执行文件是完全正常的,如果您关心性能但出于某种原因需要/需要制作32位可执行文件,那么您应该做什么。
直到最近几年左右,正常Linux发行版中的正常二进制文件如/bin/ls
都是非PIE动态可执行文件。对于x86-64代码,PIE只会使它们减慢1%,我想我已经读过了。用于将静态地址放入寄存器或索引静态数组的稍大代码。远不及32位代码对PIC / PIE的开销量。