编译链接,从源文件到可执行文件的过程

环境

编译器

这里使用的编译工具是Linux下的gcc/g++。
可以使用命令gcc -v查看gcc的版本。

编译过程

1.预处理
2.编译
3.汇编
4.链接

准备

3份.c源文件

test.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include<stdio.h>

// 想使用其他文件里面的函数需要先用extern声明一下。
extern int Add(int, int);
extern int Sub(int, int);

int main()
{
int a = 10;
int b = 20;

int c = Add(a, b); // 30
printf("%d\n", c);

int d = Sub(a, b); // -10
printf("%d\n", d);

return 0;
}

add.c

1
2
3
4
5
// 加法函数
int Add(int x, int y)
{
return x + y;
}

sub.c

1
2
3
4
5
// 减法函数
int Sub(int x, int y)
{
return x - y;
}

预处理

预处理阶段编译器做的事情:

  • 将源文件中包含的头文件展开。例如:#include<stdio.h>,其中#include被称为预处理指令。
  • #define 定义符号的替换。例如:#define Max 100,其中#define也被称为预处理指令。
  • 删除注释。

预处理命令:gcc -E test.c -o test.i
预处理产生的结果放在了test.i文件中

1
2
3
gcc -E test.c -o test.i
gcc -E add.c -o add.i
gcc -E sub.c -o sub.i


查看test.i文件的内容,观察行数发现比源文件多了800多行
头文件被展开,注释被删除,define定义的符号被替换

编译

把代码翻译成汇编语言
编译命令:gcc -S test.i -o test.s

1
2
3
gcc -S test.i -o test.s
gcc -S add.i -o add.s
gcc -S sub.i -o sub.s


查看test.s文件的内容,代码已经全部变成了汇编语言

汇编

把汇编指令翻译成二进制指令
命令:gcc -c test.s -o test.o

查看test.o文件的内容,里面全部变成了二进制指令

链接

生成可执行文件
命令:gcc test.o add.o sub.o -o test

执行程序

直接生成可执行文件

其实可以省略这些步骤,一步直接生成可执行文件

gcc test.c add.c sub.c -o example.so

深度剖析

符号汇总

在编译阶段会进行符号汇总,这里的符号是程序中的变量名、函数名,为后面的汇编和链接阶段做准备

形成符号表

在汇编阶段会形成符号表,如test.o和可执行程序的文件格式都是ELF文件格式
每个.o文件里面都有一个符号表,符号表里面有编译过程中记录的符号,并且还有与符号相关联的地址。
可以利用readelf工具来查看二进制文件

查看test.o文件的符号表

readelf -s test.o
可以看到有main,Add,printf,Sub这些符号

查看add.o文件的符号表

readelf -s add.o
可以看到有Add符号

查看sub.o文件的符号表

readelf -s sub.o
可以看到有Sub符号

链接

在链接阶段编译器会合并段表,合并符号表并重定向。
每个elf文件都是一段一段的,链接就是将每个.o文件的每一段都合并到最终的可执行程序的每一段里面。
将所有.o文件的符号表合并,形成可执行程序的符号表。
当合并完符号表后,可执行程序的符号表就拥有了所有符号有效地址,这样当其调用某个函数的时候就知道应该去那个文件里面找了。