本次环境为Linux gcc
Add.c:
int Add(int x, int y)
{
return x + y;
}
hello.c:
extern Add(int, int);
int main()
{
int ret = Add(1, 2);
printf("hello world\n");
return 0;
}
让我们先回到梦开始的地方——hello world,这行代码是怎么从hello.c到最后变成hello.exe的可执行文件的呢?
程序被其他程序翻译成不同的格式
hello程序的生命周期首先是从一个高级C语言程序开始的,因为这种程序可以被人们看懂。可是人能不能看懂这个代码与机器无关,可以说机器并不关心这段代码目前的这种形式,因为机器需要的是二进制的代码。
所以为了让机器可以顺利的理解这个程序,每条C语句都会发生转化,首先从目前我们看到的代码经历“预编译(预处理)”、“编译”变成汇编代码;
然后再经历“汇编”,这个阶段会把汇编指令变成二进制的指令,这时候机器已经可以看懂了,但是还不能保证能够正确执行;
最会经历“链接”,终于把一个.c文件变成了可执行的.exe文件。
预处理阶段:
1、头文件的包含 #include <>
2、定义符号的替换 #define xx xx
3、删除注释 //xxxxxxxxxx
预处理器根据以字符#开头的命令,修改原始的C程序。这些操作也叫做文本操作,完成此操作后,就由 hello.c 文件得到了 hello.i 文件。
编译阶段:
1、把C语言代码翻译成汇编代码。
在这里我们重点关注符号汇总,这一步操作会汇总全局符号,比如:main、或者其他自定义的函数名...
编译器在此处进行了语法分析、词法分析、语义分析、符号汇总。将文本文件 hello.i 翻译成文本文件 hello.s ,它包含一个汇编语言程序。
汇编语言程序中的每条语句都以一种标准的文本格式确切地描述了一条低级机器语言指令。汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。
汇编阶段:
1、把汇编指令翻译成了二进制的指令
2、形成符号表,通过readelf test.c -s指令打开符号表发现,记录了main、Add、printf
实际上会记录这些函数的地址,如果某个函数不在这个源文件内部,则会记录一个无效的地址
Add()是一个调用的自定义函数,存放在另外一个源文件中。
这一步完成后,hello.s 文件将被翻译成机器语言指令,生成 hello.o 文件
链接阶段:
1、合并段表
2、符号表的合并和重定位
注意 .o 文件的格式是elf的,而 .exe文件的格式也是elf的,如果有两个源文件,这里就会把这两个文件相同的段上的内容合并到一起,完成合并段表。
符号表的合并和重定位,这里注意上面有提到在汇编阶段形成符号表时,main函数里的Add对应的是一个无效地址,而另外一个文件生成的符号表里,存在一个Add的有效地址。
合并时就会用有效的地址覆盖无效地址。这也很好的解释了,为什么可以在一个源文件里可以调用其他源文件内的函数。当然,链接错误中的“无法解析的外部符号”也就是在这个阶段才会报错。