
文章目录一、程序的两种环境二、翻译环境2.1 多文件项目的构建流程2.2 编译的三个步骤2.2.1 预处理预编译2.2.2 编译2.2.3 汇编2.3 链接三、运行环境一、程序的两种环境在ANSI C的任何一种实现中都存在两个不同的环境它们共同支撑着C语言程序的生命周期。翻译环境这是源代码被转换为可执行的机器指令二进制指令的地方。我们编写的.c源文件就是在这个环境中经历一系列处理最终变成能被计算机识别的二进制代码。执行环境顾名思义它是用于实际执行代码的环境。当翻译环境生成可执行程序后就会在执行环境中运行产生我们需要的结果。举个简单的例子假设有test1.c、test2.c、test3.c三个源文件它们会先在翻译环境中经过编译、链接等过程生成可执行程序然后这个可执行程序在运行环境中输出结果。二、翻译环境翻译环境的核心任务是将源代码转换为可执行的机器指令它由编译和链接两个大的过程组成而编译又可以进一步分解为预处理预编译、编译、汇编三个步骤。2.1 多文件项目的构建流程一个C语言项目往往包含多个.c文件这些文件共同协作才能完成特定的功能。那么多个.c文件是如何一步步生成可执行程序的呢多个.c文件会单独经过编译器的处理生成对应的目标文件。这里需要注意的是在Windows环境下目标文件的后缀是.obj而在Linux环境下目标文件的后缀是.o。这些多个目标文件和链接库一起经过链接器的处理最终生成可执行程序。链接库指的是运行时库支持程序运行的基本函数集合或者第三方库。比如test.c经过编译器处理生成test.objadd.c经过编译器处理生成add.obj然后这些.obj文件与链接库在链接器的作用下生成可执行程序。Windows 环境下Linux环境下2.2 编译的三个步骤如果我们把编译器的工作展开会得到更细致的三个过程2.2.1 预处理预编译预处理阶段源文件.c为后缀和头文件.h为后缀会被处理成为以.i为后缀的中间文件。在gcc环境下我们可以使用如下命令观察对test.c文件预处理后的.i文件gcc -E test.c -o test.i预处理阶段主要处理源文件中以#开始的预编译指令具体规则如下将所有的#define删除并展开所有的宏定义。处理所有的条件编译指令如#if、#ifdef、#elif、#else、#endif。处理#include预编译指令将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的也就是说被包含的头文件也可能包含其他文件。删除所有的注释。添加行号和文件名标识方便后续编译器生成调试信息等。保留所有的#pragma的编译器指令供编译器后续使用。经过预处理后的.i文件中不再包含宏定义因为宏已经被展开并且包含的头文件都被插入到.i文件中。所以当我们不确定宏定义或者头文件是否包含正确时可以查看预处理后的.i文件来确认。2.2.2 编译编译过程是将预处理后的.i文件进行一系列的词法分析、语法分析、语义分析及优化最终生成相应的汇编代码文件.s为后缀。在gcc环境下编译过程的命令如下gcc -S test.i -o test.s我们以一段简单的代码array[index] (index4)*(26);为例来看看编译过程具体做了什么。词法分析源代码程序被输入扫描器扫描器会把代码中的字符分割成一系列的记号这些记号包括关键字、标识符、字面量、特殊字符等。上面的程序经过词法分析后会得到16个记号比如array标识符、[左方括号、index标识符、]右方括号、赋值等。语法分析语法分析器会对扫描产生的记号进行语法分析从而产生语法树。这些语法树是以表达式为节点的树。对于上面的代码会生成以赋值表达式为根节点下标表达式和乘法表达式为子节点的语法树下标表达式又包含array标识符和index标识符乘法表达式则由两个加法表达式组成等。语义分析语义分析器完成语义分析即对表达式的语法层面进行分析。编译器所能做的是语义的静态分析通常包括声明和类型的匹配、类型的转换等。这个阶段会报告错误的语法信息。比如在上面的代码中会对每个标识符和表达式进行类型标识确认array是整型数组index是整型各个表达式的结果也是整型等。经过这些步骤后就会生成汇编代码文件。2.2.3 汇编汇编器的作用是将汇编代码.s为后缀转变为机器可执行的指令目标文件.o或.obj为后缀。每一个汇编语句几乎都对应一条机器指令汇编过程就是根据汇编指令和机器指令的对照表一一进行翻译不做指令优化。在gcc环境下汇编的命令如下gcc -c test.s -o test.o2.3 链接链接是一个复杂的过程它需要把多个目标文件和链接库链接在一起最终生成可执行程序。链接过程主要包括地址和空间分配、符号决议和重定位等步骤其解决的是一个项目中多文件、多模块之间互相调用的问题。我们以一个具体的例子来理解链接的作用。假设有test.c和add.c两个.c文件test.c中的代码如下#includestdio.h// 声明外部函数externintAdd(intx,inty);// 声明外部的全局变量externintg_val;intmain(){inta10;intb20;intsumAdd(a,b);printf(%d\n,sum);printf(g_val%d\n,g_val);return0;}add.c中的代码如下intg_val2022;intAdd(intx,inty){returnxy;}我们知道每个源文件都会单独经过编译器处理生成对应的目标文件即test.c生成test.oadd.c生成add.o。在test.c中我们使用了add.c中的Add函数和g_val变量。但在编译器编译test.c的时候它并不知道Add函数和g_val变量的地址所以会暂时把调用Add的指令的目标地址和g_val的地址搁置。直到最后链接的时候链接器会根据引用的符号Add在其他模块这里就是add.o中查找Add函数的地址然后将test.c中所有引用到Add的指令重新修正让它们的目标地址为真正的Add函数的地址对于全局变量g_val也是用类似的方法来修正地址。这个地址修正的过程就被叫做重定位。三、运行环境当可执行程序生成后就会进入运行环境执行。程序的运行过程大致如下程序载入内存在有操作系统的环境中一般由操作系统完成程序的载入在独立的环境中程序的载入必须由手工安排也可能是通过可执行代码置入只读内存来完成。开始执行程序程序执行便开始接着会调用main函数。执行程序代码此时程序将使用一个运行时堆栈stack存储函数的局部变量和返回地址。同时程序也可以使用静态static内存存储于静态内存中的变量在程序的整个执行过程中一直保留它们的值。程序终止程序可能正常终止于main函数也可能意外终止。