C语言梦开始的地方21:编译和链接

发布时间:2026/9/4 17:23:04
C语言梦开始的地方21:编译和链接 文章目录C语言梦开始的地方21编译和链接翻译环境和运行环境翻译环境预处理编译词法分析语法分析语义分析汇编链接运行环境C语言梦开始的地方21编译和链接上一章我们了解了C语言的文件操作系统的数据文件和对数据的文件的各种操作。本章章节我们来了解一下C语言的.c文件是如何变成.exe文件的。翻译环境和运行环境在 ANSI C 的任何一种实现中存在两个不同的环境。第1种是翻译环境在这个环境中源代码被转换为可执行的机器指令二进制指令。第2种是执行环境它用于实际执行代码。那么翻译环境中的编译和链接做了哪些操作才使.c文件转换成了.exe文件呢翻译环境首先我们就得展开开讲解一下翻译环境所做的事情。其实翻译环境是由编译和链接两个大的过程组成的而编译又可以分解成预处理有些书也叫预编译、编译、汇编三个过程。一个C语言项目中可能存在多个.c文件一起构建那多个.c文件是如何生成可执行程序呢多个.c文件单独经过编译器编译处理生成对应的目标文件。注在Windows环境下的目标文件的后缀是.objLinux环境下目标文件的后缀是.o多个目标文件和链接库一起经过链接器处理生成最终的可执行程序。链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库。通过上述的步骤才使.c文件构建成了我们能运行的.exe文件以GCC为例就是下面这种场景下面会出现一些gcc的命令使用这些命令并不需要去Linux上。我们只需要去安装一下VSCode和mingW即可。在VSCode中要安装一个C/C的插件。安装好后就可以编写和运行C/C代码了预处理在预处理阶段源文件和头文件会被处理成为.i为后缀的文件。在gcc环境下想观察一下对test.c文件预处理后的.i文件命令如下gcc-Etest.c-otest.i使用也很简单在VSCode中点击Ctrl 即可预处理阶段主要处理那些源文件中#开始的预编译指令。比如#include,#define处理的规则如下将所有的#define删除并展开所有的宏定义。处理所有的条件编译指令如#if、#ifdef、#elif、#else、#endif。处理#include 预编译指令将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的也就是说被包含的头文件也可能包含其他文件。删除所有的注释添加行号和文件名标识方便后续编译器生成调试信息等。或保留所有的#pragma的编译器指令编译器后续会使用。经过预处理后的.i文件中不再包含宏定义因为宏已经被展开。并且包含的头文件都被插入到.i文件中。所以当我们无法知道宏定义或者头文件是否包含正确的时候可以查看预处理后的.i文件来确认。编译编译过程就是将预处理后的文件进行一系列的词法分析、语法分析、语义分析及优化生成相应的汇编代码文件。编译过程的命令如下gcc-Stest.i-otest.s对下面代码进行编译的时候会怎么做呢假设有下面的代码array[index](index4)*(26);词法分析首先我们得先了解词法分析分析的是什么词法分析就像扫描器将源代码程序输入扫描器扫描器的任务就是简单的进行词法分析把代码中的字符分割成一系列的记号关键字、标识符、字面量、特殊字符等。比如将英文符号写成了中文符号“”inta10此时词法分析就像扫描器一样将int a 10进行分割 校验。回到我们最开始的代码问题上面程序进行词法分析后得到了16个记号记号类型array标识符[左方括号index标识符]右方括号赋值(左圆括号index标识符加号4数字)右圆括号*乘号(左圆括号2数字加号6数字)右圆括号常见的报错原因中文标点符号如中文分号、中文引号“”混入代码。字符串或字符常量缺少闭合引号如 char *s “hello;此时扫描器会一直扫描到文件末尾都找不到”报错 missing terminating character。语法分析语法分析则像解析器Parser根据语法规则检查记号流时发现结构不符合C语言的文法规则即语句结构写错了。上述代码在语法分析就如下图所示常见报错原因通常是大括号、括号、分号不匹配或者关键字使用位置错误。语义分析语义分析语法正确但语句的含义不符合语言规范。语义分析器Semantic Analyzer会检查类型系统、声明匹配、作用域规则等。常见报错原因类型不兼容、使用了未声明的变量/函数、重复定义、赋值类型冲突等。汇编汇编器是将汇编代码转转变成机器可执行的指令每一个汇编语句几乎都对应一条机器指令。就是根据汇编指令和机器指令的对照表一一的进行翻译也不做指令优化。gcc-ctest.s-otest.o汇编一般有一下用途读汇编是排查“挂死”和“栈溢出”的。写出“高性能”代码。比如操作是否是原子的理解“未定义行为”的诡异后果下面我们来简单的读一下C语言转汇编代码intmain(){inta10;intb20;intcab;returnc;}转换后的汇编代码.file test.c .text .globl main .def main; .scl 2; .type 32; .endef .seh_proc main main: pushq %rbp .seh_pushreg %rbp movq %rsp, %rbp .seh_setframe %rbp, 0 subq $48, %rsp .seh_stackalloc 48 .seh_endprologue call __main movl $10, -4(%rbp) movl $20, -8(%rbp) movl -4(%rbp), %edx movl -8(%rbp), %eax addl %edx, %eax movl %eax, -12(%rbp) movl -12(%rbp), %eax addq $48, %rsp popq %rbp ret .seh_endproc .def __main; .scl 2; .type 32; .endef .ident GCC: (Rev2, Built by MSYS2 project) 16.1.0看汇编通常是看中间汇编代码比如上述代码就是从call __main开始阅读movl $10, -4(%rbp) # 将 10 存入 rbp-4 地址变量 a movl $20, -8(%rbp) # 将 20 存入 rbp-8 地址变量 b movl -4(%rbp), %edx # 把 arbp-4的值放入 edx 寄存器 movl -8(%rbp), %eax # 把 brbp-8的值放入 eax 寄存器 addl %edx, %eax # eax edx eax 即 eax a b movl %eax, -12(%rbp) # 将 eax 的结果存回 rbp-12变量 c movl -12(%rbp), %eax # 将 c 的值放入 eax因为 eax 是返回值寄存器 addq $48, %rsp # 释放之前分配的 48 字节栈空间 popq %rbp # 恢复调用者的 rbp ret # 返回因为在不同的环境会得出不一样的汇编代码上述是在Windows 11的环境下使用VSCode运行出来的如果换成Linux那么汇编也会有所不同。链接链接是一个复杂的过程链接的时候需要把一堆文件链接在一起才生成可执行程序。链接过程主要包括地址和空间分配符号决议和重定位等这些步骤。链接解决的是一个项目中多文件、多模块之间互相调用的问题比如在一个C的项目中有2个.c文件test.c和add.c代码如下test.c#includestdio.hexternintAdd(intx,inty);externintg_val;intmain(){intx10,y20;printf(%d\n,Add(x,y));return0;}add.cintg_val2022;intAdd(intx,inty){returnxy;}test.c中调用了Add函数和使用了g_val变量但它们定义在add.c中。编译test.c时编译器不知道Add和g_val的地址因此暂时将这些引用搁置生成未解析的符号。链接时链接器在所有目标文件test.o和add.o及库中查找这些符号的定义找到后将test.o中所有引用Add和g_val的指令地址修正为真正的内存地址而这个地址修正的过程也被叫做重定位。gcc环境gcc test.o add.o-otestmsvc环境直接点击运行即可。运行环境载入内存有操作系统时由操作系统载入独立环境如嵌入式需手工安排或固化到ROM。程序启动调用 main 函数。运行时资源堆栈stack存储局部变量、函数返回地址等。静态内存static存储全局变量和静态变量程序运行期间一直保留。终止正常执行完毕main 返回或意外终止。我们在函数栈帧中再去了解运行环境。完