./ hello是c中的一个简单回显程序。
根据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
此映射如何发生?
请帮助。
答案 0 :(得分:2)
基本上,链接之后,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
是用于加载PIE的Linux默认基址 禁用了ASLR的可执行文件(默认情况下就像GDB一样)。
因此,对于您的特定情况(Linux中为PIE可执行文件),加载程序会选择此base address
。
当然,位置无关只是一个选择。程序可以在没有它的情况下进行编译,并且随后将发生绝对寻址模式,在绝对寻址模式下,ELF中的段地址与实际的内存地址段之间必须没有区别。
可执行文件段通常包含绝对 码。为了使过程正确执行,段必须位于 在用于创建可执行文件的虚拟地址处。 系统使用未更改的p_vaddr值作为虚拟地址。
答案 1 :(得分:1)
您有一个PIE可执行文件(位置独立的可执行文件),因此该文件仅包含相对于加载地址的偏移量,该偏移量是操作系统选择的(并且可以随机化)。
0x400000
是Linux的默认基址,用于在禁用ASLR的情况下加载PIE可执行文件(默认情况下,就像GDB一样)。
如果您使用-m32 -fno-pie -no-pie hello.c
进行编译,以使它们依赖于正常位置,则可以动态链接可执行文件,该可执行文件可以通过mov eax, [symname]
从静态位置加载,而不必在寄存器中获取EIP,并且使用{x1}可以在没有x86-64 RIP相对寻址模式的情况下进行PC相对寻址,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
在“与位置相关的常规”可执行文件中,链接器默认情况下会选择该基地址,并将其嵌入可执行文件中。 OS的程序加载器不会选择ELF可执行文件,仅选择ELF共享对象。非PIE可执行文件无法在任何其他地址加载,因此只能对它们的库进行ASLR处理,而不是可执行文件本身。这就是发明PIE可执行文件的原因。
允许非PIE嵌入绝对地址,而没有任何会让操作系统尝试重新定位的元数据。或者允许它包含手写的asm,该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%。稍大一些的代码,用于将静态地址放入寄存器或为静态数组建立索引。 PIC / PIE的32位代码所消耗的开销几乎没有。