C语言编译和链接超详解

[复制链接]
发表于 2026-2-9 20:54:14 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

1. 翻译情况和运行情况

在 ANSIC 的任何一种实现中,存在两个差别的情况。
第1种是翻译情况,在这个情况中源代码被转换为可实验的呆板指令(二进制指令)
第2种是实验情况,它用于现实实验代码。
本文便是偏重先容翻译情况的。

2. 翻译情况

那翻译情况是怎么将源代码转换为可实验的呆板指令的呢?这里我们就得睁开开解说一下翻译情况所做的事故。
着实翻译情况是由编译和链接两个大的过程构成的,而编译又可以分解成:预处理惩罚(有些书也叫预编译)、编译、汇编三个过程。

一个C语言的项目中大概有多个.c文件一起构建,那多个.c 文件怎样天生可实验步伐呢?
多个.c文件单独颠末编译器,编译处理惩罚天生对应的目标文件。
注:在Windows情况下的目标文件的后缀是.obj,Linux情况下目标文件的后缀是.o。
多个目标文件和链接库一起颠末链接器处理惩罚天生终极的可实验步伐。
链接库是指运行时库(它是支持步伐运行的根本函数聚集)大概第三方库。
假如再把编译器睁开成3个过程,那就变成了下面的过程:

2.1 预处理惩罚(预编译)

在预处理惩罚阶段,源文件和头文件会被处理惩罚成为.i为后缀的文件。
在VSCode, gcc 情况下想观察一下,对 test.c 文件预处理惩罚后的.i文件,下令如下:
  1. gcc -E test.c -o test.i
复制代码
由于未便用图片观察,以是这里不做展示,感爱好的话可以使用VSCode自行搭建 gcc 编译情况,固然这有肯定的门槛,假如你是初学者,那就不发起浪费时间在这上面。
预处理惩罚阶段重要处理惩罚那些源文件中#开始的预编译指令。比如:#include,#define,处理惩罚的规则如下:

  • 将全部的 #define 删除,并睁开全部的宏界说。
  • 处理惩罚全部的条件编译指令,如:#if、#ifdef、#elif、#else、#endif。
  • 处理惩罚#include 预编译指令,将包罗的头文件的内容插入到该预编译指令的位置。这个过程是递归举行的,也就是说被包罗的头文件也大概包罗其他文件。
  • 删除全部的解释
  • 添加行号和文件名标识,方便后续编译器天生调试信息等。
  • 保存全部的#pragma的编译器指令,编译器后续会使用。
  • 颠末预处理惩罚后的.i文件中不再包罗宏界说,由于宏已经被睁开。而且包罗的头文件都被插入到.i文件中。以是当我们无法知道宏界说大概头文件是否包罗精确的时间,可以检察预处理惩罚后的.i文件来确认
2. 2 编译

编译过程就是将预处理惩罚后的文件举行一系列的:词法分析、语法分析、语义分析及优化,天生相应的汇编代码文件。
编译过程的下令如下:
  1. gcc -S test.i -o test.s
复制代码
对下面代码举行编译的时间,会怎么做呢?假设有下面的代码:
  1. array[index] = (index+4)*(2+6);
复制代码
2. 2. 1 词法分析

将源代码步伐被输入扫描器,扫描器的使命就是简单的举行词法分析,把代码中的字符分割成一系列的暗号(关键字、标识符、字面量、特殊字符等)
上面步伐举行词法分析后得到了16个暗号,如图所示:

2. 2. 2 语法分析

接下来是语法分析器,将对扫描产生的暗号举行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。

2. 2. 3 语义分析

由语义分析器来完成语义分析,即对表达式的语法层面分析。
编译器所能做的分析是语义的静态分析。静态语义分析通常包罗声明和范例的匹配,范例的转换等。这个阶段会陈诉错误的语法信息。

2. 3 汇编

汇编器是将汇编代码转变化成呆板可实验的指令,每一个汇编语句险些都对应一条呆板指令。
就是根据汇编指令和呆板指令的对照表逐一地举行翻译,也不做指令优化。
汇编的下令如下:
  1. gcc -c test.s -o test.o
复制代码
2. 4 链接

链接是一个复杂的过程,链接的时间须要把一堆文件链接在一起天生可实验步伐。
链接过程重要包罗:地点和空间分配,符号决媾和重定位等这些步调。
链接办理的是一个项目中多文件、多模块之间相互调用的标题。
比如:
在一个C语言项目中有2个.c文件(test.c 和 add.c),如下:

  1. //test.c
  2. #include <stdio.h>
  3. //声明外部函数
  4. extern int Add(int x, int y);
  5. //声明外部的全局变量
  6. extern int g_val;
  7. int main()
  8. {
  9.         int a = 10;
  10.         int b = 20;
  11.         int sum = Add(a, b);
  12.         printf("%d\n", sum);
  13.         return 0;
  14. }
  15. //add.c
  16. int g_val = 2022;
  17. int Add(int x, int y)
  18. {
  19.         return x + y;
  20. }
复制代码
我们已经知道,每个源文件都是单独颠末编译器处理惩罚天生对应的目标文件。
test.c颠末编译器处理惩罚天生 test.o
add.c 颠末编译器处理惩罚天生 add.o
我们在 test.c 的文件中使用了 add.c 文件中的 Add 函数和 g_val 变量。
我们在 test.c 文件中每一次使用 Add 函数和 g_val 的时间必须确切的知道 Add 和 g_val 的地点,但是由于每个文件是单独编译的,在编译器编译 test.c 的时间并不知道 Add 函数和 g_val变量的地点,以是临时把调用 Add 的指令的目标地点和 g_val 的地点搁置。
期待末了链接的时间由链接器根据引用的符号 Add 在其他模块中查找 Add 函数的地点,然后将 test.c 中全部引用到Add 的指令重新修正,让他们的目标地点为真正的 Add 函数的地点,对于全局变量 g_val 也是雷同的方法来修正地点。这个地点修正的过程也被叫做重定位。
我们非常简便的解说了一个C语言步伐是怎样编译和链接,到终极天生可实验步伐的过程,着实很多内部的细节无法睁开解说。
比如:目标文件的格式.elf,链接底层实现中的空间与地点分配,符号分析和重定位等,假如你有爱好,可以看《步伐员的自我修养》一书来详细相识。
3. 运行情况


  • 步伐必须载入内存中。在有操纵体系的情况中,一样平常这个由操纵体系完成。在独立的情况中,步伐的载入必须由手工安排,也大概是通过可实验代码置入只读内存来完成。
  • 步伐的实验便开始。接着便调用main函数。
  • 开始实验步伐代码。这个时间步伐将使用一个运行时堆栈,存储函数的局部变量和返回地点。步伐同时也可以使用静态内存,存储于静态内存中的变量在步伐的整个实验过程不停保存他们的值。
  • 克制步伐。正常克制main函数;也有大概是不测克制。
谢谢你的阅读,喜欢的话来个点赞收藏品评关注吧!
我会连续更新更多优质文章

回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表