可执行文件的生成
1. 可执行文件的生成
源代码到可执行文件的生成可分为预处理(Prepressing)、编译(Compilation)、汇编(Assembly)和链接(Linking),四个步骤。
1.1 预处理
以 C 语言为例,预处理主要是处理源代码中以“#”开头的那些预处理指令,规则如下:
-
将所有 “#define” 删除并展开宏定义;
-
处理所有条件预编译指令比如 “#if”、“#ifdef”、“#elif”、“#else”、“#endif”;
-
处理 “#include” 预编译指令,将被包含的文件插入到预编译位置。递归进行,被包含文件可能还包含其他文件;
-
删除所有注释;
-
添加行号与文件名标识,以便编译器产生调试信息;
-
保留 #pragma 编译器指令,因为编译器必须使用它们;
1.2 编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后产生相应的汇编文件。
命令:gcc -S hello.c -o hello.s
1.2.1 词法分析
首先源代码进入扫描器(Scanner),扫描器利用有限状态机(Finite State Machine)算法将源代码的字符序列分割成一系列符号(Token)。
词法分析产生的符号分为以下几类:关键字、标识符、字面量(包含数字、字符串等)和特殊符号(如加号、等号)。
与此同时,扫描器也完成了将标识符放入符号表,将数字、字符串常量放入文字表等工作,以备后续步骤使用。
1.2.2 语法分析
语法分析器(Grammar Parser)对扫描器生成的符号进行语法分析,整个过程采取上下文无关语法(Context-free Grammar)分析法,生成语法树,就是以表达式(Expression)为结点的树。
1.2.3 语义分析
接下来就是语义分析器(Semantic Analyzer)对表达式进行语义层面的分析。
编译器所能分析的语义叫做静态语义(Static Semantic),也就是在编译期可以确定的语义,与之对应的叫动态语义(Dynamic Semantic),要到运行期才能确定的语义。
静态语义包括声明和类型的匹配,类型的转换。比如浮点型表达式赋值给整型表达式,这里就隐含了类型转换工作。
动态语义一般是运行期出现的相关问题,比如将 0 作为除数就是一个运行期语义错误。
1.2.4 目标代码生成与优化
代码生成器(Code Generator)将中间代码转化为目标机器代码。 然后目标代码优化器(Target Code Generator)对目标代码进行优化,比如选择合适的寻址方式。
1.3 汇编
汇编是汇编器将汇编代码转变成机器可执行的指令的过程。汇编指令与机器指令几乎一一对应,所以汇编器直接翻译就可以了。
命令:gcc -c hello.s -o hello.o
1.4 链接的过程
为什么汇编器不直接输出可执行文件而是输出一个目标文件?
重定位:程序写好并不是一成不变的。这种重新计算各个目标的地址过程叫做重定位。
在一个程序被分割成多个模块以后,模块之间的组合问题可以归结为模块之间如何通信。比如C++模块间的函数调用,模块间的变量访问。函数访问必须知道目标函数的地址,这两者都可归结为模块符号间的引用。
而链接就是把各个模块之间的相互引用处理好,使得各个模块之间能正确衔接。
链接过程到底包含了什么内容?
链接的主要过程包括地址与空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等步骤。
最基本的静态链接过程,每个模块的源代码文件经过编译器的编译,形成目标文件(Object File,.o 或 .obj),目标文件和库(Library)一起链接形成最终的可执行文件。
比如我们在程序模块main.c
中使用另一模块func.c
中的函数foo()
,我们在main.c
模块每一处调用foo
的时候都必须确切知道foo
这个函数的地址,但是由于每个模块都是独立编译的,在编译main.c
的时候并不知道foo
函数的地址,所以暂时把这些调用foo
的指令的目标地址搁置,等待最后链接的时候由连接器去将这些指令的目标地址修正。
对其他定义在目标文件的变量来说,也存在同样的问题。比如目标文件 A 中一个变量 Foo在链接了目标文件 B 之后才能确定地址。确定后,链接器就要对这个地址进行修正,这个修正的过程就叫重定位(Relocation),每个要被修正的地方叫一个重定位入口(Relocation Entry)。
什么是目标文件?
编译器编译源代码后生成的文件叫做目标文件,目标文件从结构上讲,是已经编译后的可执行文件格式,只是还没经过链接的过程。
目标文件的格式:
现在PC平台流行的可执行文件格式主要是windows下的PE、和linux下的ELF,ELF文件标准把系统中采用ELF格式的文件归为4类:
-
可重定位文件:Linux下的.o
-
可执行文件
-
共享目标文件
-
核心转储文件
Linux下可以使用命令file来查看相应文件的格式;
-
-
ELF文件是大端还是小端;
-
ELF文件版本号;
-
魔数;
-
-
段表
-
段表描述了ELF文件各个段的信息。(比如每个段的段名、长度,在文件中的偏移、读写权限及段的其他属性)
-
-
重定位表
-
链接器在处理可重定位文件时,需要对其中某些部位进行重定位,即代码段和数据段中那些对绝对位置引用的地方。
-
这些重定位信息都记录在重定位表中。(如.rel.text 是针对 .text段的重定位表)。
-
-
符号表
-
在链接中,可重定向文件之间的相互拼合实际上是对地址的引用,即对函数和变量的地址的引用。我们将函数和变量统称为符号(Symbol)
-
C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。
-
不允许强符号被多次定义;
-
如果一个符号在某个目标文件中是强符号,在其他文件都是弱符号,那么选择强符号;
-
如果一个符号在所有目标文件都是弱符号,那么选择其中占用空间最大的一个;
3.
整个链接过程分两步:
-
第一步 空间与地址 这一步扫描所有输入文件,获取各个段的长度、属性和位置,并将所有符号表中的定义与引用统一放到全局符号表。
-
第二步 符号解析与重定位 使用第一步收集到的信息进行符号解析与重定位、调整代码中的地址。这一步是链接过程的核心,特别是重定位。
链接器根据符号地址对每个需要重定位的指令进行地址修正。
那么链接器如何知道哪些指令是要调整的呢?
- 重定位表
重定位表(Relocation Table)是专门保存有关重定位信息的结构。重定位表也叫重定位段,比如代码段.text
(数据段.data
)里有要被重定位的地方,那么就会有一个相对应叫.rel.text(.rel.data)
的段保存了代码段/数据段的重定位表。
每个要被重定位的地方叫一个重定位入口,重定位入口的偏移表示该入口在要被重定位的段中的位置。
- 符号解析
程序执行时所需要的指令和数据必须全在内存中才能正常运行,最简单的办法就是将程序原型所需要的指令和数据全都装入内存中,这就是最简单的静态装入的办法。
- 动态装入
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 零经验选手,Compose 一天开发一款小游戏!
· 因为Apifox不支持离线,我果断选择了Apipost!
· 通过 API 将Deepseek响应流式内容输出到前端