ELF 二进制分析静态与动态。汇编代码如何|指令内存映射变化?

ELF binary analysis static vs dynamic. How does assembly code| instruction memory mapping changes?

./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

这种映射是如何发生的?

请帮忙。

基本上,在链接之后,ELF 文件格式为加载程序将程序加载到内存中并 运行 提供所有必要的信息。

每段代码和数据都放在一个段内的一个偏移量内,如数据段、文本段等,通过向段起始地址添加适当的偏移量来访问特定函数或全局变量。

现在,ELF文件格式也包含了程序头table:

An executable or shared object file's program header table is an array of structures, each describing a segment or other information that the system needs to prepare the program for execution. An object file segment contains one or more sections, as described in "Segment Contents".

然后 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

The virtual address at which the first byte of the segment resides in memory

p_offset

The offset from the beginning of the file at which the first byte of the segment resides.

p_type

The kind of segment this array element describes or how to interpret the array element's information. Type values and their meanings are specified in Table 7-35.

来自Table 7-35,注意PT_LOAD

Specifies a loadable segment, described by p_filesz and p_memsz. The bytes from the file are mapped to the beginning of the memory segment. If the segment's memory size (p_memsz) is larger than the file size (p_filesz), the extra bytes are defined to hold the value 0 and to follow the segment's initialized area. The file size can not be larger than the memory size. Loadable segment entries in the program header table appear in ascending order, sorted on the p_vaddr member.

因此,通过查看这些字段(以及更多字段),加载程序可以在 ELF 文件中找到段(可以包含多个部分),并将它们加载 (PT_LOAD) 到给定虚拟机的内存中地址。

现在,ELF 文件段的虚拟地址可以在运行时间(加载时间)更改吗?是的:

The virtual addresses in the program headers might not represent the actual virtual addresses of the program's memory image. See "Program Loading (Processor-Specific)".

因此,程序头包含 OS 加载程序将加载到内存中的段(可加载段,其中包含可加载部分),但加载程序放置它们的虚拟地址可能与 ELF 文件中的地址不同.

如何?

要理解它,让我们先阅读一下 Base Address

Executable and shared object files have a base address, which is the lowest virtual address associated with the memory image of the program's object file. One use of the base address is to relocate the memory image of the program during dynamic linking.

An executable or shared object file's base address is calculated during execution from three values: the memory load address, the maximum page size, and the lowest virtual address of a program's loadable segment. The virtual addresses in the program headers might not represent the actual virtual addresses of the program's memory image. See "Program Loading (Processor-Specific)".

所以实践如下:

position-independent code. This code enables a segment's virtual address change from one process to another, without invalidating execution behavior.

Though the system chooses virtual addresses for individual processes, it maintains the relative positions of the segments. Because position-independent code uses relative addressing between segments, the difference between virtual addresses in memory must match the difference between virtual addresses in the file.

因此,通过使用相对寻址,(PIE-位置独立执行table)实际位置可能与 ELF 文件中的地址不同。

来自PeterCordes的回答:

0x400000 is the Linux default base address for loading PIE executables with ASLR disabled (like GDB does by default).

因此对于您的特定情况(Linux 中的 PIE executable)加载程序选择此 base address.

当然,位置独立只是一种选择。没有它也可以编译程序,然后发生绝对寻址方式,其中ELF中的段地址与实际内存地址之间不能有差异段被加载到:

Executable file segments typically contain absolute code. For the process to execute correctly, the segments must reside at the virtual addresses used to create the executable file. The system uses the p_vaddr values unchanged as virtual addresses.

我建议你看一下 linux elf 图片加载的实现 here, and those two SO threads here and here

段​​落摘自 Oracle ELF 文档 (here and here)

你有一个 PIE 可执行文件(位置独立可执行文件),所以文件只包含一个相对于加载地址的偏移量,OS 选择(并且可以随机化)。

0x400000 是 Linux 默认基地址,用于在禁用 ASLR 的情况下加载 PIE 可执行文件(就像 GDB 默认情况下所做的那样)。

如果您使用 -m32 -fno-pie -no-pie hello.c 进行编译以创建一个正常位置 dependent 动态链接的可执行文件,可以使用 mov eax, [symname] 从静态位置加载而不必获取寄存器中的 EIP 并使用它在没有 x86-64 RIP-relative 寻址模式的情况下进行 PC-relative 寻址,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

在 "regular" position-dependent 可执行文件中,链接器 默认选择该基地址,并将其嵌入到可执行文件中. OS 的程序加载器无法选择 ELF 可执行文件,只能选择 ELF 共享的 object。 non-PIE 可执行文件不能加载到任何其他地址,因此只有它们的库可以被 ASLRed,而不是可执行文件本身。这就是发明 PIE 可执行文件的原因。

允许 non-PIE 嵌入绝对地址而无需任何元数据,这会让 OS 尝试重新定位它。或者它被允许包含 hand-written asm,它利用它想要的关于地址数值的任何东西。


PIE 是与 entry-point 共享 object 的 ELF。在 PIE 被发明之前,ELF shared objects 通常只用于共享库。有关 PIE 的更多信息,请参阅 32-bit absolute addresses no longer allowed in x86-64 Linux?

它们对于 32 位代码来说效率很低,我建议不要制作 32 位 PIE。


静态可执行文件不能是PIE,因此gcc -static将创建一个non-PIE elf可执行文件;它意味着 -no-pie。 (因此将直接与 ld 链接,因为只有 gcc 更改为默认制作 PIE,gcc 需要将 -pie 传递给 ld 才能做到这一点。)

所以很容易理解为什么你在标题中写了 "static vs. dynamic",如果你曾经看过的唯一动态可执行文件是 PIE。但是动态链接的 non-PIE ELF 可执行文件是完全正常的,如果您关心性能但出于某种原因想要/需要制作 32 位可执行文件,您应该做什么。

直到过去几年左右,普通 Linux 发行版中的 /bin/ls 等普通二进制文件是 non-PIE 动态可执行文件。 对于 x86 -64 代码,作为 PIE 只会减慢它们的速度,我想我已经读过 1%。用于将静态地址放入寄存器或索引静态数组的代码稍大一些。远不及 32 位代码对 PIC/PIE.

的开销量