📝前言: 这篇文章主要讲解一下C语言的编译和链接,帮我们更好的理解程序的执行过程,更好的理解计算机系统。
在编写C语言程序时,我们通常会写一个或多个.c
文件(源代码文件)。计算机并不能直接理解这些文本文件,需要将它们转换为机器可以执行的二进制文件。这个过程分为两个主要步骤:
.c
文件)转换为目标文件(.o
或.obj
文件)。.exe
或.out
文件)。下面我们详细讲解这两个过程。
编译是将C语言源代码转换为机器代码的过程。它分为以下几个步骤:
预处理阶段,源文件和头文件会被处理成以.i
为后缀的文件。预处理主要处理源文件中以#
开头的预编译指令,比如:
#define
,并展开所有宏定义。假设代码中有#define PI 3.14159
,预处理后代码中所有的PI
都会被替换为3.14159
。#if
、#ifdef
、#elif
、#else
、#endif
等条件编译指令,根据条件决定代码的取舍。#include
预编译指令,将包含的头文件内容插入到该指令的位置,这个过程是递归进行的。如果test.c
中#include "stdio.h"
,预处理时stdio.h
的内容会被插入到#include
所在的位置。#pragma
指令:保留#pragma
编译器指令,供后续编译器使用。在gcc环境下,可以使用gcc -E test.c -o test.i
命令查看预处理后的结果。
示例:
#include <stdio.h>
#define PI 3.14159
int main() {
printf("PI = %f\n", PI);
return 0;
}
预处理后,#include <stdio.h>
会被替换为stdio.h
文件的内容,PI
会被替换为3.14159
。
编译过程是将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件,使用gcc -S test.i -o test.s
命令。
汇编代码一般具备的信息:
MOV
)、算术运算(ADD、SUB
)、逻辑运算(AND、OR
)、跳转(JMP、JE
)等JMP、JE、JNE
等)来实现。下面通过array[index] = (index + 4) * (2 + 6);
这段代码来看看编译过程:
array
、[
、index
…6
、)
等16个记号。
汇编器将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。使用gcc -c test.s -o test.o
命令完成汇编过程。汇编器会根据汇编指令和机器指令的对照表进行翻译,这个过程不做指令优化。
链接是将多个目标文件和库文件合并,生成最终可执行文件的过程。链接器的主要任务包括:
在编译过程中,每个源文件会生成一个目标文件。如果多个文件之间有函数调用或变量引用,链接器需要解析这些符号(函数名、变量名等),确保它们能够正确关联。
示例:
main.c
中调用了math.c
中的add
函数。add
函数的定义,并将其与main.c
中的调用关联起来。目标文件中的地址通常是相对地址。链接器会将这些相对地址转换为最终可执行文件中的绝对地址。
假设有test.c
和add.c
两个文件,test.c
中使用了add.c
中的Add
函数和g_val
变量。每个源文件单独编译生成对应的目标文件,test.c
生成test.o
,add.c
生成add.o
。在编译test.c
时,并不知道Add
函数和g_val
变量的地址,所以暂时搁置调用Add
指令的目标地址和g_val
的地址。链接器会根据引用的符号Add
在其他模块中查找Add
函数的地址,然后将test.c
中所有引用到Add
的指令重新修正,让它们的目标地址为真正的Add
函数的地址,对于全局变量g_val
也采用类似方法修正地址,这个过程就是重定位。
链接器将所有目标文件和库文件合并,生成一个可执行文件(如a.out
或program.exe
)。这个文件可以直接在操作系统中运行。
以下是一个简单的示意图,展示了从源代码到可执行文件的过程:
在实际开发中,我们通常使用编译器(如gcc
)来自动完成编译和链接的过程。例如:
gcc main.c math.c -o program
这条命令会:
main.c
和math.c
,生成目标文件。program
。翻译环境就是由上面提到的两个过程:编译和链接组成。而编译又可以进一步细分为预处理、编译、汇编三个子过程。 总结一下:
#
开头的预编译指令程序在翻译环境生成可执行文件后,就进入运行环境:
main
函数。main
函数执行完毕返回,也有可能因为意外情况终止。