C语言编译与链接过程详解
源文件
main.c
#include <stdio.h>
extern int data;
extern int add(int a,int b);
int a1;
int a2 = 0;
int a3 = 10;
static int b1;
static int b2 = 0;
static int b3 = 20;
int main()
{
int c1;
int c2 = 0;
int c3 = 30;
static int d1;
static int d2 = 0;
static int d3 = 40;
c1 = data;
c2 = add(a1,a2);
while(1);
return 0;
}
add.c
int data = 3;
int add(int a,int b)
{
return a+b;
}
两大过程:编译、链接
一、编译过程:
-
预处理 (.i)
-
处理#开头的预处理指令:#include #define #ifndef #if #else 等等
-
去注释、加行号、生成文件索引等等
命令:gcc -E main.c -o main.i,生成 .i 文件
-
-
编译 (.s)
将 .i 文件编译生成 .s 汇编文件
命令:gcc -S main.i 生成 .s 文件
-
汇编 (.o)
将汇编文件翻译成二进程可重定位文件,即 .o 文件
命令:gcc -c main.s 生成 .o 文件
PS:gcc命令只是一些后台程序的包装,它会根据不同的参数调用其他程序:
-
预编译和编译合并成了一个步骤,使用的是程序cc1,也可以通过如下命令生成.s文件
cc1 hello.c
等同于 gcc -S hello.c -o hello.s
-
汇编器 as
-
链接器 ld
分析二进制可重定位文件
main.c文件
#include <stdio.h>
int a1;
int a2 = 0;
int a3 = 10;
static int b1;
static int b2 = 0;
static int b3 = 20;
int main(void)
{
int c1;
int c2 = 0;
int c3 = 30;
static int d1;
static int d2 = 0;
static int d3 = 40;
return 0;
}
编译命令:在64位的机器上编译32位的.o文件
*gcc -m32 -fno-PIC -c .c
-m32指定编译生成32位文件;-fno-PIC去除和位置无关的段(只留下.text .data .bss .comment 等)
1. 读取 elf 文件头
$ readelf -h main.o
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: REL (可重定位文件)
系统架构: ARM
版本: 0x1
入口点地址: 0x0
程序头起点: 0 (bytes into file)
Start of section headers: 268 (bytes into file)
标志: 0x5000000, Version5 EABI
本头的大小: 52 (字节)
程序头大小: 0 (字节)
Number of program headers: 0
节头大小: 40 (字节)
节头数量: 10
字符串表索引节头: 7
(1) 魔数
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
(2) REL (可重定位文件)
(3) 入口点地址: 0x0
(4) Start of section headers: 268 (bytes into file)
(5) 本头的大小: 52 (字节)
2. 获取 elf 文件的 section headers(段头) 信息 (供链接使用)
$ readelf -S main.o
There are 12 section headers, starting at offset 0x2ec:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000044 00 AX 0 0 1
[ 2] .rel.text REL 00000000 00026c 000020 08 I 9 1 4
[ 3] .data PROGBITS 00000000 000078 00000c 00 WA 0 0 4
[ 4] .bss NOBITS 00000000 000084 000014 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 000084 00002a 01 MS 0 0 1
[ 6] .note.GNU-stack PROGBITS 00000000 0000ae 000000 00 0 0 1
[ 7] .eh_frame PROGBITS 00000000 0000b0 00003c 00 A 0 0 4
[ 8] .rel.eh_frame REL 00000000 00028c 000008 08 I 9 7 4
[ 9] .symtab SYMTAB 00000000 0000ec 000140 10 10 14 4
[10] .strtab STRTAB 00000000 00022c 000040 00 0 0 1
[11] .shstrtab STRTAB 00000000 000294 000057 00 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),
p (processor specific)
有12个段头,起始段头偏移为 0x2ec
可以看到每个段的偏移与大小
3. 打印出段的内容
~ $ objdump -s main.o
main.o: 文件格式 elf32-i386
Contents of section .text:
0000 8d4c2404 83e4f0ff 71fc5589 e55183ec .L$.....q.U..Q..
0010 14c745ec 00000000 c745f01e 000000a1 ..E......E......
0020 00000000 8945f48b 15000000 00a10000 .....E..........
0030 000083ec 085250e8 fcffffff 83c41089 .....RP.........
0040 45ecebfe E...
Contents of section .data:
0000 0a000000 14000000 28000000 ........(...
Contents of section .comment:
0000 00474343 3a202855 62756e74 7520372e .GCC: (Ubuntu 7.
0010 352e302d 33756275 6e747531 7e31382e 5.0-3ubuntu1~18.
0020 30342920 372e352e 3000 04) 7.5.0.
Contents of section .eh_frame:
0000 14000000 00000000 017a5200 017c0801 .........zR..|..
0010 1b0c0404 88010000 20000000 1c000000 ........ .......
0020 00000000 44000000 00440c01 00471005 ....D....D...G..
0030 02750043 0f03757c 06000000 .u.C..u|....
4. 读取 .o 文件符号表
~ $ objdump -t main.o
main.o: 文件格式 elf32-little
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1881
0000000c l O .bss 00000004 d2.1880
00000010 l O .bss 00000004 d1.1879
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000044 main
00000000 *UND* 00000000 data
00000000 *UND* 00000000 add
标出了每个符号处于那个段,占多大内存,其中 a1 标记为 *COM* 表示它是弱符号(未初始化的非静态全局变量,可能其他文件里也定义了同名的)
data 和 add 这两个符号被标记为 *UND* ,表示未定义的符号,在本文件中找不到定义,链接时会从其他文件中寻找
5. 根据 section headers(段头) 信息,画出二进制可重定位文件的组成(.o文件)
可以发现bss段和comment段的起始卫视相同,但实际计算得出bss段在.o文件中并没有存储,但是符号表中对bss段有记录。
得出结论:bss段保存的都是未初始化 / 初始化为0的全局变量,和未初始化 / 初始化为0的静态局部变量,所以他们的默认值都为0 ,故为了节省.o文件的空间,无需存储,但是需要在符号表中记录,在最后执行可执行文件后,将bss段的符号存到虚拟地址空间中。
二、链接过程:
在64位x86机器上编译-链接生成32位目标文件和可执行文件的命令
编译:
gcc -m32 -fno-PIC -c *.c
手动链接:
ld -e main -melf_i386 *.o -o run
生成如下文件:
$ ls
add.c add.o main.c main.o run
PS:
-m32指定编译生成32位文件;
-fno-PIC去除和位置无关的段(只留下.text .data .bss .comment 等)
-e 指定程序入口,-e后跟着符号即可,也可以把add函数作为程序入口,即 -e add
-melf_i386指定链接生成32位的,x86架构的可执行文件
链接过程的本质主要是将多个目标文件“粘”在一起,实质上拼合的是目标文件之间对地址的引用,即函数名和全局变量
符号表就是.o文件的一个段,symtab,查看符号表命令
readelf -s main.o
objdump -t main.o
nm main.o
符号表中包含什么,主要关注1和2:
-
- 定义在本目标文件中的全局符号,例如变量名、函数名等
-
- 引用的其他目标文件中的符号,没有在本文件中定义,一般叫做外部符号
-
- 段名,如 “.text”, “.data” 等
-
- 局部符号,只在编译单元内部可见,调试器可以使用这些符号来分析程序或崩溃时的核心转储文件,链接过程中链接器往往忽略它们
$ objdump -t main.o
main.o: 文件格式 elf32-i386
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000004 l O .bss 00000004 b1
00000008 l O .bss 00000004 b2
00000004 l O .data 00000004 b3
00000008 l O .data 00000004 d3.1877
0000000c l O .bss 00000004 d2.1876
00000010 l O .bss 00000004 d1.1875
00000000 l d .note.GNU-stack 00000000 .note.GNU-stack
00000000 l d .eh_frame 00000000 .eh_frame
00000000 l d .comment 00000000 .comment
00000004 O *COM* 00000004 a1
00000000 g O .bss 00000004 a2
00000000 g O .data 00000004 a3
00000000 g F .text 00000016 main
1. 合并所有 .o 文件的段
如上图所示,text段合并,data段合并,bss段合并的同时,需要将弱符号转化为强符号(或者弱符号被强符号替换),bss段大小增加
并且发现链接后,生成的可执行文件的每个段都分配了内存地址(虚拟内存)
2. 合并符号表、符号解析、重定位
- 合并符号表
可以看出,可执行文件的符号表就是将多个.o文件的符号表简单的合并起来
- 符号解析
将弱符号(*COM*)转化为强符号
在其他文件中找到本文件中未定义的符号(*UND*)
- 重定位
为符号分配虚拟内存地址,符号的地址是根据段的地址加上自身的偏移计算的
可执行文件分析
1. 查看文件头
$ readelf -h run
ELF 头:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
类别: ELF32
数据: 2 补码,小端序 (little endian)
版本: 1 (current)
OS/ABI: UNIX - System V
ABI 版本: 0
类型: EXEC (可执行文件)
系统架构: Intel 80386
版本: 0x1
入口点地址: 0x80480a1
程序头起点: 52 (bytes into file)
Start of section headers: 4676 (bytes into file)
标志: 0x0
本头的大小: 52 (字节)
程序头大小: 32 (字节)
Number of program headers: 3
节头大小: 40 (字节)
节头数量: 9
字符串表索引节头: 8
入口点地址:0x80480a1。
2. 查看段信息
$ readelf -S run
There are 9 section headers, starting at offset 0x1244:
节头:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 08048094 000094 000051 00 AX 0 0 1
[ 2] .eh_frame PROGBITS 080480e8 0000e8 00005c 00 A 0 0 4
[ 3] .data PROGBITS 0804a000 001000 000010 00 WA 0 0 4
[ 4] .bss NOBITS 0804a010 001010 000018 00 WA 0 0 4
[ 5] .comment PROGBITS 00000000 001010 000029 01 MS 0 0 1
[ 6] .symtab SYMTAB 00000000 00103c 000170 10 7 14 4
[ 7] .strtab STRTAB 00000000 0011ac 000059 00 0 0 1
[ 8] .shstrtab STRTAB 00000000 001205 00003f 00 0 0 1
每个段都分配了虚拟地址。
3. 查看 program headers
$ readelf -l run
Elf 文件类型为 EXEC (可执行文件)
Entry point 0x80480a1
There are 3 program headers, starting at offset 52
程序头:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x000000 0x08048000 0x08048000 0x00144 0x00144 R E 0x1000
LOAD 0x001000 0x0804a000 0x0804a000 0x00010 0x00028 RW 0x1000
GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10
Section to Segment mapping:
段节...
00 .text .eh_frame
01 .data .bss
02
二进制可重定位文件只有 “section headers”,只有可执行文件里有 “program headers”,“program headers” 中显示了各个段的虚拟地址、对齐字节(一页4K)
按段的属性合并,只读(text+rodata)、可读可写(data+bss)等等
使用 readelf -l main 查看ELF的 “Segment” (供装载使用)
PS:因为我们是自己链接的,没有链接C库,所以段里的内容比较少
* 如果直接运行 gcc main.c -o main,则会默认链接C库,查看可执行文件的每个段时就有很多内容了
* 可执行文件是被 execve 加载到进程中的
* 可执行文件之所以可以运行,因为其指定了入口地址(main)、program headers(指定加载的虚拟地址)
* 描述 “Segment” 的结构叫 ”程序头” ,它描述了ELF文件该如何被操作系统映射到进程的虚拟空间。