编译器的任务是把我们人类通常能够读懂的文本形式的 C 语言文件转化成计算机能明白的目标文件。
1. 预编译
生成的仍然是.c文件
1)把"include"的文件拷贝到要编译的源文件中。
2)用实际值替代"define"的文本。
3)在调用宏的地方进行宏替换。
2. 编译
这个过程是用于生成汇编语言,.asm文件。
3. 生成目标文件
.o文件,包含了以下两项内容
- 代码:对应着 C 文件中函数的定义
- 数据:对应着 C 文件中全局变量的定义(对于一个已初始化的全局变量,它的初值也存于目标文件中)。
编译器会留个空白(blank),这个“空白”(我们也称之为“引用”(reference))拥有与之相关联的一个名字,但该名字对应的值还尚未可知。
链接器的任务是连接变量/函数的名字与变量/函数的定义,生成可执行文件。
如果不能连接,就会抛出external error
初始化变量已经提前赋予了某个特定的初值,这些值同时保存于目标文件和可执行文件中。当程序开始运行时,操作系统将这些值拷贝至内存中一块名为数据段(data segment)的区域;对未初始化变量,操作系统假设其初值均为0, 因此没有必要对这些值进行拷贝,操作系统保留一部分全为0内存空间,我们称其为 bss 段(bss segment)。
局部变量的生命周期只存在于程序运行之时,所以链接器无需处理。他们存储在栈或堆中,由于堆和栈在程序运行过程中都会动态地改变大小,通常的处理方式是让栈从一个方向向另一个方向增长,而堆则从另一端增长。
库:如果有很多不同的程序都需要做一些相同的操作(例如将输出打印到屏幕上,从硬盘读取文件等),那么所有相关的目标文件集合都统一存放在一个方便访问的地方,这个地方就称为库
静态库(static library),在Windows平台上,静态库的扩展名为 .LIB,可用 .LIB 工具生成。
缺点1:循环依赖。
从静态库中导入文件的粒度:如果某个特定符号的定义是必须的,那么包含该符号定义的整个目标文件(不是整个静态库)被导入。
库的处理顺序。链接器按命令行从左到右的顺序进行处理,只有前一个库处理结束了,才会继续处理下一个库。换句话说,如果后一个库中导入的目标文件依赖于前一个库中的某个符号,那么链接器将无法进行自动关联,导致链接失败。
缺点2:一旦程序完成静态链接后,代码就永久保持不变了,如果万一有人发现并修复了 printf 中的某个bug,那么所有使用了printf的程序都不得不重新链接才能应用上这个修复。
缺点3:所有可执行文件都要存储一份如printf,fopen这类常用函数的拷贝,将占用大量的硬盘空间。
共享库(Shared libraies),其扩展名在 Unix 系统中为 .so,在 Windows 系统中为 .dll,在Mac OS X系统中为 .dylib。
优点1:链接的粒度(the granularity of the link)。
如果程序中只引用了共享库里的某个符号(比如,只使用了 libc.so 库中的 printf),那么整个共享库都将映射到程序地址空间中,之后可自行解决相互引用问题。并且,库内的符号集中保存在一片连续的空间里。
优点2:链接完后改变的代码,在运行时会生效。
优点3:对于共享库,链接器没有必要将所有的符号都关联起来,而是贴上一个“我欠你(IOU)”这样的标签,直到程序真正运行时才对贴有这样标签的内容进行处理。