目录
在ANSI C标准的任何一种实现中,存在两个不同的环境。
- 第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
- 第2种是执行环境,它用于实际执行代码
计算机能够执行二进制指令,但是我们写出的C语言代码是文本信息,计算机不能理解,翻译环境将C语言代码翻译成二进制指令,二进制指令放在可执行程序中,执行环境进行执行二进制的代码。
每个 .c 文件单独经过编译器生成 .obj(.o)目标文件,这个过程叫作“编译”,多个目标文件加上链接库经过链接器的处理生成“可执行程序”,链接器处理的过程叫链接。
我们之所以能够使用库函数,是因为这些函数被编译并存储在静态库(也称为静态链接库)中(后缀为 .LIB)。
当我们使用scanf时,在vs编译器中自动将”目标文件和链接库“通过链接器生成可执行程序。
vs2022 是集成开发环境
集成了:编译器 + 链接器 + 调试器
将 .c 文件翻译成可执行程序需要两个操作:编译和链接 。
编译中分为三个过程:预编译、编译和汇编。
在VS这样的集成开发环境中不方便观察这些细节,可以使用Linux系统上使用gcc编译器演示整个过程。
ELF格式和段表:
在Linux下,GCC编译器产生的目标文件(如test.o)和可执行程序都使用ELF格式来存储。ELF格式与程序的存储和加载密切相关,其中段表是一个关键概念:
ELF格式:ELF(Executable and Linkable Format)是一种通用的二进制文件格式,用于存储可执行程序、共享库和目标文件的信息。ELF文件包括有关程序结构和布局的重要信息。
段表:在ELF格式中,段表是一个重要的数据结构,用于描述可执行程序或目标文件中的各个段(segments)。段可以包括代码段、数据段、BSS段等,每个段都有其自己的属性和数据。段表记录了每个段的位置、大小、权限等信息。
预处理: 在编译之前,C编译器会对源代码进行预处理。在这个阶段,预处理器会处理源代码中的预处理指令,如#include
、#define
等,展开宏定义,将头文件的内容插入到源文件中,以及去除注释。预处理器生成的输出通常是一个经过预处理的中间文件。
编译: 编译器将预处理后的源代码翻译成汇编语言。这个阶段包括词法分析、语法分析、语义分析、符号汇总等步骤,以生成一个表示程序的中间表示形式。编译器的输出通常是一个或多个目标文件,这些文件包含了程序的机器码和相关信息。
符号汇总:
编译器首先会分析源代码,识别并收集所有变量和函数的名称。这是为了构建一个符号表,其中包括全局符号(如globalVar和myFunction)以及局部符号(如localVar如下图)。每个符号都会被分配一个唯一的地址或标识符,以便在后续的编译和链接阶段引用。
#include int globalVar = 42; void myFunction(int x) { printf("Parameter: %d\n", x); } int main() { int localVar = 10; myFunction(localVar); return 0; }
汇编: 汇编器将中间表示形式翻译成目标平台的机器码或汇编语言。这个阶段生成的文件通常是包含二进制机器码的目标文件,同时形成符号表。
符号表是一个数据结构,用于存储程序中所有的符号信息,包括名称、类型、地址或标识符等。编译器在编译过程中会构建符号表,以便在后续的链接和代码生成阶段使用。
链接: 编译过程中的合并段表、符号表的合并和重定位是与链接器相关的关键步骤,用于将多个源文件编译成一个可执行文件或共享库。如果程序由多个源文件组成,链接器将这些目标文件合并在一起,解决外部符号引用,并生成最终的可执行文件。这个阶段还包括对标准库和其他共享库的链接,以满足程序的依赖关系。
合并段表:
在编译和链接过程中,源代码被分成不同的段(segments)或节(sections),如代码段、数据段、堆栈段等。这些段包含了不同类型的程序代码和数据。合并段表是指将来自不同源文件的相同类型的段合并成一个大的段,以便在最终的可执行文件或共享库中有效组织和管理代码和数据。
例如,如果你有多个源文件,每个都包含一个名为
main
的代码段,链接器将合并这些代码段以创建一个名为main
的单一代码段。
符号表的合并:
符号表是一个记录程序中定义和引用的所有符号(如变量、函数、常量等)的表格。在编译过程中,每个源文件都会生成一个局部符号表,记录该文件内部定义的符号。链接器的任务之一是将这些局部符号表合并成一个全局符号表,以确保在整个程序中可以正确解析符号的引用和定义。
例如,如果在不同的源文件中都定义了一个名为
counter
的变量,链接器会合并这些定义,以便在整个程序中只有一个counter
符号。
重定位:
重定位是链接过程的重要步骤,用于解决在不同模块中引用的符号的地址问题。当一个模块引用另一个模块中的符号时,链接器需要确保这些引用正确指向符号的实际地址。这就涉及到重定位操作。
链接器会扫描所有模块,找到符号引用并将其关联到正确的符号定义。这可能涉及修改代码中的地址或生成一个跳转表来跟踪符号的地址变化。这确保了在最终可执行文件或共享库中,符号的引用与其实际定义相匹配。
生成可执行文件: 最终,链接器将生成的目标文件转换为可执行文件的格式,通常具有.exe
(Windows)或无扩展名(Linux)的文件名,这个可执行文件包含了程序的机器码、数据、元数据以及其他必要信息。
例如:我们有两个源文件 main.c
和 helper.c
编译: 每个源文件都独立编译为目标文件。这会产生例如 main.o
和 helper.o
这样的目标文件。每个目标文件都有自己的局部符号表,其中列出了该文件中定义的所有符号(如函数和全局变量)和它们的地址。
链接: 链接器的主要任务之一是合并所有目标文件的局部符号表以创建一个全局符号表。这个表中列出了程序中所有的符号及其地址。链接器还解决了所有目标文件中的外部符号引用,确保它们指向正确的地址。
- +-------+ +-------+ +-------+ +-------+
- | main.c| ----1--->|编译器 | --2--> |main.o | --3--(汇总)---> |可执行文件 |
- +-------+ +-------+ +-------+ +-------+
- |
- +-------+ +-------+ |
- |helper.c| ----1--->|编译器 | -------> |helper.o|
- +-------+ +-------+ +-------+
- |
- (符号汇总)
- |
- +-------+
- |全局符号表|
- +-------+
符号汇总过程:
main.o
和 helper.o
),查看其局部符号表。