一个c程序运行的流程:
大致流程:
进一步
每个.c文件都会单独经过编译器处理生成自己的目标文件。
windows下,目标文件后缀为.obj;
linux下,目标文件后缀为.o.
然后【所有目标文件】和链接库通过链接器处理,生成可执行程序
再进一步
编译也具体拆分成3个阶段 —— 预编译、编译、汇编。
一 预编译
1 执行各种预处理指令。如#include的头文件包含,对#define定义的符号进行替换等
2 注释的删除
3 生成对应的.i文件
【.i文件就是,对源文件预处理后,生成的文件】
例如:text.c -> text.i
二 编译
1 把c语言代码翻译成汇编代码
2 生成对应的.s文件
例:text.i -> text.s
此时text.s里面的代码是汇编代码
编译过程具体包括:
语法分析
词法分析
语义分析
符号汇总
其中(符号汇总)是编译期间将.i文件中出现的全局符号梳理出来,例如函数名.
例:
//以下是不同文件的代码
//text.c
#include <stdio.h>
extern void print1(void);
int main()
{
print1();
printf("hehe\n");
return 0;
}
//print1.c
void print1(void)
{
printf("haha\n");
}
此时在text.i会汇总main 和 print1;
在print1.i会汇总print1
三 汇编
1 将汇编指令翻译成二进制指令
2 生成对应的目标文件,后缀.obj(windows下)
例:test.s -> test.obj
3 形成符号表,给编译期间汇总的符号赋一个有序或无效的地址。
例:
| text.c | 地址 | 原因 |
|---|---|---|
| print1 | 无效地址 | 因为print在另一个文件定义 |
| main | 0x5f933179 | 该函数就在text.c中 |
| printf | 无效地址 | 因为该函数有部分在链接库中,在<stdio.h>并不完整 |
| print1.c | 地址 |
|---|---|
| print1 | 0x6a893496 |
四 链接
1 合并段表
通俗讲,每个目标文件都按照某种相同的格式排版
当把【所有目标文件】和链接库链接生成可执行程序
该程序的格式也和目标文件一样。
2 符号表的合并和重定位
将每个目标文件的符号表和链接库进行合并,此时就能覆盖print1和printf的无效地址
| 函数 | 地址 |
|---|---|
| main | 0x5f933179 |
| printf | 0x5c578946 |
| print1 | 0x6a893496 |
这解决了外部符号如何跨文件使用的问题。