编译链接:
编译程序的时候,具体的编译过程分为四个步骤,预编译,编译,汇编,链接。实际上,Gcc这个命令只是后台程序的包装,他会根据不同的参数要求去调用预编译编译程序cc1,汇编器as,链接器ld。
总结一下:编译程序分为四个步骤,预编译,编译,汇编,链接。预编译阶段主要将注释删除,添加行号,头文件展开,保留编译器指令。编译阶段,处理编译器指令,进行词法分析,语法分析,语义分析,代码的优化。汇编阶段则是将汇编代码转换成相应目标机器中的二进制机器码。链接阶段,将相同属性的段合并起来,并且为每个段重新调整段长度和段偏移量,接下来为合并符号表,每个符号声明的地方寻找到该符号定义的地方。为每一个符号分配虚拟地址空间上的绝对地址。
预编译:
使用gcc -E 表示只进行预编译。.cpp文件预编译成为.i文件。
预处理阶段,1、将所有的#define删除,并且展开所有的宏定义
1、处理所有的条件预编译指令,比如#if,#ifdef,#else
2、处理#include预编译指令,将包含的文件插入到该预编译指令的位置,注意,这个过程是递归进行的。
3、删除所有的注释// /* */
4、添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告信息时打印行号
5、保留所有的#pragma编译器指令
编译:
编译过程处理#pragma编译器指令,并且将预编译完成的文件进行一系列的词法分析(首先源代码程序被输入到扫描器,扫描器的任务很简单,他只是简单的进行词法分析,运用一种类似于有限状态机的算法将源代码的字符序列分割成一系列的记号),语法分析(语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树,整个分析过程采用上下文无关语法的分析手段,由语法分析器生成的语法树就是以表达式为节点的树),语义分析(由语义分析器来完成,语法分析器仅仅只能完成对表达式语法层面的分析,但是并不了解这个语句是否真正有意义,比如两个指针的乘法是没有意义的。编译器所能分析的语义是静态语义即可以在编译期确定的语义,比如类型的声明以及转换)和优化后生成相应的汇编代码文件。编译器还会做一些优化过程,主要包括针对代码的优化以及针对最终文件的优化,主要都有循环优化,删除公共表达式,删除无用的赋值,同时比较常见的就是三地址码。最基本的三地址码就是 x = y op z,这个三地址码表示将变量y和z进行op操作以后,赋值给x,这里的op操作可以是算数运算,比如加减乘除等。这都属于中间代码。而gcc强大的原因在于其支持多种语言,而他支持依据在于他产生的中间代码使得编译器被分为前端和后端。
编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。编译器后端主要包括代码生成器和目标代码优化器,代码生成器是将中间代码转换成目标机器代码,这个过程依赖于目标主机,因为不同的机器有不同的字长、数据类型等。最后目标代码优化树对目标代码进行优化,比如选择合适的寻址方式,使用位运算代替乘法运算、删除多余的指令等。最终生成汇编代码。
.i文件编译成.s文件。
汇编:
汇编过程是将汇编代码转变成机器可以执行的二进制目标代码,每一个汇编语句几乎都对应于一条机器指令。.s文件汇编成.o文件。
链接:
链接有一个重要的概念,就是函数库。一些像printf等函数在我们引用的头文件中声明,他们的实现一般都在库文件中,Linux下默认是/usr/lib。而库文件有两种,静态库.a,.lib和动态库.so,.dll,静态库是指在编译链接的时候,将库文件的代码加入到可执行文件中,所以在生成可执行文件之后,不需要库文件的支持了,但是他的体积比较大,而且库文件改变势必需要重新编译。而动态库是指在程序运行时加载库文件,所以他的可执行代码体积较小,而且库文件改变不需要重新编译可执行文件,但是执行时需要时间较长。
而链接的过程主要包括了地址和空间分配,符号决议,重定位等步骤。
地址和空间分配,一般有两种方式,一是按序叠加,就是直接将各个目标文件依次合并,,但是这样会造成输出文件有很多零散的段,而每个段都需要有一定的地址和空间对齐要求,很浪费空间,也造成大量内部碎片。二是相似段合并,他将相同性质(可读可写,只读等)的段合并到一起。现在的链接器基本都采用这种方式。使用这种方式链接,他会进行空间和地址的分配,符号解析和重定位。空间和地址的分配是扫描所有输入目标文件,并且获得他们各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表并建立映射关系。其次是符号的解析和重定位,读取到输入文件中段的数据、重定位信息,并且进行符号解析和重定位,调整代码中的地址等。
重定位的具体过程:当一个源代码需要引用另一个源代码中的变量或者函数时,这个源代码并不知道该函数的地址,所以编译器先暂时随便看一个地址,一般是0,然后其他的函数在使用偏移量。而链接器在完成地址和空间的分配之后就可以确定所有符号的虚拟地址,然后就可以根据符号的地址对每个需要重定位的指令进行地址修正。链接器在这块之所以能知道哪个指令需要修正,因为在ELF文件中,包含有一个重定位表,他的作用是描述如何修改相应的段中的内容。例如代码段.text有需要被重定位的地方,那么会有一个相应的叫.rel.text的段保存代码段的重定位表。重定位表的结构是一个结构体类型的数组,每个数组元素对应一个重定位入口。
符号解析的具体过程:在编译时,编译器向汇编器输出每个全局符号,这些符号分为强符号和弱符号。函数和初始化过的全局变量叫强符号,未初始化的全局变量叫弱符号。对于多重定义的符号,采用一条规则:有强符号用强符号,无强符号用弱符号。
重定位的过程也伴随着符号的解析过程,每个目标文件都可能定义着一些符号,也可能引用到定义在其他目标文件的符号,重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用重定位时,他就要确定这个符号的目标地址,这时候链接器会查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号。如果在扫描器扫描完所有的输入目标文件之后,未定义的符号不能在全局符号表中找到,则会引发符号未定义错误。
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。每个函数和变量都有自己独特的名字,以避免在链接过程中同名变量和函数之间的混淆,在链接中,我们将函数和变量统称为符号,函数名和变量名就是符号名。整个链接过程正是基于符号才能正确的完成,每个目标文件中都会有一个相应的符号表,每个定义的符号有一个对应的值叫做符号值。对于变量和函数来说,符号值就是他们的地址。ELF文件中的符号表是文件中的一个段,叫.symtab,符号表就是结构体数组。
总体而言链接过程,链接器为输出目标文件分配空间和地址,一旦确定地址,接下来就进行符号的解析和重定位,链接器会把各个输入目标文件中对于外部的引用进行解析,把每个段中需要重定位的指令和数据进行更正,使他们指向正确的位置。
编译链接:
编译程序的时候,具体的编译过程分为四个步骤,预编译,编译,汇编,链接。实际上,Gcc这个命令只是后台程序的包装,他会根据不同的参数要求去调用预编译编译程序cc1,汇编器as,链接器ld。
总结一下:编译程序分为四个步骤,预编译,编译,汇编,链接。预编译阶段主要将注释删除,添加行号,头文件展开,保留编译器指令。编译阶段,处理编译器指令,进行词法分析,语法分析,语义分析,代码的优化。汇编阶段则是将汇编代码转换成相应目标机器中的二进制机器码。链接阶段,将相同属性的段合并起来,并且为每个段重新调整段长度和段偏移量,接下来为合并符号表,每个符号声明的地方寻找到该符号定义的地方。为每一个符号分配虚拟地址空间上的绝对地址。
预编译:
使用gcc -E 表示只进行预编译。.cpp文件预编译成为.i文件。
预处理阶段,1、将所有的#define删除,并且展开所有的宏定义
1、处理所有的条件预编译指令,比如#if,#ifdef,#else
2、处理#include预编译指令,将包含的文件插入到该预编译指令的位置,注意,这个过程是递归进行的。
3、删除所有的注释// /* */
4、添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息以及用于编译时产生编译错误或警告信息时打印行号
5、保留所有的#pragma编译器指令
编译:
编译过程处理#pragma编译器指令,并且将预编译完成的文件进行一系列的词法分析(首先源代码程序被输入到扫描器,扫描器的任务很简单,他只是简单的进行词法分析,运用一种类似于有限状态机的算法将源代码的字符序列分割成一系列的记号),语法分析(语法分析器将对由扫描器产生的记号进行语法分析,从而产生语法树,整个分析过程采用上下文无关语法的分析手段,由语法分析器生成的语法树就是以表达式为节点的树),语义分析(由语义分析器来完成,语法分析器仅仅只能完成对表达式语法层面的分析,但是并不了解这个语句是否真正有意义,比如两个指针的乘法是没有意义的。编译器所能分析的语义是静态语义即可以在编译期确定的语义,比如类型的声明以及转换)和优化后生成相应的汇编代码文件。编译器还会做一些优化过程,主要包括针对代码的优化以及针对最终文件的优化,主要都有循环优化,删除公共表达式,删除无用的赋值,同时比较常见的就是三地址码。最基本的三地址码就是 x = y op z,这个三地址码表示将变量y和z进行op操作以后,赋值给x,这里的op操作可以是算数运算,比如加减乘除等。这都属于中间代码。而gcc强大的原因在于其支持多种语言,而他支持依据在于他产生的中间代码使得编译器被分为前端和后端。
编译器前端负责产生机器无关的中间代码,编译器后端将中间代码转换成目标机器代码。编译器后端主要包括代码生成器和目标代码优化器,代码生成器是将中间代码转换成目标机器代码,这个过程依赖于目标主机,因为不同的机器有不同的字长、数据类型等。最后目标代码优化树对目标代码进行优化,比如选择合适的寻址方式,使用位运算代替乘法运算、删除多余的指令等。最终生成汇编代码。
.i文件编译成.s文件。
汇编:
汇编过程是将汇编代码转变成机器可以执行的二进制目标代码,每一个汇编语句几乎都对应于一条机器指令。.s文件汇编成.o文件。
链接:
链接有一个重要的概念,就是函数库。一些像printf等函数在我们引用的头文件中声明,他们的实现一般都在库文件中,Linux下默认是/usr/lib。而库文件有两种,静态库.a,.lib和动态库.so,.dll,静态库是指在编译链接的时候,将库文件的代码加入到可执行文件中,所以在生成可执行文件之后,不需要库文件的支持了,但是他的体积比较大,而且库文件改变势必需要重新编译。而动态库是指在程序运行时加载库文件,所以他的可执行代码体积较小,而且库文件改变不需要重新编译可执行文件,但是执行时需要时间较长。
而链接的过程主要包括了地址和空间分配,符号决议,重定位等步骤。
地址和空间分配,一般有两种方式,一是按序叠加,就是直接将各个目标文件依次合并,,但是这样会造成输出文件有很多零散的段,而每个段都需要有一定的地址和空间对齐要求,很浪费空间,也造成大量内部碎片。二是相似段合并,他将相同性质(可读可写,只读等)的段合并到一起。现在的链接器基本都采用这种方式。使用这种方式链接,他会进行空间和地址的分配,符号解析和重定位。空间和地址的分配是扫描所有输入目标文件,并且获得他们各个段的长度、属性和位置,并且将输入目标文件中的符号表中所有的符号定义和符号引用收集起来,统一放到一个全局符号表并建立映射关系。其次是符号的解析和重定位,读取到输入文件中段的数据、重定位信息,并且进行符号解析和重定位,调整代码中的地址等。
重定位的具体过程:当一个源代码需要引用另一个源代码中的变量或者函数时,这个源代码并不知道该函数的地址,所以编译器先暂时随便看一个地址,一般是0,然后其他的函数在使用偏移量。而链接器在完成地址和空间的分配之后就可以确定所有符号的虚拟地址,然后就可以根据符号的地址对每个需要重定位的指令进行地址修正。链接器在这块之所以能知道哪个指令需要修正,因为在ELF文件中,包含有一个重定位表,他的作用是描述如何修改相应的段中的内容。例如代码段.text有需要被重定位的地方,那么会有一个相应的叫.rel.text的段保存代码段的重定位表。重定位表的结构是一个结构体类型的数组,每个数组元素对应一个重定位入口。
符号解析的具体过程:在编译时,编译器向汇编器输出每个全局符号,这些符号分为强符号和弱符号。函数和初始化过的全局变量叫强符号,未初始化的全局变量叫弱符号。对于多重定义的符号,采用一条规则:有强符号用强符号,无强符号用弱符号。
重定位的过程也伴随着符号的解析过程,每个目标文件都可能定义着一些符号,也可能引用到定义在其他目标文件的符号,重定位的过程中,每个重定位的入口都是对一个符号的引用,那么当链接器需要对某个符号的引用重定位时,他就要确定这个符号的目标地址,这时候链接器会查找由所有输入目标文件的符号表组成的全局符号表,找到相应的符号。如果在扫描器扫描完所有的输入目标文件之后,未定义的符号不能在全局符号表中找到,则会引发符号未定义错误。
在链接中,目标文件之间相互拼合实际上是目标文件之间对地址的引用,即对函数和变量的地址的引用。每个函数和变量都有自己独特的名字,以避免在链接过程中同名变量和函数之间的混淆,在链接中,我们将函数和变量统称为符号,函数名和变量名就是符号名。整个链接过程正是基于符号才能正确的完成,每个目标文件中都会有一个相应的符号表,每个定义的符号有一个对应的值叫做符号值。对于变量和函数来说,符号值就是他们的地址。ELF文件中的符号表是文件中的一个段,叫.symtab,符号表就是结构体数组。
总体而言链接过程,链接器为输出目标文件分配空间和地址,一旦确定地址,接下来就进行符号的解析和重定位,链接器会把各个输入目标文件中对于外部的引用进行解析,把每个段中需要重定位的指令和数据进行更正,使他们指向正确的位置。