从源代码生成可执行文件可以分为四个步骤:分别是预处理、编译、汇编和链接。
处理源文件和头文件中以#开头的命令,比如 #include、#define、#ifdef 等。预处理的规则一般如下:
1、将所有的#define删除,并展开所有的宏定义。
2、处理所有条件编译命令,比如 #if、#ifdef、#elif、#else、#endif 等。
3、处理#include命令,将被包含文件的内容插入到该命令所在的位置,递归进行。
4、删除所有的注释。
5、添加行号和文件名标识。
6、保留所有的#pragma命令,因为编译器需要使用它们。
预处理的结果是生成.i文件,gcc下使用下面命令生成i文件:
gcc -E demo.c -o demo.i // -E表示只进行预编译二、编译
编译就是把预处理完的文件进行一些列的词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件,由.i文件生成.s文件。最难的一部分,有专门的书籍深入研究,不细说。
三、汇编汇编代码转换成可以执行的机器指令。大部分汇编语句对应一条机器指令,有的汇编语句对应多条机器指令,一般是根据汇编语句和机器指令的对照表一一翻译。
四、链接A.链接的作用:经汇编过后的.o目标文件已经是二进制文件,只是有些函数和全局变量的地址还未找到,程序不能执行。链接的作用就是找到这些目标地址,将所有的目标文件组织成一个可以执行的二进制文件。
B.举例:假设一个程序有两个模块 main.c 和 module.c,在 module.c 中定义了函数 func(),并在 main.c 中进行了调用,但由于每个模块都是单独编译的,编译器在处理 main.c 时并不知道 func() 的地址,所以需要把这些调用 func() 的指令的目标地址搁置,等到最后链接的时候再由链接器将这些地址修正。
C.符号与符号决议(重定位):符号表示一个函数或变量的地址,符号决议表示计算符号地址的过程。
D.符号决议计算地址的过程:要进行链接时,链接器首先扫描所有的.o目标文件,获得各个段的长度、属性、位置等信息,并将目标文件中的所有(符号表中的)符号收集起来,统一放到一个全局符号表。在这一步中,链接器会将目标文件中的各个段合并到可执行文件,并计算出合并后的各个段的长度、位置、虚拟地址等。在目标文件的符号表中,保存了各个符号在段内的偏移,生成可执行文件后,原来各个段起始位置的虚拟地址就确定了下来,这样,使用起始地址加上偏移量就能够得到符号的地址(在进程中的虚拟地址)。
E.强符号和弱符号:在多个源文件中定义了名字相同的全局变量会造成符号重复定义,而在这些重复定义的符号中,函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。链接器对于强弱符号的规则如下:
1、 不同的目标文件中不能有同名的强符号。
2、 一个符号在某个目标文件中是强符号,在其他文件中是弱符号,那么选择强符号。
3、 一个符号在所有的目标文件中都是弱符号,那么选择其中占用空间最大的一个。
在 GCC 中,可以通过__attribute__((weak))来强制定义任何一个符号为弱符号:
int weak1; //弱符号 int strong = 100; //强符号 __attribute__((weak)) weak2 = 2; //弱符号
注:对于同一个变量或函数,强弱符号只能在不同的目标文件中定义,在同一目标文件中同时存在强弱符号会报错。
F.强引用和弱引用:对强符号的引用称为强引用,对弱符号的引用称为弱引用。强引用要求符号必须被定义,否则链接器报错,弱引用符号未定义时,链接器不报错,例如:
#include__attribute__((weak)) extern int a; __attribute__((weak)) extern void func(); int main(){ printf("&a: %d, func: %dn", &a, func); //地址为0 printf("a = %dn", a); //弱引用,变量未定义链接器不报错,但0地址不可访问,运行报段错误 func(); //弱引用,函数未定义链接器不报错,但0地址不可访问,运行报段错误 return 0; }
弱引用和强引用非常利于程序的模块化开发,可以将程序的扩展模块定义为弱引用,当我们将扩展模块和程序链接在一起时,程序就可以正常使用;如果我们去掉了某些模块,那么程序也可以正常链接,只是缺少了某些功能。
五、extern和static关键字A.extern关键字
extern可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他模块中寻找其定义。例如:
main.c中:
extern void func(); //函数和全局变量在其他文件中定义,提示在其他文件中寻找 extern int m;
注:exterb仅用于声明,所以不推荐这种写法:
extern datatype name = value; //相当于datatype name = value,extern没起作用
B.static关键字
static修饰全局变量或函数时,会修改其作用域,static修饰的全局变量或函数仅对本文件有效。
static修饰局部变量,会改变变量的存储区域,被 static 修饰的局部变量会存储在全局数据区(与全局变量存储的区域一样),而未被static修饰的局部变量应该是在栈空间里的。所以不会因为函数调用结束而销毁,同时注意全局数据区的变量只能被初始化(定义)一次。
下面这个例子:
#includeint func(){ static int n = 0; //static修饰局部变量 n++; printf("Function is called %d times.n", n); return n; } int main(){ int i, n = 0; for(i = 1; i<=5; i++){ func(); } printf("n = %dn", n); return 0; }
运行结果:
Function is called 1 times.
Function is called 2 times.
Function is called 3 times.
Function is called 4 times.
Function is called 5 times.
n = 0
注意static int n 只初始化一次,且static int n 和main里的n互不影响(main的n在栈空间里)
更多动静态库,共享库的创建、链接问题见linux系统编程-静态库、共享库的创建和链接。



