问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C语言的编译和链接:从源代码到可执行文件

创作时间:
作者:
@小白创作中心

C语言的编译和链接:从源代码到可执行文件

引用
1
来源
1.
https://cloud.tencent.com/developer/article/2503064

编译和链接是C语言程序从源代码到可执行文件的关键过程。本文将详细介绍这两个过程的具体步骤和原理,帮助读者深入理解程序的执行机制。

一、编译和链接

1. 什么是编译和链接?

在编写C语言程序时,我们通常会写一个或多个.c文件(源代码文件)。计算机并不能直接理解这些文本文件,需要将它们转换为机器可以执行的二进制文件。这个过程分为两个主要步骤:

  • 编译:将源代码(.c文件)转换为目标文件(.o.obj文件)。
  • 链接:将多个目标文件和库文件合并,生成最终的可执行文件(如.exe.out文件)。

下面我们详细讲解这两个过程。

2. 编译过程

编译是将C语言源代码转换为机器代码的过程。它分为以下几个步骤:

2.1 预处理(Preprocessing)

预处理阶段,源文件和头文件会被处理成以.i为后缀的文件。预处理主要处理源文件中以#开头的预编译指令,比如:

  1. 宏定义处理:删除所有#define,并展开所有宏定义。假设代码中有#define PI 3.14159,预处理后代码中所有的PI都会被替换为3.14159
  2. 条件编译指令处理:处理#if#ifdef#elif#else#endif等条件编译指令,根据条件决定代码的取舍。
  3. 头文件包含处理:处理#include预编译指令,将包含的头文件内容插入到该指令的位置,这个过程是递归进行的。如果test.c#include "stdio.h",预处理时stdio.h的内容会被插入到#include所在的位置。
  4. 保留#pragma指令:保留#pragma编译器指令,供后续编译器使用。
  5. 注释删除:删除所有注释,让代码更加简洁,便于后续处理。

在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

2.2 编译(Compilation)

编译过程是将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件,使用gcc -S test.i -o test.s命令。汇编代码一般具备的信息:

  • 指令信息:这些指令对应着计算机处理器的基本操作,如数据传送(MOV)、算术运算(ADD、SUB)、逻辑运算(AND、OR)、跳转(JMP、JE)等
  • 数据信息:定义了程序中使用的各种数据,包括变量、常量等。
  • 控制信息:包含了程序的控制结构信息,如循环、条件判断等。这些控制结构通过跳转指令(如JMP、JE、JNE等)来实现。
  • 内存和寄存器信息明确了程序中使用的内存地址和寄存器。寄存器是处理器内部的高速存储单元,汇编代码会频繁地使用寄存器来进行数据处理和传递。

下面通过array[index] = (index + 4) * (2 + 6);这段代码来看看编译过程:

  1. 词法分析:将源代码输入扫描器,扫描器把代码中的字符分割成一系列记号,如关键字、标识符、字面量、特殊字符等。上述代码经过词法分析后会得到array[index6)等16个记号。
  2. 语法分析:语法分析器对扫描产生的记号进行语法分析,生成以表达式为节点的语法树。在这个过程中,编译器会检查代码的语法结构是否正确,比如括号是否匹配、语句是否完整等。
  3. 语义分析:语义分析器完成语义分析,主要包括声明和类型的匹配、类型的转换等,同时报告错误的语法信息。比如,如果代码中把两个不同类型的变量进行不兼容的运算,语义分析阶段就会报错。

2.3 汇编(Assembly)

汇编器将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。使用gcc -c test.s -o test.o命令完成汇编过程。汇编器会根据汇编指令和机器指令的对照表进行翻译,这个过程不做指令优化。

3. 链接过程

链接是将多个目标文件和库文件合并,生成最终可执行文件的过程。链接器的主要任务包括:

3.1 符号解析(Symbol Resolution)

在编译过程中,每个源文件会生成一个目标文件。如果多个文件之间有函数调用或变量引用,链接器需要解析这些符号(函数名、变量名等),确保它们能够正确关联。

示例:

  • main.c中调用了math.c中的add函数。
  • 链接器会找到add函数的定义,并将其与main.c中的调用关联起来。
3.2 重定位(Relocation)

目标文件中的地址通常是相对地址。链接器会将这些相对地址转换为最终可执行文件中的绝对地址。

假设有test.cadd.c两个文件,test.c中使用了add.c中的Add函数和g_val变量。每个源文件单独编译生成对应的目标文件,test.c生成test.oadd.c生成add.o。在编译test.c时,并不知道Add函数和g_val变量的地址,所以暂时搁置调用Add指令的目标地址和g_val的地址。链接器会根据引用的符号Add在其他模块中查找Add函数的地址,然后将test.c中所有引用到Add的指令重新修正,让它们的目标地址为真正的Add函数的地址,对于全局变量g_val也采用类似方法修正地址,这个过程就是重定位。

3.3 生成可执行文件

链接器将所有目标文件和库文件合并,生成一个可执行文件(如a.outprogram.exe)。这个文件可以直接在操作系统中运行。

4. 编译和链接的示意图

以下是一个简单的示意图,展示了从源代码到可执行文件的过程:

5. 实际使用中的编译和链接

在实际开发中,我们通常使用编译器(如gcc)来自动完成编译和链接的过程。例如:

gcc main.c math.c -o program

这条命令会:

  1. 编译main.cmath.c,生成目标文件。
  2. 链接目标文件,生成可执行文件program

二、翻译环境和运行环境

1. 翻译环境

翻译环境就是由上面提到的两个过程:编译和链接组成。而编译又可以进一步细分为预处理、编译、汇编三个子过程。 总结一下:

  • 预处理:主要处理源文件中以#开头的预编译指令
  • 编译:将预处理后的文件进行词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件
  • 汇编:汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令
  • 链接:将多个目标文件和库文件合并,生成最终可执行文件的过程。

2. 运行环境

程序在翻译环境生成可执行文件后,就进入运行环境:

  1. 程序载入内存:在有操作系统的环境中,一般由操作系统完成程序的载入;在独立环境中,程序的载入可能需要手工安排,或者通过可执行代码置入只读内存来完成。
  2. 程序执行开始:程序载入内存后,开始执行,首先调用main函数。
  3. 执行程序代码:程序使用运行时堆栈存储函数的局部变量和返回地址,同时也可以使用静态内存,静态内存中的变量在程序整个执行过程中一直保留其值。
  4. 终止程序:程序执行结束,正常情况下是main函数执行完毕返回,也有可能因为意外情况终止。
© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号