gcc hello.c ./a.out
编译分解成四步骤:
预处理,编译,汇编和链接
源码->.i文件->.s文件->.o文件
gcc -E hello.c -o hello.i (只进行预编译) --预处理器
通过查看.i文件可以看到宏展开后和头文件展开后的情况
gcc -S hello.i -o hello.s(进行编译,生成汇编代码)--编译器
gcc只是个封装,会根据不同参数调用 预编译编译程序cc1,汇编器as,链接器ld
gcc -c hello.s -o hello.o (将汇编转换成机器指令,即目标文件 Object File,也经常被称为模块) --汇编器
ld -static crtl.o crti.o crtbeginT.o hello.o --start-group -lgcc -lgcc_eh -lc-end-group crtend.o crtn.o(将相关.o链接起来形成一个可执行文件) --链接器
ps:通过编译选项 -save-temps可保留生成的.s .i等临时文件
编译器做了什么?
一般可分成6步:扫描,语法分析,源代码优化,代码生成,目标代码优化
扫描器(lex)的功能:词法分析,将源代码分割解析成一系列记号,一般可以分成:关键字,标识符,字面量(包含数字、字符串等),和特殊符号(如加号,等号)及将标识符存放到符号表,数字字符串等常量放到文字表等,供后面的步骤使用
语法分析(语法分析器 yacc):对扫描器产生的记号进行语法分析,从而产生语法树(Syntax tree),整个分析过程采用了上下文无关语法。语法树就是以表达式为节点的树
在这个过程中要根据运算符号确定语法树,因此该阶段必须能够识别运算符是否合法,如缺少括号等就是在这个过程报错(参见图2-3 p44)
语义分析(语义分析器 ):语法分析只是完成了语法层面的分析,实际是否有意义并不知道,比如两个指针相乘,是没有意义的,但语法上是合法的。编译器所能分析的只是静态语义(编译期可以确定的语义,如声明和类型的匹配,类型的转换),与之对应的动态语义(运行期才能确定的语义,比如除0错误);语义分析后,整个语法树都被标识了类型(参见图2-4 p45),整棵语法树都被更新
中间语言生成(源码级优化器):由于直接在语法树上进行优化会比较困难,会将整个语法树转换成中间代码(中间代码有很多种类型,不同编译器不一样,比较常见的有:三地址码和P-代码)
基本的三地址码:z=x op y, op 代表各类操作,如加减乘除
为了符合三地址码的格式,所有操作都会符合三地址码的格式,实例参见p46
中间代码使得编译器被分为前端和后端,前端负责生成中间代码(与机器无关),后端负责将中间代码转换成目标机器代码
目标代码生成和优化(编译器后端,主要包含代码生成器和目标代码优化器,p47):
代码生成器将中间代码转换成目标机器代码
目标代码优化器将对目标代码进行优化,比如使用位移代替乘法运算,删除多余指令
最终运行时的绝对地址都要在最终链接的时候确定,编译器负责将源码文件编译成一个未链接的目标文件,最终由链接器将目标文件链接起来,形成可执行文件
重定位:重新计算各个目标地址的过程(编译时无法确认另外一个模块变量地址,链接时可以确定,将无法确认的地址(0x00)修改成确定的地址)
每个要被修正的地方叫做重定位入口(参考p53)
符号:使用一个名称来代表一个地址(比如 jmp foo,foo代表一个地址)
链接:通过模块间符号的引用将多个模块拼接在一起的过程称为链接
模块拼接-静态链接
链接过程主要包括:地址和空间分配,符号决议和重定位
符号决议别名(符号绑定,名称绑定,名称决议,地址绑定,指令绑定)静态链接更倾向于使用决议,动态链接更倾向于绑定