关键步骤
预处理、编译、汇编、链接是C语言(以及许多其他编程语言)编译过程中的四个关键步骤,每个步骤都承担着特定的任务。以下是这四个步骤的详细说明:
1. 预处理(Preprocessing)
任务:在源代码文件被编译之前进行一系列文本替换和处理操作。
具体工作:
- 宏定义展开 :将源代码中所有的宏定义(如
#define
)进行替换,将宏的名称替换为其定义的内容。 - 头文件包含 :处理所有的
#include
指令,将指定的头文件内容插入到源代码的相应位置。 - 条件编译 :根据
#if
、#ifdef
、#ifndef
、#else
、#elif
、#endif
等预处理指令,决定哪些代码段应该被编译。 - 删除注释:移除源代码中的所有注释,这些注释对于编译器来说是多余的。
- 处理特殊操作符 :如
#pragma
指令,这些指令用于提供编译器特定的指令或信息。
目的:为后续的编译过程准备一个干净、无宏定义、无头文件包含指令、无多余注释的源代码版本。
2. 编译(Compilation)
任务:将预处理后的源代码转换成汇编代码。
具体工作:
- 词法分析:将源代码分解成一系列的标记(tokens),如关键字、标识符、字面量等。
- 语法分析:根据语言的语法规则,将标记组织成语法树(或抽象语法树AST)。
- 语义分析:检查语法树中的操作在语义上是否有效,如类型检查、作用域检查等。
- 优化:对生成的中间代码进行优化,以提高程序的执行效率。
- 生成汇编代码:将优化后的中间代码转换成目标机器的汇编代码。
目的:将人类可读的源代码转换成机器可读的汇编代码。
3. 汇编(Assembly)
任务:将汇编代码转换成机器码。
具体工作:
- 代码转换:将汇编指令转换成对应的机器指令。
- 生成目标文件 :将机器指令和相关的数据(如常量、变量等)打包成一个或多个目标文件(如
.o
或.obj
文件)。
目的:将汇编代码转换成计算机可以直接执行的机器码。
4. 链接(Linking)
任务:将多个目标文件以及所需的库文件合并成一个可执行文件。
具体工作:
- 符号解析:查找并解决目标文件中引用的外部符号(如函数、变量等)。
- 重定位:调整目标文件中代码和数据的位置,确保它们在可执行文件中的正确布局。
- 合并段:将不同的目标文件和库文件中的代码段、数据段等合并成一个整体。
- 生成可执行文件:将合并后的代码和数据写入一个可执行文件中,该文件可以在操作系统上直接运行。
目的:生成一个完整的、可以在操作系统上直接运行的可执行文件。
这四个步骤共同构成了C语言(及其他编程语言)的编译和链接过程。每个步骤都扮演着不可或缺的角色,确保了源代码能够最终被转换成计算机可以直接执行的机器码。
流程
该部分借鉴了《嵌入式C语言的自我修养》。
从C程序到可执行文件,整个编译过程并不是一气呵成、一步完成的,而是环环相扣、多步执行的。如图4-3所示,程序的整个编译流程主要分为以下几个阶段:预处理、编译、汇编、链接。每个阶段需要调用不同的工具去完成,上一阶段的输出作为下一阶段的输入,步步推进。
图4-3 程序的编译、链接流程
在一个多文件的C项目中,编译器是以C源文件为单位进行编译的。在编译的不同阶段,编译程序(如gcc、arm-linux-gcc)会调用不同的工具来完成不同阶段的任务。在编译器安装路径的bin目录下,你会看到各种各样的编译工具,gcc在程序编译过程中会分别调用它们,常见的工具有预处理器、编译器、汇编器、链接器。● 预处理器:将源文件main.c经过预处理变为main.i。● 编译器:将预处理后的main.i编译为汇编文件main.s。● 汇编器:将汇编文件main.s编译为目标文件main.o。● 链接器:将各个目标文件main.o、sub.o链接成可执行文件a.out。
最后生成的可执行文件a.out其实也是目标文件(object file),唯一不同的是,a.out是一种可执行的目标文件。目标文件一般可以分为3种。
● 可重定位的目标文件(relocatable files)。
● 可执行的目标文件(executable files)。
● 可被共享的目标文件(shared object files)。
汇编器生成的目标文件是可重定位的目标文件,是不可执行的,需要链接器经过链接、重定位之后才能运行。可被共享的目标文件一般以共享库的形式存在,在程序运行时需要动态加载到内存,跟应用程序一起运行。
其他
指令集
指令集在编译过程中的汇编阶段发挥关键作用。具体来说,汇编器(Assembler)使用指令集将汇编代码转换成机器代码(即二进制指令)。指令集是每种CPU特有的,定义了CPU可以执行的所有指令的集合,包括数据处理指令、跳转指令、内存访问指令等。
汇编器根据汇编语言中的指令和指令集中的定义,将每一条汇编指令转换成对应的机器指令。这些机器指令是CPU可以直接执行的二进制代码。指令集不仅决定了CPU能够执行哪些操作,还影响了程序的性能和效率。
在编译过程中,预处理、编译和链接阶段虽然也涉及代码的转换和处理,但它们并不直接涉及指令集的使用。预处理主要处理宏定义、条件编译等;编译阶段将源代码转换成汇编代码,但尚未涉及具体的CPU指令;链接阶段则是将多个目标文件合并成一个可执行文件,处理外部引用和重定位等,同样不直接涉及指令集的使用。
因此,可以说指令集在汇编阶段被用于将汇编代码转换成机器代码,是编译过程中不可或缺的一部分。
机器码
不同架构的机器的机器码不一样,因此需要根据汇编指令编译成不同的机器码文件。
首先,需要明确几个基本概念:
- 机器码:也称为机器指令或机器语言,是计算机可以直接执行的二进制代码。它是计算机硬件与操作系统、应用程序以及其他软件交互的最低级别语言。由于机器码是与特定硬件架构相关的,因此不同的计算机架构可能有不同的机器码集。
- 指令集架构(ISA):定义了计算机支持的指令集,以及每条指令的格式和功能。不同的指令集架构会有不同的性能特性和应用场景。
接下来,我们分析不同架构机器的机器码差异:
- 指令集差异:不同架构的机器采用不同的指令集。例如,ARM架构通常采用RISC(精简指令集计算)模式,而x86架构则往往采用CISC(复杂指令集计算)模式。这两种模式在指令的组织、执行效率和内存访问模式上存在显著差异。因此,针对ARM架构编写的汇编指令无法直接在x86架构的机器上执行,反之亦然。
- 内存管理差异:不同架构的机器在内存管理方面也有所不同。这包括缓存机制、内存对齐以及页表管理等方面的差异。这些差异会影响编译时内存访问模式的优化,进而影响生成的机器码。
- 优化选项和工具链兼容性:为了最大化芯片的性能,对代码进行特定架构的优化是至关重要的。这涉及到编译器和工具链对特定架构的支持能力。不同架构的芯片需要不同的编译器选项和工具链来生成优化的机器码。
因此,在编译过程中,编译器需要根据目标机器的指令集架构、内存管理特性以及优化选项来生成相应的机器码文件。这些机器码文件是特定于目标机器的,无法直接在其他架构的机器上执行。
总结来说,由于不同架构的机器在指令集、内存管理等方面存在差异,因此需要根据汇编指令编译成与特定架构相匹配的机器码文件。这是实现跨平台软件开发和部署的关键步骤之一。
链接器选择
链接器的主要任务是将多个编译后的目标文件(.o或.obj文件)以及所需的库文件链接成一个可执行文件。在选择链接器时,同样需要考虑目标架构的兼容性。链接器需要理解目标架构的内存布局、符号解析规则等,以确保链接过程的正确性。
与编译器类似,链接器也通常是作为编译器套件的一部分提供的。例如,GCC套件中就包含了ld链接器,它支持多种架构的链接需求。
交叉编译
在跨平台开发场景中,开发者可能需要在一种架构的计算机上为另一种架构的硬件编译程序。这时就需要使用交叉编译器和交叉链接器。交叉编译器和链接器被设计为能够生成针对特定目标架构的机器代码,而无需在目标硬件上实际运行编译过程。
交叉编译技术广泛应用于嵌入式系统开发、移动应用开发等领域。通过使用交叉编译工具链,开发者可以在PC等高性能计算机上完成编译工作,并将生成的可执行文件部署到目标硬件上运行。
预处理过程
预处理过程本身并不直接考虑不同架构的差异,但它为后续步骤(如编译、汇编、链接)针对不同架构的优化提供了基础。
预处理的主要任务是:进行文本替换和处理,如宏定义展开、头文件包含、条件编译等。这些操作是基于源代码文本的,与具体的硬件架构无直接关系。然而,预处理的结果会直接影响到后续的编译过程,因为编译器需要根据预处理后的源代码来生成汇编代码。
虽然预处理不直接考虑不同架构,但开发者在编写源代码时,通常会考虑到目标架构的特性,并在代码中做出相应的调整。例如,使用与目标架构相匹配的数据类型、函数调用约定等。这些考虑因素在预处理阶段可能并不明显,但在后续的编译和链接过程中会体现出来。
此外,当涉及到跨平台开发时,开发者可能会使用交叉编译工具链,这些工具链已经针对不同架构进行了优化。在这种情况下,预处理、编译、汇编和链接等步骤都会针对目标架构进行相应的处理。但即使如此,预处理步骤本身仍然主要是基于源代码文本的文本替换和处理,并不直接涉及架构差异。
综上所述,预处理过程不直接考虑不同架构的差异,但它为后续步骤针对不同架构的优化提供了必要的源代码基础。开发者在编写源代码时需要考虑目标架构的特性,并在代码中做出相应的调整,以确保程序能够在目标架构上正确运行。