链接阶段:符号解析
链接阶段主要包含:
- 符号解析
- 重定位
一般情况下,每个 C 文件可以看成一个程序模块,比如下边的main.c
就是一个程序模块
#include <stdio.h>
extern int shared;
int sum(int *a, int n);
int array[2] = {1, 2};
static int m = 10;
static int swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main() {
static int b = 4;
int val = sum(array, 2);
swap (&val, &shared);
printf("%d, %d\n", val, shared);
return val;
}
每个程序模块一般会:
- 自己定义一些符号,称为符号定义
- 引用其他模块的符号,称为符号引用
符号定义:
-
全局符号:非静态 C 函数和全局变量,比如上面代码中的
array
、main
-
局部符号:
static
修饰的 C 函数和全局变量,相当于 Java/C++ 中的private
比如上面代码中的m
、swap
-
本地符号:函数内部定义的符号
1)非静态本地变量,比如上面代码中的val
2)静态本地变量,比如上面代码中的b
-
外部符号:引用其他模块定义的全局符号,比如上面代码中的
shared
、sum
以及printf
说明:除了非 static
局部符号外,其他符号都是在链接器管理范畴。程序在运行时,函数中本地变量,属于函数调用栈管理。每个程序模块,在编译后,都会有一个符号表,用于记录程序模块中的符号。
上面代码中涉及 3 个外部符号:shared
、sum
和 printf
,其中 printf
符号是在 printf.o
程序模块中, gcc
工具会帮你自动找到。shared
和 sum
符号需要在 sum.o
程序模块中找到。这就是符号解析需要做的工作:输入的其他的程序模块中的符号表中,为每个外部符号,寻找确定的符号定义,并且关联它们。
链接阶段:重定位(Relocation)
-
汇编后的
.o
文件中每个指令的位置,只是这条指令在文件中的偏移量,不是内存地址,所以此时.o
文件还不能直接加载到内存运行。 -
在链接阶段,合并所有
.o
文件中的指令,重新定位这些指令的位置,并为每个指令分配唯一内存地址。还需要重新定位外部符号的内存地址。这就是为什么.o
文件称为可重定位目标文件!!! -
经过链接之后变成可执行目标文件,这时才可以加载到内存运行。
静态链接库
静态链接:将多个可重定位目标文件,合并成一个可执行目标文件。
静态库很有用,实现了程序代码的复用。但是,因为静态库的链接属于静态链接,会出现浪费内存的现象。比如,程序 A 和程序 B 都静态链接了 libvector.a
静态库,那么当程序 A 和程序 B 同时在运行时,内存中会有两份 libvector.a
的代码指令。
将静态链接库编译成动态链接库,就可以解决多份.a
文件同时使用会重复浪费内存的问题。
ELF - 可重定位目标文件格式
一个典型的 ELF 目标文件的格式包含:ELF 头、若干个节 (section) 以及描述节的头部表 (节头部表),如下图:
我们边对照着下面的程序,然后边看看以上每个节都存储些啥。
#include <stdio.h>
extern int shared;
int sum(int *a, int n);
int array[2] = {1, 2};
static int m = 10;
int* arr_p = (int*)&array;
int c;
static int swap(int *a, int *b)
{
int tmp = *a;
*a = *b;
*b = tmp;
}
int main()
{
static int b = 4;
static int m;
int val = sum(array, 2);
swap(&val, &shared);
printf("%d, %d\n", val, shared);
return val;
}
.text
:用于已编译程序的机器代码,通过下面的命令可以看到.text
中存储的机器代码
objdump -Dj .text main2.o
// 查看二进制存储
objdump -sj .text main2.o
.rodata
:用于存储只读数据,比如printf
语句中的格式串等,通过下面的命令可以查看:
objdump -Dj .rodata main2.o
objdump -sj .rodata main2.o
.data
:存储已初始化的全局和静态 C 变量,比如array
、m
等。非静态的局部 C 变量在运行的时候被保存在栈中,不会出现在.data
中,
objdump -Dj .data main2.o
objdump -sj .data main2.o
.bss
:未初始化的全局和静态 C 变量,以及所有被初始化为0
的全局或静态变量。
objdump -Dj .bss main2.o
objdump -sj .bss main2.o
在上面的程序中,只有静态变量 m
在 .bss
中,未初始化的全局变量 c
,会出现在可执行目标文件中的 .bss
中,在目标文件中,这个 .bss
节不占据实际的空间,它仅仅是一个占位符。
区分初始化和未初始化变量的目的是为了节省空间,未初始化变量不需要占据任何实际的磁盘空间
在运行的时候,给这些变量分配内存,并初始化为 0
,所以所有被初始化为 0
的全局或静态变量也放在 .bss
中。
.symtab
:符号表,存放程序中定义和引用的函数和全局符号的信息
objdump -t main2.o
.rel.text
:也可以使用.rela.text
表示,存储.text
节中需要重定位的条目
objdump -rj .text main2.o
一般而言,以下两种情况需要重定位:
- 引用全局变量 (不管是内部还是外部)
- 调用外部函数
可以通过下面的指令来查看哪些指令需要生成重定位条目:
objdump -dx main2.o
.rel.data
:也可以使用rela.data
表示,存储.data
节中需要重定位的条目
objdump -rj .data main2.o
以上的重定位条目来源于全局变量 arr_p
。
一般而言,任何已初始化的全局变量,如果它的初始值是一个全局变量地址或者外部定义函数的地址,都需要重定位。
ELF 文件格式详解
对上面的程序编译成可重定位目标文件,我们可以使用 readelf
来查看 ELF 文件的格式
readelf -all main2.o
我们先看看 ELF Headers:
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1232 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 11
ELF Headers 是以一个 16
字节的数字开头,然后是生成该文件的字节顺序 (小端序)
还描述了文件的类型,这里是 REL
,表示的是可重定位目标文件,还有可执行目标文件,比如 prog
文件
还描述了机器类型,这里是 x86-64
,还有节头部表的的文件偏移 (也就是节头部表在文件中起始位置),还有节头部表的大小和节的数目
接下来看看节头部表 (section header table):
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000076 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000003f8
0000000000000090 0000000000000018 I 12 1 8
[ 3] .data PROGBITS 0000000000000000 000000b8
000000000000001c 0000000000000000 WA 0 0 8
[ 4] .rela.data RELA 0000000000000000 00000488
0000000000000018 0000000000000018 I 12 3 8
[ 5] .bss NOBITS 0000000000000000 000000d4
0000000000000004 0000000000000000 WA 0 0 4
[ 6] .rodata PROGBITS 0000000000000000 000000d4
0000000000000008 0000000000000000 A 0 0 1
[ 7] .comment PROGBITS 0000000000000000 000000dc
000000000000002e 0000000000000001 MS 0 0 1
[ 8] .note.GNU-stack PROGBITS 0000000000000000 0000010a
0000000000000000 0000000000000000 0 0 1
[ 9] .eh_frame PROGBITS 0000000000000000 00000110
0000000000000058 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000004a0
0000000000000030 0000000000000018 I 12 9 8
[11] .shstrtab STRTAB 0000000000000000 00000168
0000000000000066 0000000000000000 0 0 1
[12] .symtab SYMTAB 0000000000000000 000001d0
00000000000001e0 0000000000000018 13 13 8
[13] .strtab STRTAB 0000000000000000 000003b0
0000000000000043 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)
节头部表描述了每一个节所在的位置和大小。包含了下面的 10 个字段:
Name
表示节的名称Type
表示节的类型,NULL 表示无效的节,PROGBITS 表示包含了程序代码的节,RELA 表示包含重定位条目的节,STRTAB 表示包含了字符串表的节Address
表示节在内存中的其实地址,因为可重定位目标文件是不可执行的,所以还没有内存地址,可执行文件中会记录每个节的其实内存地址Offset
表示节的在文件中的偏移,也就是节在文件中存储的位置Size
表示节中存储数据的大小EntSize
表示节中每个条目的大小,前提是节中的条目大小是固定长度的,比如 symtab 符号表等Flags
表示节的一些属性,比如 .text 的 flags 为 AX,表示可重定位且可执行Link
表示当前节关联的其他节Info
表示当前节的一些额外信息Align
表示节中存储数据大小对应字节大小对齐,比如 4 字节对齐,8 字节对齐等
接下来看看 .symtab 符号表:
Symbol table '.symtab' contains 20 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main2.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 m
6: 0000000000000000 44 FUNC LOCAL DEFAULT 1 swap
7: 0000000000000000 0 SECTION LOCAL DEFAULT 6
8: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 m.2192
9: 0000000000000018 4 OBJECT LOCAL DEFAULT 3 b.2191
10: 0000000000000000 0 SECTION LOCAL DEFAULT 8
11: 0000000000000000 0 SECTION LOCAL DEFAULT 9
12: 0000000000000000 0 SECTION LOCAL DEFAULT 7
13: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
14: 0000000000000010 8 OBJECT GLOBAL DEFAULT 3 arr_p
15: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM c
16: 000000000000002c 74 FUNC GLOBAL DEFAULT 1 main
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
字段解释:
Value
表示符号在对应节中的偏移位置Size
表示符号表示的对象的大小Type
表示符号的类型,NOTYPE 表示没有指定类型,FILE 表示文件类型,SECTION 表示一个节类型,FUNC 表示一个函数,OBJECT 表示的是一个变量Bind
表示 LOCAL 和 GLOBALVis
Ndx
符号所属的节的 index 值,UND 表示未定义的符号,ABS 表示绝对值,不参与重定位,COM 表示属于 COMMON 节,其他数字表示对应的节的 indexName
表示符号的名字
sections 每节在文件中的具体位置
接下来,我们来看看每一节在可重定位目标文件中的具体位置
在文件的开头是 ELF Header:
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: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1232 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 14
Section header string table index: 11
Entry point address 等于 0,表示的就是 ELF Header 位于文件开头,
Size of this header 等于 64 字节,表示的是 ELF Header 的大小是 64 个字节,那么 ELF Header 是从 0 ~ 63
Start of section headers 等于 1232 字节,表示的是 section headers table 也就是节头部表位于文件中第 1232 字节,根据这个,我们可以得到节头部表的内容:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 0000000000000000 00000040
0000000000000076 0000000000000000 AX 0 0 1
[ 2] .rela.text RELA 0000000000000000 000003f8
0000000000000090 0000000000000018 I 12 1 8
[ 3] .data PROGBITS 0000000000000000 000000b8
000000000000001c 0000000000000000 WA 0 0 8
[ 4] .rela.data RELA 0000000000000000 00000488
0000000000000018 0000000000000018 I 12 3 8
[ 5] .bss NOBITS 0000000000000000 000000d4
0000000000000004 0000000000000000 WA 0 0 4
[ 6] .rodata PROGBITS 0000000000000000 000000d4
0000000000000008 0000000000000000 A 0 0 1
[ 7] .comment PROGBITS 0000000000000000 000000dc
000000000000002e 0000000000000001 MS 0 0 1
[ 8] .note.GNU-stack PROGBITS 0000000000000000 0000010a
0000000000000000 0000000000000000 0 0 1
[ 9] .eh_frame PROGBITS 0000000000000000 00000110
0000000000000058 0000000000000000 A 0 0 8
[10] .rela.eh_frame RELA 0000000000000000 000004a0
0000000000000030 0000000000000018 I 12 9 8
[11] .shstrtab STRTAB 0000000000000000 00000168
0000000000000066 0000000000000000 0 0 1
[12] .symtab SYMTAB 0000000000000000 000001d0
00000000000001e0 0000000000000018 13 13 8
[13] .strtab STRTAB 0000000000000000 000003b0
0000000000000043 0000000000000000 0 0 1
从上面可以看到 .text 节的 Offset 等于 0x00000040,也就说 .text 是从第 64 个字节开始,也就是位于 ELF Header 后面,.text 节的 Size 等于 0x0000000000000076,也就是 118 字节
.text 是从 64 ~ 181 字节段,对于 .text 需要 4 字节对齐,也就是 .text 实际大小为 120,那么 .text 就是从 64 ~ 183 字节段
接下来是 .data 节,起始位置为 184 字节,即 0xb8, .data 的大小是 0x1c
接下来是 .bss 和 .rodata 节,它们俩的起始位置都是:0xb8 + 0x1c = 0xd4,但是 .bss 是不占据磁盘空间的,只是作为标记而已
所以 0xd4 开始存储的实际上是 .rodata 节,大小为 0x8,接下来是 0xd4 + 0d8 = 0xdc,存储的是 .comment
按照上面的逻辑,可以将每一个部分定位在文件的具体位置上:
以上,我们可以通过偏移定位到每一个节的位置
在节的内部,也可以通过偏移定位节内的数据,比如符号表中:
Symbol table '.symtab' contains 20 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main2.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 5
5: 0000000000000008 4 OBJECT LOCAL DEFAULT 3 m
6: 0000000000000000 44 FUNC LOCAL DEFAULT 1 swap
7: 0000000000000000 0 SECTION LOCAL DEFAULT 6
8: 0000000000000000 4 OBJECT LOCAL DEFAULT 5 m.2192
9: 0000000000000018 4 OBJECT LOCAL DEFAULT 3 b.2191
10: 0000000000000000 0 SECTION LOCAL DEFAULT 8
11: 0000000000000000 0 SECTION LOCAL DEFAULT 9
12: 0000000000000000 0 SECTION LOCAL DEFAULT 7
13: 0000000000000000 8 OBJECT GLOBAL DEFAULT 3 array
14: 0000000000000010 8 OBJECT GLOBAL DEFAULT 3 arr_p
15: 0000000000000004 4 OBJECT GLOBAL DEFAULT COM c
16: 000000000000002c 74 FUNC GLOBAL DEFAULT 1 main
17: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND sum
18: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND shared
19: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
符号 b 的值位于第 3 节,也就是 .data 节,它在 .data 节的 0x18 这个位置上,也就是第 24 个字节,值为 0x04000000,注意二进制是小端序存储的,所以 0x04000000 实际上是等于 0x00000004
objdump -sj .data main2.o
总结
接下来,我们来总结下 ELF 中所有的表,如下图:
- 通过 ELF Header 中的信息,可以知道 ELF Header 的大小,以及节索引表的位置
- 根据节索引表,就可以找到所有的节
- 根据符号表,我们就可以知道所有的符号在每一个节中的偏移位置
- 符号表用于链接阶段中符号解析
- 链接阶段中的重定位需要用到 .rela.text 和 rela.data 等表
ELF - 可执行目标文件格式
ELF - 可执行目标文件也是由ELF头、节头部表以及若干个节组成。
-
.text
、.rodata
、.data
、.bss
这 些节已经被重定位到它们最终的运行时内存地址 -
init
节定义了一个小函数,叫做_init
,程序的初始化代码会调用它 -
因为文件是完全链接的(已被重定位),所以它不再需要
.rela
节了
为了使得 ELF 可执行目标文件更加容易加载到内存,根据指令和数据的特点,将可执行文件连续的节映射到连续的内存段中。
段头部表维护了这层映射关系:
总结
ELF 可执行目标文件格式跟 ELF 可重定位目标文件格式其实是差不多类似的,只不过多了一个 段头部表,另外每个部分的偏移地址被映射成了真实的内存地址。
也就是说,可重定位目标文件(.o
文件)中每条指令和数据只有在文件中的相对偏移位置,到了链接阶段,才会真正的为每条指令和数据分配内存地址,因此才能加载到内存中执行,所以此时叫可执行目标文件。
一个程序的内存布局
一个程序需要的内存: