本帖最后由 gclome 于 2020-4-2 17:28 编辑
原文链接:计算机原理系列之六 ——– 可执行文件详解
目录:
一、生成可执行文件
二、可执行文件结构
2.1 可执行文件的ELF header
2.2 可执行文件的section
2.3 可执行文件的segment
三、参考阅读
前面的文章我们详细的讲解了.o文件的结构及其编译过程,这篇文章我们从ELF文件的观点来分析可执行文件的结构。
一、生成可执行文件
我们以下面两个文件为例来研究多个文件生成一个可执行文件的过程。一个文件名为add.c,主要内容包含一个add函数,该函数功能为:将调用次数加1,然后,返回传递进来的两个参数之和。
- extern int times;
- int add(int n1, int n2) {
- times++;
- return n1 + n2;
- }
复制代码 另一个文件名为main.c,其功能为:调用两次add函数,并打印出调用次数和add函数的返回值。
- #include <stdio.h>
- extern int add(int, int);
- int times = 0;
- int main(void)
- {
- int a = 2;
- int b = 3;
- printf("%d, The sum of a and b is %d\n", times, add(a,b));
- printf("%d, The sum of a and b is %d\n", times, add(a,b));
- return 0;
- }
复制代码 由于这次我们的主要研究对象是可执行文件,因此,使用下面的命令编译这两个文件:
- gcc -c add.c -o add.o
- gcc -c main.c -o main.o
- gcc -o add add.o main.o
复制代码 这样我们生成了链接前的两个输入文件:add.o和main.o,以及链接之后的可执行文件add。其中.o的生成在可重定位文件详解和编译过程分析已经做过类似的分析,这里就不再做过多的解释。
二、可执行文件结构
2.1 可执行文件的ELF header
下面我们开看一下生成的可执行文件add的结构,由于可执行文件也属于ELF格式的文件,因此,按照惯例,我们依然先使用readelf工具从文件头开始分析。
- $ readelf -h add
- ELF Header:
- Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
- Class: ELF64
- Data: 2's complement, little endian
- Version: 1 (current)
- OS/ABI: UNIX - System V
- ABI Version: 0
- Type: EXEC (Executable file)
- Machine: Advanced Micro Devices X86-64
- Version: 0x1
- Entry point address: 0x400430
- Start of program headers: 64 (bytes into file)
- Start of section headers: 6712 (bytes into file)
- Flags: 0x0
- Size of this header: 64 (bytes)
- Size of program headers: 56 (bytes)
- Number of program headers: 9
- Size of section headers: 64 (bytes)
- Number of section headers: 31
- Section header string table index: 28
复制代码
我们分析一下字段: - Type: EXEC (Executable file): 该文件是一个可执行文件;
- Entry point address: 0x400430: 文件的入口地址为0x400430,该地址是代码段的起始地址;
- xxx of program headers: xxx: 该文件中共有9个program header,每个的大小为56字节;
- xxx of section headers: xxx: 该文件中共有31个section header,每个的大小为64字节。
这里可以看出,经过链接之后,生成了可执行文件,该文件的结构具有了如详解ELF文件中描述的完整的ELF文件结构。
2.2 可执行文件的section通过读ELF header的信息可以看出上述生成的可执行文件add共有31个section。下面我们逐个分析各个section的内容。首先,通过readelf命令查看各个section的起始地址和大小,如下: - $ readelf -S add
- There are 31 section headers, starting at offset 0x1a38:
- Section Headers:
- [Nr] Name Type Address Offset
- Size EntSize Flags Link Info Align
- [ 0] NULL 0000000000000000 00000000
- 0000000000000000 0000000000000000 0 0 0
- [ 1] .interp PROGBITS 0000000000400238 00000238
- 000000000000001c 0000000000000000 A 0 0 1
- [ 2] .note.ABI-tag NOTE 0000000000400254 00000254
- 0000000000000020 0000000000000000 A 0 0 4
- [ 3] .note.gnu.build-i NOTE 0000000000400274 00000274
- 0000000000000024 0000000000000000 A 0 0 4
- [ 4] .gnu.hash GNU_HASH 0000000000400298 00000298
- 000000000000001c 0000000000000000 A 5 0 8
- [ 5] .dynsym DYNSYM 00000000004002b8 000002b8
- 0000000000000060 0000000000000018 A 6 1 8
- [ 6] .dynstr STRTAB 0000000000400318 00000318
- 000000000000003f 0000000000000000 A 0 0 1
- [ 7] .gnu.version VERSYM 0000000000400358 00000358
- 0000000000000008 0000000000000002 A 5 0 2
- [ 8] .gnu.version_r VERNEED 0000000000400360 00000360
- 0000000000000020 0000000000000000 A 6 1 8
- [ 9] .rela.dyn RELA 0000000000400380 00000380
- 0000000000000018 0000000000000018 A 5 0 8
- [10] .rela.plt RELA 0000000000400398 00000398
- 0000000000000030 0000000000000018 AI 5 24 8
- [11] .init PROGBITS 00000000004003c8 000003c8
- 000000000000001a 0000000000000000 AX 0 0 4
- [12] .plt PROGBITS 00000000004003f0 000003f0
- 0000000000000030 0000000000000010 AX 0 0 16
- [13] .plt.got PROGBITS 0000000000400420 00000420
- 0000000000000008 0000000000000000 AX 0 0 8
- [14] .text PROGBITS 0000000000400430 00000430
- 0000000000000202 0000000000000000 AX 0 0 16
- [15] .fini PROGBITS 0000000000400634 00000634
- 0000000000000009 0000000000000000 AX 0 0 4
- [16] .rodata PROGBITS 0000000000400640 00000640
- 0000000000000022 0000000000000000 A 0 0 4
- [17] .eh_frame_hdr PROGBITS 0000000000400664 00000664
- 000000000000003c 0000000000000000 A 0 0 4
- [18] .eh_frame PROGBITS 00000000004006a0 000006a0
- 0000000000000114 0000000000000000 A 0 0 8
- [19] .init_array INIT_ARRAY 0000000000600e10 00000e10
- 0000000000000008 0000000000000000 WA 0 0 8
- [20] .fini_array FINI_ARRAY 0000000000600e18 00000e18
- 0000000000000008 0000000000000000 WA 0 0 8
- [21] .jcr PROGBITS 0000000000600e20 00000e20
- 0000000000000008 0000000000000000 WA 0 0 8
- [22] .dynamic DYNAMIC 0000000000600e28 00000e28
- 00000000000001d0 0000000000000010 WA 6 0 8
- [23] .got PROGBITS 0000000000600ff8 00000ff8
- 0000000000000008 0000000000000008 WA 0 0 8
- [24] .got.plt PROGBITS 0000000000601000 00001000
- 0000000000000028 0000000000000008 WA 0 0 8
- [25] .data PROGBITS 0000000000601028 00001028
- 0000000000000010 0000000000000000 WA 0 0 8
- [26] .bss NOBITS 0000000000601038 00001038
- 0000000000000008 0000000000000000 WA 0 0 4
- [27] .comment PROGBITS 0000000000000000 00001038
- 0000000000000035 0000000000000001 MS 0 0 1
- [28] .shstrtab STRTAB 0000000000000000 00001925
- 000000000000010c 0000000000000000 0 0 1
- [29] .symtab SYMTAB 0000000000000000 00001070
- 000000000690 0000000000000018 30 48 8
- [30] .strtab STRTAB 0000000000000000 00001700
- 0000000000000225 0000000000000000 0 0 1
- Key to Flags:
- W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
- I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
- O (extra OS processing required) o (OS specific), p (processor specific)
复制代码
- .plt和.plt.got:plt全名(Procedure Linkage Table)保存了一组16字节代码,用于在动态链接中生成所有外部过程调用符号。
- .got和.got.plt:got全名(Global Offset Table)保存了一个数组,用于配合PLT生成库函数的真正地址并保存该地址。
- .rela.plt:这个section保存了.plt中保存的符号的重定位信息。
- .rela.dyn:这个section保存了除了.plt中保存的符号外的其他符号的重定位信息。
- .jcr:这个section保存了编译java类所必须的信息。它里面内容是编译器指定的,并且被编译器的初始化函数使用。
.text:这个section用来保存可执行程序的代码编译生成的二进制代码。其内容我们通过objdump -C add命令查看后发现,反编译出来的函数不仅包括我们定义的add函数和main函数还包括了其他一些函数,这些函数是链接器加进去的。 .rodata,.rodata,.eh_frame,.data,.bss,.comment,.shstrtab,.symtab,.strtab和可重定位文件详解包含内容的意义是一样的,这里就不再重复介绍。
2.3 可执行文件的segment
前面我们谈到过,segment其实是由section组成的,那么哪些section组成了哪些segment呢?我们可以从readelf -l add输出的program header table中得到这些信息。 - $ readelf -l add
- Elf file type is EXEC (Executable file)
- Entry point 0x400430
- There are 9 program headers, starting at offset 64
- Program Headers:
- Type Offset VirtAddr PhysAddr
- FileSiz MemSiz Flags Align
- PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
- 0x00000000000001f8 0x00000000000001f8 R E 8
- INTERP 0x0000000000000238 0x0000000000400238 0x0000000000400238
- 0x000000000000001c 0x000000000000001c R 1
- [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
- LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
- 0x00000000000007b4 0x00000000000007b4 R E 200000
- LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
- 0x0000000000000228 0x0000000000000230 RW 200000
- DYNAMIC 0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
- 0x00000000000001d0 0x00000000000001d0 RW 8
- NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254
- 0x0000000000000044 0x0000000000000044 R 4
- GNU_EH_FRAME 0x0000000000000664 0x0000000000400664 0x0000000000400664
- 0x000000000000003c 0x000000000000003c R 4
- GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
- 0x0000000000000000 0x0000000000000000 RW 10
- GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
- 0x00000000000001f0 0x00000000000001f0 R 1
- Section to Segment mapping:
- Segment Sections...
- 00
- 01 .interp
- 02 .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame
- 03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
- 04 .dynamic
- 05 .note.ABI-tag .note.gnu.build-id
- 06 .eh_frame_hdr
- 07
- 08 .init_array .fini_array .jcr .dynamic .got
复制代码
根据上述信息: - 一共生成了9个segment,其中第0个segment对应PHDR,第1个segment对应INTERP,第2个segment对应LOAD,以此类推,第8个segment对应GNU_RELRO。
- 第0个segment(PHDR)保存了各个segment的VirtAddr和MemSiz。
- 第1个segment是只读的,只保存了.interp section。
- 第2个segment就是我们常说的一个可执行文件的代码段,这部分是要由加载器加载到内存中执行的,因此其属性是可读可执行的,保存了.interp .note.ABI-tag .note.gnu.build-id ... 等18个section。
- 第3个segment是我们常说的数据段,它保存了和可执行程序执行过程相关的所有数据,这部分也是要加载到内存中的,因此其属性是可读可写的,保存了.init_array .fini_array ...等8个section。
- 第4个segment是可读可写的,保存了.dynamic section。
- 第5个segment是只读的,保存了.note.ABI-tag .note.gnu.build-id这两个section。
- 第6个segment是只读的,保存了.eh_frame_hdr section。
- 第7个segment是可读可写的,它告诉系统,当加载这个可执行文件的时候,如何设置其栈。
- 第8个segment是只读的,保存了.init_array .fini_array .jcr .dynamic .got这5个section。
三、参考阅读
|