上篇文章:Linux C 语言编译链接全解析:静态库与动态库从原理到实战
目录
[1.2查看ELF Header](#1.2查看ELF Header)
[1.3查看ELF Program Header Table](#1.3查看ELF Program Header Table)
[1.4 ELF Section Header Table](#1.4 ELF Section Header Table)
1.ELF文件
通过编译和链接,在当前文件下有两个源文件,两个扩展名为.o的文件,还有一个可执行程序:


其中,.o文件被称为目标文件 ,目标文件是一个二进制文件,它的文件格式是ELF,是对二进制代码的一种封装。
使用file命令用于辨识文件类型:

为了更好的理解编译链接的细节,我们需要了解一下ELF文件,以下四种文件都属于ELF文件:
- 可重定位文件(Relocatable File):即xxx.o文件。包含适用于与其他目标文件链接来创建可执行文件或者共享目标文件的代码和数据。
- 可执行文件(Executable File):即可执行程序。
- 共享目标文件(Shared Object File):即xxx.so文件。
- 内核转储(core dumps):存放当前进程的执行上下文,用于dump信号触发。
一个ELF文件由以下四部分组成:
- ELF头(ELF header):描述文件的主要特性。其位于文件的开始位置,它的主要目的是定位文件的其他部分。
- 程序头表(Program header table):列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在二进制文件中,需要段表的描述信息,才能把他们每个段分隔开。
- 节头表(Section header table):包含对节(sections)的描述。
- 节(Section):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。
最常见的节:
-
代码节(.text):用于保存机器指令,是程序的主要执行部分。
-
数据节(.data):保存已初始化的全局变量和局部静态变量。
$ size code
text data bss dec hex
filename
3312 636 4 3952 f70
code

上图中的ELF Header和Section Header Table属于ELF的管理字段,类似:Super block,GDT,各种位图......
Section 1,Section 2......Section n为ELF的核心数据内容,类似:data block!
1.1ELF结构

1.2查看ELF Header
-h 或 --file-header:显示ELF文件的文件头信息。文件头包含了ELF文件的基本信息,比如文件类型、机器类型、版本、入口点地址、程序头表和节头表的位置和大小等。

内核中关于ELF Header相关的数据结构,操作系统自己必须能够识别特定格式的可执行程序:/linux/include/elf.h
typedef struct elf32_hdr
{
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type;
Elf32_Half e_machine;
Elf32_Word e_version;
Elf32_Addr e_entry; /* Entry point */
Elf32_Off e_phoff;
Elf32_Off e_shoff;
Elf32_Word e_flags;
Elf32_Half e_ehsize;
Elf32_Half e_phentsize;
Elf32_Half e_phnum;
Elf32_Half e_shentsize;
Elf32_Half e_shnum;
Elf32_Half e_shstrndx;
} Elf32_Ehdr;
typedef struct elf64_hdr
{
unsigned char e_ident[EI_NIDENT]; /* ELF "magic number" */
Elf64_Half e_type;
Elf64_Half e_machine;
Elf64_Word e_version;
Elf64_Addr e_entry; /* Entry point virtual address */
Elf64_Off e_phoff; /* Program header table file offset */
Elf64_Off e_shoff; /* Section header table file offset */
Elf64_Word e_flags;
Elf64_Half e_ehsize;
Elf64_Half e_phentsize;
Elf64_Half e_phnum;
Elf64_Half e_shentsize;
Elf64_Half e_shnum;
Elf64_Half e_shstrndx;
} Elf64_Ehdr;
1.3查看ELF Program Header Table
查看section合并的segment
-l 或 --program-headers:显示ELF文件的程序头部(也称为段头)信息。可执行文件在内存中的布局和加载过程非常重要。
输入命令:
readelf -l a.out
显示段头信息:
Elf file type is DYN (Position-Independent Executable file)
Entry point 0x1060
There are 13 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000000040 0x0000000000000040
0x00000000000002d8 0x00000000000002d8 R 0x8
INTERP 0x0000000000000318 0x0000000000000318 0x0000000000000318
0x000000000000001c 0x000000000000001c R 0x1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000628 0x0000000000000628 R 0x1000
LOAD 0x0000000000001000 0x0000000000001000 0x0000000000001000
0x0000000000000199 0x0000000000000199 R E 0x1000
LOAD 0x0000000000002000 0x0000000000002000 0x0000000000002000
0x0000000000000124 0x0000000000000124 R 0x1000
LOAD 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000258 0x0000000000000260 RW 0x1000
DYNAMIC 0x0000000000002dc8 0x0000000000003dc8 0x0000000000003dc8
0x00000000000001f0 0x00000000000001f0 RW 0x8
NOTE 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
NOTE 0x0000000000000368 0x0000000000000368 0x0000000000000368
0x0000000000000044 0x0000000000000044 R 0x4
GNU_PROPERTY 0x0000000000000338 0x0000000000000338 0x0000000000000338
0x0000000000000030 0x0000000000000030 R 0x8
GNU_EH_FRAME 0x000000000000201c 0x000000000000201c 0x000000000000201c
0x000000000000003c 0x000000000000003c R 0x4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
GNU_RELRO 0x0000000000002db8 0x0000000000003db8 0x0000000000003db8
0x0000000000000248 0x0000000000000248 R 0x1
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.gnu.property .note.gnu.build-id .note.ABI-tag .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt
03 .init .plt .plt.got .plt.sec .text .fini
04 .rodata .eh_frame_hdr .eh_frame
05 .init_array .fini_array .dynamic .got .data .bss
06 .dynamic
07 .note.gnu.property
08 .note.gnu.build-id .note.ABI-tag
09 .note.gnu.property
10 .eh_frame_hdr
11
12 .init_array .fini_array .dynamic .got
关于上述,PhysAddr:现代OS,可以不考虑了;LOAD:不是所有的section都要被加载到内存,有的是与加载链接有关的字段,但是有LOAD的就是将来要加载的部分。
内核中关于ELF Program Header相关的数据结构:
typedef struct elf32_phdr
{
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;
typedef struct elf64_phdr
{
Elf64_Word p_type;
Elf64_Word p_flags;
Elf64_Off p_offset; /* Segment file offset */
Elf64_Addr p_vaddr; /* Segment virtual address */
Elf64_Addr p_paddr; /* Segment physical address */
Elf64_Xword p_filesz; /* Segment size in file */
Elf64_Xword p_memsz; /* Segment size in memory */
Elf64_Xword p_align; /* Segment alignment, file & memory */
} Elf64_Phdr;
1.4 ELF Section Header Table
查看可执行程序的section
-S 或 --section-headers:显示ELF文件的节头信息。节头描述了ELF文件的各个节的起始地址、大小、标志等信息
命令:
readelf -S a.out
显示结果:
There are 31 section headers, starting at offset 0x36d0:
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 0000000000000318 00000318
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.gnu.pr[...] NOTE 0000000000000338 00000338
0000000000000030 0000000000000000 A 0 0 8
[ 3] .note.gnu.bu[...] NOTE 0000000000000368 00000368
0000000000000024 0000000000000000 A 0 0 4
[ 4] .note.ABI-tag NOTE 000000000000038c 0000038c
0000000000000020 0000000000000000 A 0 0 4
[ 5] .gnu.hash GNU_HASH 00000000000003b0 000003b0
0000000000000024 0000000000000000 A 6 0 8
[ 6] .dynsym DYNSYM 00000000000003d8 000003d8
00000000000000a8 0000000000000018 A 7 1 8
[ 7] .dynstr STRTAB 0000000000000480 00000480
000000000000008d 0000000000000000 A 0 0 1
[ 8] .gnu.version VERSYM 000000000000050e 0000050e
000000000000000e 0000000000000002 A 6 0 2
[ 9] .gnu.version_r VERNEED 0000000000000520 00000520
0000000000000030 0000000000000000 A 7 1 8
[10] .rela.dyn RELA 0000000000000550 00000550
00000000000000c0 0000000000000018 A 6 0 8
[11] .rela.plt RELA 0000000000000610 00000610
0000000000000018 0000000000000018 AI 6 24 8
[12] .init PROGBITS 0000000000001000 00001000
000000000000001b 0000000000000000 AX 0 0 4
[13] .plt PROGBITS 0000000000001020 00001020
0000000000000020 0000000000000010 AX 0 0 16
[14] .plt.got PROGBITS 0000000000001040 00001040
0000000000000010 0000000000000010 AX 0 0 16
[15] .plt.sec PROGBITS 0000000000001050 00001050
0000000000000010 0000000000000010 AX 0 0 16
[16] .text PROGBITS 0000000000001060 00001060
000000000000012b 0000000000000000 AX 0 0 16
[17] .fini PROGBITS 000000000000118c 0000118c
000000000000000d 0000000000000000 AX 0 0 4
[18] .rodata PROGBITS 0000000000002000 00002000
000000000000001c 0000000000000000 A 0 0 4
[19] .eh_frame_hdr PROGBITS 000000000000201c 0000201c
000000000000003c 0000000000000000 A 0 0 4
[20] .eh_frame PROGBITS 0000000000002058 00002058
00000000000000cc 0000000000000000 A 0 0 8
[21] .init_array INIT_ARRAY 0000000000003db8 00002db8
0000000000000008 0000000000000008 WA 0 0 8
[22] .fini_array FINI_ARRAY 0000000000003dc0 00002dc0
0000000000000008 0000000000000008 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000003dc8 00002dc8
00000000000001f0 0000000000000010 WA 7 0 8
[24] .got PROGBITS 0000000000003fb8 00002fb8
0000000000000048 0000000000000008 WA 0 0 8
[25] .data PROGBITS 0000000000004000 00003000
0000000000000010 0000000000000000 WA 0 0 8
[26] .bss NOBITS 0000000000004010 00003010
0000000000000008 0000000000000000 WA 0 0 1
[27] .comment PROGBITS 0000000000000000 00003010
000000000000002d 0000000000000001 MS 0 0 1
[28] .symtab SYMTAB 0000000000000000 00003040
0000000000000390 0000000000000018 29 19 8
[29] .strtab STRTAB 0000000000000000 000033d0
00000000000001e6 0000000000000000 0 0 1
[30] .shstrtab STRTAB 0000000000000000 000035b6
000000000000011a 0000000000000000 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), l (large), p (processor specific)
.got 通常与全局偏移表(Global Offset Table,简称 GOT)。
内核中关于ELF Section Header相关的数据结构
typedef struct
{
Elf32_Word sh_name;
Elf32_Word sh_type;
Elf32_Word sh_flags;
Elf32_Addr sh_addr;
Elf32_Off sh_offset;
Elf32_Word sh_size;
Elf32_Word sh_link;
Elf32_Word sh_info;
Elf32_Word sh_addralign;
Elf32_Word sh_entsize;
} Elf32_Shdr;
typedef struct elf64_shdr
{
Elf64_Word sh_name; /* Section name, index in string tbl */
Elf64_Word sh_type; /* Type of section */
Elf64_Xword sh_flags; /* Miscellaneous section attributes */
Elf64_Addr sh_addr; /* Section virtual addr at execution */
Elf64_Off sh_offset; /* Section file offset */
Elf64_Xword sh_size; /* Size of section in bytes */
Elf64_Word sh_link; /* Index of another section */
Elf64_Word sh_info; /* Additional section information */
Elf64_Xword sh_addralign; /* Section alignment */
Elf64_Xword sh_entsize; /* Entry size if section holds table */
} Elf64_Shdr;
1.5查看具体的sections信息
命令:
objdump -S a.out
大致信息:


查看编译后的.o目标文件

2.ELF从形成到加载轮廓
2.1ELF形成可执行
第一步:将多份C/C++源代码,翻译成为目标.o文件 + 动静态库(ELF)
第二步:将多份.o文件section进行合并(库的本质是.o,都是ELF)

注意:实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并。
2.2ELF可执行文件的加载
一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。
这样,即使是不同的Section,在加载内存中,可能会以segment的形式,加载到一起
显然,这个合并工作也已经在形成ELF的时候,合并方式已经确定了,具体合并原则被记录在了ELF的程序头表(Program header table)中。
# 查看可执⾏程序的section
$ readelf -S a.out
# 查看section合并的segment
$ readelf -l a.out
思考
为什么要将section合并为segment?
- Section合并的主要原因是为了减少页面碎片,提高内存使用效率。如果不进行合并,假设页面大小为4096字节(内存块基本大小,加载,管理的基本单位),如果.text部分为4097字节,.init部分为512字节,那么它们将占用3个页面,而合并后,它们只需2个页面。
- 操作系统在加载程序时,会将具有相同属性的section合并为一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
对于程序头表(Program header table)和节头表(Section header table)又有什么用呢?
ELF文件提供两个不同的视图/视角来让我们理解这两个部分:
- 链接视图(Linking view) - 对应节头表(Section header table)
- 文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解ELF文件中包含的各个部分的信息
- 为了空间布局上的效率,将来在链接目标文件时,链接器会把很多节(section)合并,规整为可执行的段(segment),可读写的段、只读段等。合并后,空间利用率就高了,否则,很小很小的一段,未来物理内存页浪费太大(物理内存页分配一般都是整数倍一起给你,比如4K),所以,链接器趁着链接就把小块们都合并了。
- 执行视图(execution view) - 对应程序头表(Program header table)
- 告诉操作系统,如何加载可执行文件,完成进程内存的初始化。一个可执行程序的格式中,一定有program header table
总结:一个在链接时作用,一个在运行加载时作用。

从链接视图看:
- 命令readelf -S hello.o 可以帮助查看ELF文件的节头表。
- .text节:是保存了程序代码指令的代码节。
- .data节:保存了初始化的全局变量和局部静态变量等数据。
- .rodata节:保存了只读的数据,如一行c语言代码中的字符串。由于.rodata节是只读的,所以只能存在于一个可执行文件的只读段中。因此,只能是在text段(不是data段)中找到.rodata节。
- .BSS节:为未初始化的全局变量和局部变量预留位置
- .symtab节:Symbol Table符号表,这就是源码里面那些函数名、变量名和代码的对应关系。
- .got.plt节(全局偏移表-过程链接表):.got节保存了全局偏移表。.got节和.plt节一起提供了对导入的共享库函数的访问入口,由动态链接器在运行时进行修改。
使用readelf命令查看.so文件,可以看到该节。
从执行视图看:
- 告诉操作系统哪些模块可以被加载进内存。
- 加载进内存之后,哪些分段是可读可写,哪些分段是只读,哪些分段是可执行。
我们可以在ELF头中找到文件的基本信息,以及可以看到ELF头是如何定位程序头表和节头表的。例如我们查看hello.o这个可重定位文件的主要信息:
// 查看⽬标⽂件
$ readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF⽂件标识符(魔数)
Class: ELF64 # ⽂件类:64位架构
Data: 2's complement, little endian # 数据编码:⼩端序⼆进制补码
Version: 1 (current) # ELF版本:当前版本
(1)
OS/ABI: UNIX - System V # 操作系统ABI:System V UNIX
ABI Version: 0 # ABI扩展版本:未扩展(0)
Type: REL (Relocatable file) # ⽂件类型:可重定位⽂件(⽬标⽂件)
Machine: Advanced Micro Devices X86-64 # ⽬标平台:x86-64架构
Version: 0x1 # 对象⽂件版本:1
Entry point address: 0x0 # ⼊⼝点地址:⽆(⽬标⽂件为0)
Start of program headers: 0 (bytes into file) # 程序头表起始偏移:⽆(⽬标⽂件为0)
Start of section headers: 728 (bytes into file) # 节头表起始偏移:728字节处
Flags: 0x0 # 处理器特定标志:
⽆标志(0)
Size of this header: 64 (bytes) # ELF头⼤⼩:64字
节
Size of program headers: 0 (bytes) # 程序头表条⽬⼤
⼩:⽆(⽬标⽂件为0)
Number of program headers: 0 # 程序头表条⽬数:
⽆(⽬标⽂件为0)
Size of section headers: 64 (bytes) # 节头表条⽬⼤⼩:
64字节
Number of section headers: 13 # 节头表条⽬数:13
个节
Section header string table index: 12 # 节名称字符串表索
引:第12节(.shstrtab)
// 查看可执⾏程序
$ gcc *.o
$ readelf -h a.out
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64 # 64位架构
Data: 2's complement, little endian # ⼩端序⼆进制补码
Version: 1 (current) # ELF版本:1(当前标准)
OS/ABI: UNIX - System V # 操作系统ABI:System V UNIX
ABI Version: 0 # ⽆扩展ABI
Type: DYN (Shared object file) # ⽂件类型:动态共享库(.so)
Machine: Advanced Micro Devices X86-64 # ⽬标平台:x86-64
Version: 0x1 # 对象⽂件版本:1
Entry point: 0x1060 # 程序⼊⼝点虚拟地址(动态链接后解析)
Start of program headers: 64 (bytes into file) # 程序头表起始偏移:64字节
Start of section headers: 14768 (bytes into file) # 节头表起始偏移:14768字节
Flags: 0x0 # ⽆处理器特定标志
Size of this header: 64 (bytes) # ELF头⼤⼩:64字节
Size of program headers: 56 (bytes) # 程序头表每个条⽬⼤⼩:56字节
Number of program headers: 13 # 程序头表条⽬数:13(如LOAD、DYNAMIC等段)
Size of section headers: 64 (bytes) # 节头表每个条⽬⼤⼩:64字节
Number of section headers: 31 # 节头表条⽬数:31个节(如.text、.data等)
Section header string table index: 30 # 节名称字符串表索引:第30节(.shstrtab)
本章完。