01 编译原理
何为编译,简单来说就是将源代码转换为可执行程序的过程,这个过程包含若干步骤:预处理
、编译
、汇编
、链接
,统称为编译
,流程可见下图演示。
下面针对这4个步骤展开说明。
预处理
gcc -E xxx.c ... # 分别生成对应.i
预处理是编译过程的第一阶段,主要工作如下:
- 处理文件中所有带
#
的内容,主要有头文件包含(删除include并且将头文件内容拷贝到源文件中)
、条件编译(根据实际语义删除无用的内容保留目标)
、宏替换(会将宏全部替换为定义的符号,本质是文本替换)
- 添加行号、文件标识信息(便于后续调试),删除文件中的注释
预处理后的代码通常保存为.i文件,这个文件不包含任何宏定义,因为所有的宏都已经被展开,并且包含的文件已经被插入到.i文件中。然后,编译器会处理这个预处理后的文件,生成汇编代码或目标代码。
编译
gcc -S xxx.i ....
编译过程就是将经过预处理之后的源程序文件转换为汇编代码,其中要经过词法、语法、语义等进行分析,生成中间代码,编译器可能还会对其进行一定的优化,提高程序的效率。最后生成汇编语言文件,以.s结尾。
汇编
gcc -c xxx.s ...
汇编是将编译后生成的汇编语言代码,生成与机器相关的机器语言(根据微指令集),本质其实就是一系列01序列。
汇编之后的产物:静态库(.a本质也是可重定位目标文件,只不过将多个文件指令打包在一起)、动态库(.so)、可重定位目标文件(.o)
链接
gcc xxx.o xxx1.o ... -o #yyy(yyy为可执行文件)
链接就是将多个目标文件与外部符号、启动程序进行链接,最后得到一个可执行二进制文件。
产物:可执行目标文件。
这里的可重定位、可执行以及动态库都是ELF文件,具体内容会在后续说明。
02 链接阶段-符号解析
链接阶段的符号解析是链接器的一个关键任务,它涉及将符号的定义和引用关联起来。符号可以是变量、函数或其他在代码中引用的实体。符号解析的目的是确保在链接过程中,所有的符号引用都能够正确地找到它们对应的定义。
链接器会处理来自各个编译单元(通常是不同的源文件或目标文件)的符号信息,这些信息通常存储在符号表中。符号表是一个数据结构,它包含了程序中定义的所有符号的信息,如符号的名称、类型、作用域以及它们在内存中的位置等。
一般情况下,每个C文件可以看作一个程序模块,比如mian.o,sum.o就可以分别看作一个模块。
每个模块一般会:
- 自己定义一些符号,称为
符号定义
- 引用其他模块的符号,称为
符号引用
符号定义:
全局符号
:非静态C函数和全局变量局部符号
:static修饰的C函数和全局变量本地符号
:函数内部定义的符号,非静态本地变量,静态本地变量外部符号
:引用其他模块定义的全局符号
说明:除了非static的局部变量之外,其他符号都在链接器的管理范畴(准确来说,static局部变量其实归编译器管,编译器会在数据段为每个静态变量分配一个固定的位置;而不会出现在符号表中)
程序在运行时,函数本地非静态变量属于函数调用栈管理
每个程序模块,在编译后,都会有一个符号表,用来记录程序模块中符号
而所谓符号解析,其实就是在输入的其他程序模块的符号表中,为每个外部符号,寻找确定好的符号定义,并且关联它们
03 链接阶段-重定位
链接阶段的重定位是一个关键过程,它确保程序中的代码和数据在运行时能够正确地被加载到内存中,并执行相应的操作。重定位的作用主要是解决程序在编译和链接过程中产生的地址不确定性的问题。
在编译和链接过程中,程序中的代码和数据会被分配到不同的内存地址。然而,在编译阶段,这些地址通常是相对地址,而不是实际的物理地址。因此,在链接阶段,需要进行重定位,将这些相对地址转换为实际的物理地址。
重定位的作用是确保程序在运行时能够正确地访问内存中的代码和数据。如果没有重定位过程,程序可能会因为地址错误而无法正常运行,导致程序崩溃或数据错误。
下面举个例子看看:
通过反汇编可以看到对应可重定位目标文件中指令以及符号是只有偏移,没有地址的
汇编后的.o文件中每个指令的位置,只是这条指令在文件中的存储位置而已,而不是内存地址,所以.o文件还不能直接加载到内存运行。
在链接阶段,合并所有.o文件中的指令,并且重新定位这些指令以及指令中符号的位置,并且为每一条指令分配唯一的内存地址,还需要重新定位外部符号的内存地址,这就是为什么.o
被称之为可重定位目标文件。