本文将介绍linux系统中C代码的编译链接原理,如果你对.c源文件是怎么变成一个可执行程序并最终执行的过程很感兴趣但是还不是很了解,那么本文就为你系统的讲解这个过程。
将一个.c文件变成可执行文件的过程是很复杂的,为了使问题简单,前人将整个过程细分为五大步骤,分别为:预编译,编译,汇编,链接,运行。这五步层层递进,最终结果就是将一个.c文件变成一个运行在内存中的进程。
预编译阶段:
命令:
gcc -E main.c -o main.i (将main.c文件预编译后生成main.i文件)
该阶段做的事情:
1. 删除 #define 并做文本替换
2. 删除 #if #endif #elif 等预编译指令
3. 递归展开 #include 的头文件;以#include<stdio.h>为例,递归展开的原因是因为stdio.h中可能还引用了其他的.h文件
4. 删除注释
5. 添加行号和文件标识
6. 保留 #pragma 给编译器;一个例子,在计算结构体大小的时候, #pragma pack(4) 就是告诉编译器以4字节对齐
图片示意:
编译阶段:
命令:
gcc -S mian.i -o main.s
该阶段做的事情:
1. 词法分析 :例如一个标识符可以由字母数字下划线组成,但不能由数字开头,若在命名时违反就属于词法分析的错误
2. 语法分析 :例如圆括号个数不匹配
3. 语义分析 ;词法分析,语法分析都是针对局部的,但语义分析是需要联系程序上下文的
4. 代码优化 ;代码优化的一个实例:https://mp.csdn.net/postedit/96191270
5. 生成汇编指令:该阶段生成的.s文件就是一些汇编指令
汇编阶段:
命令:
gcc -c main.s -o main.o
该阶段做的事情:
将汇编指令翻译成机器指令
链接阶段:
命令:
ld a.o b.o -e main -o run (-e之后是用来指定程序的入口)
该阶段做的事情:
1. 合并 段和符号表 : 前面的三个阶段都是对独立的文件进行处理,而要生成可执行文件,就需要将全部的信息合并
2. 符号解析 : 有一些外部符号之前看不到,在链接阶段,段和符号表合并,我们就能确认这些外部符号
3. 分配地址和空间 :分配虚拟地址空间地址
4. 符号重定位 :原来一些符号的地址是虚假的地址,当有了真实的虚拟地址之后,就要将这些符号地址重定位
单个文件的.o文件信息查看:
先给出源代码(生成.o文件的步骤读者自己完成):
首先给出 ELF 文件的一般构成:
用命令 objdump -h main.o 可以查看各个段的基本信息:
我们通过查看目标文件的段布局可以看到目标文件中有.bss这个段,而且bss段还有大小,其为20个字节,针对我们写的代码,也就是说存放了5个整型值,但是bss段的属性为 ALLOC 即证明这个段真实是不存在。其中.data段和.bss段存放的都是数据,当我们把.data段的大小和.bss段的大小相加为32个字节,即8个int的大小,而局部的普通变量生成的是数据,也就是说在我们定义的12个变量中,有11(8+3)个我们可以确认是分派到了.text .data .bss段,还有一个去了哪里?
我们可以通过命令 objdump -s main.o 查看符号表中各个符号所处的区域:
我们发现gdata是放在了*COM*段,其实放在这是和c语言的强弱符号有关,大家都知道一个项目可能有多个源文件。编译阶段都是每个文件单独编译的。可能在其他文件中存在强符号,所以没办法在编译期间确定具体的符号。因此将本文件的弱符号存放在*COM*块,而不是.bss段。
对于引用的外部符号大家可以看到符号表中是*UND*标志。也就是说这个符号没有确定具体定义的地方。
强符号:全局的已初始化的符号
弱符号:全局的未初始化的符号
强弱符号的选取规则:
两个强符号 编译报错
一个强符号 一个弱符号 选择强符号
两个弱符号 根据不同编译器处理方式不同
接下来我们用命令 readelf -h main.o 来查看整个ELF文件的头部信息:
在ELF Header中有一个叫做section headers的段,这个段保存了目标文件中每个段的详细信息,包括段的大小,起始偏移等等信息。编译器只要访问这个段就可以知道每个段的详细信息。.bss虽然不占空间但是.bss段的详细信息都被保存在section headers中了。
而且Entry point address的值是0,也就是说程序的入口地址为0,知道4G虚拟内存空间的都知道这个是不可能的,因此在由各个单个文件生成的.o文件中,各个符号的地址都是虚拟的,只有当多个文件进行链接,然后才会有正确的地址。
链接之后的ELF文件信息查看:
链接阶段链接器只关注符号表中的全局符号!!
链接完成后我们再次输命令查看其文件的一些信息:
可以看到链接后,对每个符号给出了具体的虚拟地址。gdata3存放在.bss段了,因为链接完了以后就可以确定gdata3这个弱符号的符号选择了。对于main.o文件中引用的外部符号也确定了具体的定义位置,把这两个外部符号放到对应的段中。
符号解析:
在每个文件符号引用(引用外部符号)的地方找到符号的定义。这就是符号解析
符号重定位:
未链接之前程序的入口地址是0,而链接之后程序的入口地址都变成了有效地址,这种现象就是符号重定位。
运行阶段:
步骤:
1. 建立虚拟地址空间和物理内存的映射(也就是创建映射结构体PCB)
2. 通过load寄存器 加载指令和数据到内存上
3. 将程序的入口地址写入下一行指令寄存器(写入的虚拟空间的地址)
经历这5步之后,程序就由一个.c源文件变成了一个在内存中执行的进程!!