C语言编译和链接从.c到.exe程序到底经历了什么很多刚开始学习 C 语言的小伙伴都会有一个疑问我们写的.c文件CPU 根本看不懂那它到底是怎么变成可以运行的.exe程序的答案就是预处理 → 编译 → 汇编 → 链接 → 运行本文详细梳理 C 语言程序从源代码到最终运行的完整过程帮助你彻底搞懂编译、汇编、链接以及多文件之间是如何协同工作的。文章目录C语言编译和链接从 .c 到 .exe程序到底经历了什么一、C语言程序的整体流程二、翻译环境和运行环境1. 翻译环境2. 运行环境三、第一阶段预处理四、预处理阶段主要做什么1. 展开宏五、#include 到底是什么六、条件编译七、删除注释八、为什么要查看 .i 文件九、第二阶段编译十、编译阶段到底做什么十一、什么是词法分析十二、什么是语法分析十三、什么是语义分析十四、第三阶段汇编十五、为什么有了 .o 文件还不能直接运行十六、什么是链接十七、为什么 test.c 可以调用 add.c 中的函数十八、声明和定义一定要分清声明定义十九、链接器如何找到 Add二十、什么是重定位二十一、链接主要做哪三件事情① 地址和空间分配② 符号决议③ 重定位二十二、完整分析一个多文件程序二十三、.c、.i、.s、.o 分别是什么二十四、GCC中的 -E、-S、-c-E-S-c二十五、编译器和链接器不是一回事编译器汇编器链接器二十六、运行环境1. 加载到内存2. 开始执行3. 使用运行时栈4. 程序结束二十七、整节课的核心知识点1. C语言程序的翻译过程2. 预处理主要处理什么3. 编译阶段做什么4. 汇编阶段做什么5. 链接阶段做什么二十八、最容易混淆的几个概念① #include 不等于链接② 声明不等于定义③ 编译不等于链接④ .o 文件不是最终程序二十九、用一句话理解整个过程三十、最后总结一、C语言程序的整体流程我们平时写的 C 语言代码本质上只是源代码计算机不能直接执行一个 C 语言程序从源代码到最终运行大致经历下面几个阶段C源代码 │ ▼ ① 预处理 │ ▼ .i文件 │ ▼ ② 编译 │ ▼ .s文件 │ ▼ ③ 汇编 │ ▼ .o / .obj │ │ ┌──────┴──────┐ │ │ 其他目标文件 链接库 │ │ └──────┬──────┘ ▼ ④ 链接 │ ▼ 可执行程序 │ ▼ 运行程序所以一定要记住C语言程序的翻译过程预处理 → 编译 → 汇编 → 链接课件中指出翻译环境由编译和链接两个大的过程组成而编译又可以进一步分成预处理编译汇编二、翻译环境和运行环境C语言中存在两个不同的环境1. 翻译环境翻译环境负责把源代码转换成可执行的机器指令也就是说我们写intmain(){inta10;return0;}计算机最终需要把它转换成机器能够执行的二进制指令这个过程就是翻译环境2. 运行环境程序被翻译成最终的可执行程序之后还需要真正运行运行环境负责可执行程序 ↓ 加载到内存 ↓ 开始执行 ↓ 调用 main 函数 ↓ 执行程序代码 ↓ 程序结束所以可以简单理解翻译环境 把程序“造出来” 运行环境 把程序“跑起来”三、第一阶段预处理预处理也叫预编译假设我们有一个文件test.c经过预处理之后test.c ↓ 预处理 ↓ test.i使用 GCC 可以通过下面的命令观察预处理结果gcc-Etest.c-otest.i四、预处理阶段主要做什么预处理主要处理以#开头的预处理指令例如#includestdio.h#defineMAX100常见的预处理指令包括#include #define #if #ifdef #elif #else #endif #pragma1. 展开宏例如#defineMAX100intaMAX;经过预处理后可以理解成inta100;也就是说#define MAX 100 ↓ MAX ↓ 100预处理阶段会展开宏定义五、#include到底是什么我们经常写#includestdio.h那么#include到底干了什么从预处理阶段来看可以简单理解为把头文件的内容插入到#include所在的位置例如#includetest.hintmain(){return0;}如果test.h中有intAdd(intx,inty);那么经过预处理后可以理解为intAdd(intx,inty);intmain(){return0;}而且这个包含过程是可以递归进行的test.c ↓ #include test.h ↓ test.h ↓ #include xxx.h ↓ xxx.h所以#include可以简单理解为把头文件的内容包含进当前源文件六、条件编译预处理阶段还会处理条件编译例如#ifdefDEBUGprintf(debug);#endif或者#if1printf(hello);#elseprintf(world);#endif相关指令包括#if #ifdef #elif #else #endif预处理器会根据条件决定哪些代码参与后续编译七、删除注释例如// 这是一个注释intmain(){/* 这也是注释 */return0;}预处理阶段会删除注释所以源代码 ↓ 预处理 ↓ 注释被删除八、为什么要查看.i文件如果我们不知道宏到底有没有正确展开头文件到底有没有正确包含条件编译到底保留了哪些代码可以生成.i文件进行查看gcc-Etest.c-otest.i然后打开test.i就可以看到预处理之后的结果所以可以记住.i文件就是预处理之后的结果九、第二阶段编译预处理完成之后test.c ↓ 预处理 ↓ test.i接下来进入test.i ↓ 编译 ↓ test.sGCC 命令gcc-Stest.i-otest.s十、编译阶段到底做什么编译阶段主要会进行词法分析 ↓ 语法分析 ↓ 语义分析 ↓ 优化 ↓ 生成汇编代码最终得到.s也就是汇编代码文件十一、什么是词法分析假设我们有array[index](index4)*(26);词法分析首先要做的事情就是把源代码中的字符划分成一个个有意义的记号Token例如可以拆分成array [ index ] ( index 4 ) * ( 2 6 ) ;这些一个个的元素就是记号所以词法分析可以理解为“把代码切成一个个单词”十二、什么是语法分析词法分析完成之后编译器知道array [ index ] ...这些是什么。但是还需要判断这些东西组合起来是否符合 C 语言的语法规则这就是语法分析例如ab*c编译器需要知道运算的结构而不是简单地从左到右理解可以表示成一棵语法树 / \ a * / \ b c也就是a (b * c)课件中指出语法分析器会根据词法分析得到的记号进行分析并生成语法树所以语法分析就是分析代码的结构十三、什么是语义分析语法正确并不代表代码的含义一定正确例如inta;a();从代码结构来看形式上可能可以进行分析但是a是一个int类型变量不是函数所以a();在语义上存在问题这就是语义分析要解决的问题语义分析主要涉及声明和类型是否匹配类型转换表达式含义是否合理静态语义分析所以可以用一句话区分词法看“单词” 语法看“结构” 语义看“意义”这是非常重要的一个知识点十四、第三阶段汇编编译阶段结束之后test.i ↓ 编译 ↓ test.s现在得到的是汇编代码但是 CPU 最终需要执行的是机器指令所以接下来需要进行汇编代码 ↓ 汇编器 ↓ 机器指令例如gcc-ctest.s-otest.o最终得到目标文件test.o在 Windows 环境下目标文件通常使用.obj在 Linux 环境下通常使用.o所以.s ↓ 汇编 ↓ .o / .obj十五、为什么有了.o文件还不能直接运行这是这节课最重要的问题之一假设我们的项目中有两个.c文件test.c add.c其中test.c使用了Add(a,b);但是Add函数真正的定义在add.c例如intAdd(intx,inty){returnxy;}那么test.c和add.c是两个不同的源文件它们会被分别编译最终得到test.c → test.o add.c → add.o此时问题出现了test.o里面需要调用Add但是Add在add.o里面所以这两个目标文件需要被组合起来这就是链接十六、什么是链接链接就是把多个目标文件、链接库等组合起来最终生成可执行程序例如test.o \ \ → 链接器 → test.exe / / add.o实际项目可能更加复杂main.o student.o teacher.o math.o 标准库 第三方库 ↓ 链接器 ↓ 可执行程序所以链接主要解决多文件、多模块之间互相调用的问题十七、为什么test.c可以调用add.c中的函数例如externintAdd(intx,inty);这里的extern表示Add 函数存在但是它的定义不在当前源文件中然后intmain(){intretAdd(10,20);}编译器在编译test.c的时候知道Add 是一个函数 参数有两个 int 返回值是 int但是编译器并不知道Add 最终会被放到内存中的哪个地址因为Add的定义在另一个文件add.c而test.c是单独编译的所以这个问题要留给链接器来解决。十八、声明和定义一定要分清这是学习 C 语言非常重要的概念例如externintg_val;这是声明意思是g_val 存在但是intg_val2022;才是定义也就是说声明 告诉编译器“它存在” 定义 真正创建这个东西函数也是一样声明externintAdd(intx,inty);定义intAdd(intx,inty){returnxy;}十九、链接器如何找到Add假设test.o需要Add而add.o提供Add那么链接器就会把它们对应起来test.o │ │ 需要 Add ▼ 链接器 ▲ │ 提供 Add │ add.o这里的Add g_val都可以称为符号所以可以简单理解test.o 我要 Add 我要 g_val add.o 我有 Add 我有 g_val链接器Add ───────→ Add g_val ───────→ g_val这就是符号决议二十、什么是重定位找到Add之后还存在一个问题Add最终到底位于什么地址编译test.c的时候Add最终放在哪里还不知道所以编译器只能先把相关位置留下来等到链接阶段链接器把整个程序的空间安排好之后Add → 某个具体地址然后再修改之前留下的位置这个过程就叫重定位Relocation课件中也指出链接器找到其他模块中的Add和g_val后会对引用它们的位置进行修正这个地址修正过程就是重定位二十一、链接主要做哪三件事情课件中给出了三个重要概念1. 地址和空间分配 2. 符号决议 3. 重定位可以简单理解成① 地址和空间分配决定每个东西最终放在哪里② 符号决议找到你使用的符号到底对应哪个定义例如Add ↓ 找到 add.o 中的 Add③ 重定位把之前不知道的地址修改成最终确定的地址二十二、完整分析一个多文件程序假设我们有test.c add.c其中test.c调用Add(a,b);而add.c定义intAdd(intx,inty){returnxy;}整个过程就是test.c │ ▼ 预处理 │ ▼ test.i │ ▼ 编译 │ ▼ test.s │ ▼ 汇编 │ ▼ test.o │ │ │ ▼ 链接 ▲ │ │ add.o ▲ │ 汇编 ▲ │ 编译 ▲ │ add.i ▲ │ 预处理 ▲ │ add.c最终test.o add.o 链接库 ↓ 链接器 ↓ 可执行程序二十三、.c、.i、.s、.o分别是什么这个表格建议直接记下来文件含义.cC语言源代码.h头文件通常包含声明、宏等.i预处理后的文件.s汇编代码文件.oLinux等环境下的目标文件.objWindows环境下常见的目标文件.exeWindows环境下常见的可执行程序完整流程.c ↓ 预处理 ↓ .i ↓ 编译 ↓ .s ↓ 汇编 ↓ .o / .obj ↓ 链接 ↓ 可执行程序二十四、GCC中的-E、-S、-c这三个参数非常重要-Egcc-Etest.c-otest.i作用只进行预处理.c → .i-Sgcc-Stest.i-otest.s作用进行编译生成汇编代码.i → .s-cgcc-ctest.s-otest.o作用进行汇编生成目标文件.s → .o可以直接记-E预处理 -S编译 -c汇编二十五、编译器和链接器不是一回事很多初学者容易把它们混在一起实际上编译器主要负责C代码 ↓ 汇编代码过程中会进行词法分析 语法分析 语义分析 优化汇编器负责汇编代码 ↓ 机器指令链接器负责多个目标文件 各种链接库 ↓ 可执行程序所以编译器 ≠ 链接器虽然我们平时可能只输入一条gcc test.c但实际上编译器驱动程序会帮我们完成多个阶段二十六、运行环境当链接完成以后终于得到了可执行程序例如test.exe接下来进入运行环境1. 加载到内存程序首先需要被加载到内存可以简单理解为硬盘 ↓ 可执行程序 ↓ 内存在有操作系统的环境中一般由操作系统负责程序的加载2. 开始执行程序加载完成后开始执行接下来程序启动 ↓ 调用 main()3. 使用运行时栈程序执行函数时需要使用栈空间保存局部变量返回地址函数调用相关信息例如intmain(){inta10;return0;}这里的局部变量a就涉及函数运行时的存储空间4. 程序结束正常情况下return0;程序结束当然程序也可能因为其他原因异常终止二十七、整节课的核心知识点如果只记住下面这些内容基本就抓住了本节课的主线1. C语言程序的翻译过程预处理 ↓ 编译 ↓ 汇编 ↓ 链接 ↓ 可执行程序2. 预处理主要处理什么#include #define #if #ifdef #elif #else #endif同时还会展开宏 处理条件编译 包含头文件 删除注释 添加相关标识信息3. 编译阶段做什么词法分析 ↓ 语法分析 ↓ 语义分析 ↓ 优化 ↓ 生成汇编代码4. 汇编阶段做什么汇编代码 ↓ 机器指令生成.o / .obj5. 链接阶段做什么多个目标文件 链接库 ↓ 链接 ↓ 可执行程序主要涉及地址和空间分配 符号决议 重定位二十八、最容易混淆的几个概念①#include不等于链接#include发生在预处理阶段可以简单理解为把头文件内容包含进来而链接发生在后面链接阶段主要解决不同目标文件之间的符号引用② 声明不等于定义例如externintg_val;是声明而intg_val2022;是定义③ 编译不等于链接编译主要处理C代码 ↓ 汇编代码链接主要处理目标文件 库 ↓ 可执行程序④.o文件不是最终程序.o是目标文件它还可能存在符号没有最终确定 地址没有最终确定需要经过链接之后才能得到最终的可执行程序二十九、用一句话理解整个过程如果让我把整节课压缩成一句话我们写的.c文件首先经过预处理、编译和汇编分别变成.i、.s和.o然后链接器把多个目标文件和链接库组合起来完成符号决议和重定位最终生成可执行程序然后程序进入运行环境可执行程序 ↓ 加载到内存 ↓ 调用 main ↓ 执行代码 ↓ 程序结束三十、最后总结整个 C 语言程序的生命周期可以浓缩成下面这张图C源代码 │ ▼ 【预处理】 #include / #define 条件编译 / 删除注释 │ ▼ .i │ ▼ 【编译】 词法 / 语法 / 语义 优化 │ ▼ .s │ ▼ 【汇编】 汇编 → 机器码 │ ▼ .o/.obj │ │ ┌───────────┴───────────┐ │ │ 其他目标文件 链接库 │ │ └───────────┬───────────┘ ▼ 【链接】 地址和空间分配 符号决议 重定位 │ ▼ 可执行程序 │ ▼ 【运行】 │ ▼ 加载到内存 │ ▼ main() │ ▼ 执行代码 │ ▼ 程序结束最后记住这句口诀预处理看#编译看语法汇编变机器码链接找符号重定位修地址。搞懂这条主线之后后面学习extern头文件多文件编程静态库动态库符号表ELF链接错误都会容易很多如果这篇文章对你有帮助欢迎点赞、收藏、关注C语言学习路漫漫一起把基础打牢