在21世纪的我用C语言探寻世界本质 ——编译和链接(编译环境和运行环境)

人无完人,持之以恒,方能见真我!!!
共同进步!!

文章目录

一、翻译环境和运行环境

在 ANSI C 的任何⼀种实现中,存在两个不同的环境,如下:

  1. 翻译环境:在翻译环境中,会通过编译和链接两个大步骤,其中编译又分为了预处理(预编译)、编译和汇编,将源代码转换为可执⾏的机器指令(⼆进制指令),生成可执行程序
  2. 运行环境:即执行环境,在运行环境中会执行可执行程序,并输出结果

二、翻译环境

上面讲到了,翻译环境是用来将源代码转换为可执⾏的机器指令(⼆进制指令),生成可执行程序的,那么它到底是怎么将源代码转换成可执行的机器指令,又是怎么把机器指令生成可执行程序呢?我们一起来学习一下
翻译环境是由编译和链接两个⼤的过程组成的,⽽编译⼜可以分解成:预处理(有些书也叫预编译)、编译、汇编三个过程,接下来我们就来学习它们

1.编译

编译要完成的就是将我们的源代码转换成可执行的机器指令

编译器就可以实现编译的功能,也就包括了预处理,编译,汇编这三个操作,我们学习编译也就是学习这三个操作的过程
由于VS是一个高度集成的开发环境,它已经把编译这样的细节隐藏起来了,在VS中,我们只需要按下ctrl+F5,那么VS就会一下就帮我们把编译、链接和执行这三个动作一起完成了,瞬间就可以看到结果
所以在VS中我们无法看到.c的源文件编译和链接的完整过程,这个时候我们就可以借助其它的编译器,在下文中就是以gcc为例进行整个编译链接的讲解

预处理

预处理又称预编译,在预处理阶段,后缀为.c的文件将会被处理为.i的文件,如test.c经过预处理后就会变成test.i
预处理阶段的主要工作:

  1. 将所有的 #define 删除,并展开所有的宏定义,比如使用宏定义了一个常量,我们一般会这样写:
c 复制代码
//使用宏定义了一个常量
#define N 100
//使用宏
int arr[N];

那么经过预处理之后,#define N 100这条语句就会被删除,并且这个宏定义将会被展开,在这里就是将所有N替换成100

c 复制代码
//预处理后,宏定义语句被删除
//展开宏定义,在这里就是将N替换成100
int arr[100];
  1. 处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif等等
  2. 处理#include 预编译指令,将包含的头⽂件的内容插⼊到该预编译指令的位置,比如包含头文件stdio.h,那么就会将头文件stdio.h中的所有内容插入到原位置
    虽然VS不会生成.i的文件,但是我们还是可以看到头文件的内容,首先使用#include包含stdio.h,然后使用ctrl+单击鼠标,就可以看到stdio.h这个头文件的内容,有两千多行的代码,在预处理后,就会全部插入到我们的源文件中来
  3. 经过预处理后,会删除所有的注释
  4. 添加⾏号和⽂件名标识,⽅便后续编译器⽣成调试信息等
  5. 保留所有的#pragma的编译器指令,它可以保证我们不重复包含头文件
    经过预处理后的 .i ⽂件中不再包含宏定义,因为宏已经被展开,并且包含的头⽂件都被插⼊到 .i⽂件中,当我们⽆法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的 .i ⽂件来确认

编译

当我们进行预处理后,就来到了编译阶段,编译过程就是将预处理后的⽂件进⾏⼀系列的:词法分析、语法分析、语义分析及优化,⽣成相应的汇编代码⽂件
我们现在就用一句代码为例,看看词法分析、语法分析、语义分析的大致实现思路,如下:

c 复制代码
arr[index] = (index+4)*(2+6);
  1. 词法分析:首先代码就来到了词法分析,这个阶段会将源代码程序输⼊扫描器,扫描器的任务就是简单的进⾏词法分析,把代码中的字符分割成⼀系列的记号(关键字、标识符、字⾯量、特殊字符等)
    上⾯程序进⾏词法分析后得到了16个记号,然后就会生成一个符号表

在这个阶段会简单的标记每个记号,然后生成一个符号表,这个符号表在后面链接的地方还会用到
语法分析:来到语法分析阶段后,会将源代码放入语法分析器,将对扫描产⽣的记号进⾏语法分析,从⽽产⽣语法树,这些语法树是以表达式为节点的树,如图:

在这个阶段,我们代码的意思基本上就明确了,相当于在语法分析阶段,会把要组合的记号组合起来,明确这些记号的基本含义
如上图,这颗语法树的节点是一个又一个的表达式组成,如果在这个时候出现简单的语法错误就可以发现,比如少写一个括号或者符号,就不能像这样构成一个以表达式为节点的语法树,程序就可能会报错
语义分析:经过词法分析和语法分析后,由语义分析器来完成语义分析,即对表达式的具体语法层⾯的分析
编译器所能做的分析是语义的静态分析,静态语义分析通常包括声明和类型的匹配,类型的转换等,这个阶段会报告错误的语法信息,如下图:

在语义分析这个阶段就能基本明确程序的语法含义了,如明确了类型、类型的转换等信息,而语法分析中只是对记号表中的记号进行了组合和简单的翻译
在这个阶段已经可以判断表达式之间的关系了,比如 整型 加 整型 是 整型,整型 赋值给 整型 等等,并且在这个阶段可以找出语法错误
比如在赋值表达式的左边算出的结果是一个浮点型,而左边算出来了的却是一个整型,那么就会进行强制类型转换,如果表达式左边是整型,而右边是结构体,就会报错
而且最关键的一点是,我们通过语义分析已经知道了代码的含义,那么把它翻译成汇编代码也不是难事了,所以在这个阶段会正在将源代码翻译成汇编代码,并做相关的优化

汇编

经过预处理和编译阶段后就来到了汇编阶段,在编译阶段的语义分析我们就把源代码翻译成了汇编代码,而在汇编阶段做的事情就是将翻译过来的汇编代码再次转换为计算机可以识别的机器指令(二进制指令)
在翻译时,每⼀个汇编语句⼏乎都对应⼀条机器指令,在翻译期间也不会对代码做什么优化,只是根据汇编指令和机器指令的对照表⼀⼀的进⾏翻译,最终将汇编代码转换成了机器指令,生成一个.obj的目标文件

2.链接

链接是⼀个复杂的过程,链接的时候需要把⼀堆⽂件链接在⼀起才⽣成可执⾏程序,链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤,那么我们为什么要使用链接这一个步骤呢?
链接解决的是⼀个项⽬中多⽂件、多模块之间互相调⽤的问题,比如在一个项目中有两个.c文件
add.c:

c 复制代码
//定义一个函数add
int add(int x, int y)
{
	return x + y;
}

test.c

c 复制代码
#include <stdio.h>

int main()
{
	int a = 3;
	int b = 2;
	int ret = add(a, b);
	printf("%d\n", ret);
	return 0;
}

当我们实现好这两个文件后,但是直接运行后发现出现了错误,我们来看看具体报错:

它说函数add未定义,a是没有声明的标识符,这是为什么呢?这就要涉及到链接了,我们在编译阶段会将我们的源代码翻译成机器指令,生成后缀名为.obj的目标文件,但是我们要注意的是,编译是针对于单个文件的
什么意思呢?就是一个.c的文件生成一个.obj的目标文件,如果有多个.c的文件则生成多个.obj的文件,它们之间互不影响,所以如果我们想要一个文件中的某个函数在另一个文件中使用就做不到,我们可以画图理解,如图:

从图片中我们很明显地看出来了,多个.c文件生成多个.obj文件,之间互不影响,所以如果我们要让一个文件中的某个函数在另一个文件中使用就必须通过链接来完成,接下来我们就一起来学习链接的过程
在链接过程中,需要用到之前我们在词法分析时生成的符号表,将那些特殊记号记录下来,但是链接时的符号表则更为复杂,会有导出符号表、未解决符号表和地址重定向表三个表,这里我们就简单将它们合并一下,用一个表把类似的原理讲一讲,等后期会出详细的链接过程
首先我们要知道符号的类型有哪些,如下:

  1. 全局符号(Global symbols):由当前模块定义并能被其他模块引用的符号(指不带static的全局变量)
  2. 外部符号(External symbols):由其他模块定义,并能被当前引用的全局符号
  3. 局部符号(Local symbols):仅由当前模块定义和引用的本地符号。例如,定义的static函数和变量
    接下来我们可以简单的画出add.c中的符号表,在画的时候我们要注意,在add.c中并没有外部符号的引用,出现的符号add拥有正常的声明和定义,所以会直接分配一个虚拟地址,如图:

接下来我们就来画test.c的符号表,为了进行对比,在test.c的符号表中我们也只画出只有add符号的符号表,其它符号就省略掉,如下图:

可以对比看出来,在add.c中,add符号被看做全局符号,拥有自己的地址,但是在test.c中,add符号被看做外部符号,只有一段可能错误的地址(不一定就是0地址)
链接中关键的就是这一步,由于test.c不认识这个符号所以要报错,为了能够正常链接,我们需要做的就是:在test.c中使用extern关键字对add符号进行声明,然后链接器就会知道,这个符号在其它文
然后最后为了修正test.c中add符号的地址,使它正确,会进行重定位操作,重定位会计算每个定义的符号在虚拟地址空间的绝对地址,将可执行文件中的符号引用处修改为重定位后的地址信息
将符号表修正后,test.c文件的符号表中的add符号的地址就会修正为正确的地址,test.c文件就可以通过这个地址来访问add函数,这就是链接中的重定位

三、运行环境

在运行环境中有几个要点,我们只需要简单了解一下:

  1. 程序首先必须载⼊内存中。在有操作系统的环境中:⼀般这个由操作系统完成。在独⽴的环境中,程序的载⼊必须由⼿⼯安排,也可能是通过可执⾏代码置⼊只读内存来完成
  2. 随后程序开始执行。会直接调⽤main函数
  3. 开始执⾏程序代码。这个时候程序将使⽤⼀个运⾏时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使⽤静态(static)内存,存储于静态内存中的变量在程序的整个执⾏过程⼀直保留他们的值
  4. 终⽌程序。正常终⽌main函数;也有可能是意外终⽌
    到这里,编译和链接就简单的介绍完了,C语言也快要结尾了,希大家继续努力
相关推荐
十年一梦实验室7 分钟前
【C++】sophus : rxso3.hpp 实现了 3D 空间中的旋转和缩放操作的 RxSO3 类 (二十一)
开发语言·c++·人工智能·算法·3d
爱吃土豆的马铃薯ㅤㅤㅤㅤㅤㅤㅤㅤㅤ14 分钟前
MyBatis执行完sql后,返回的数值代表的意思
java·开发语言
yoyo勰20 分钟前
sqlite3
c语言·sqlite
西岸行者20 分钟前
捋一捋相关性运算,以及DTD和NLP中的应用
人工智能·算法·自然语言处理·信号处理
小爬虫程序猿28 分钟前
如何利用Python爬虫精准获取苏宁易购商品详情
开发语言·爬虫·python
API快乐传递者29 分钟前
Python爬虫获取1688详情接口详细解析
开发语言·爬虫·python
Fan_55832 分钟前
008 Qt_显示类控件_QLabel
开发语言·qt
No0d1es37 分钟前
GESP CCF C++六级编程等级考试认证真题 2024年12月
开发语言·c++·算法·青少年编程·gesp·ccf·六级
IT199540 分钟前
Qt笔记-Qt Creator开发环境搭建
开发语言·笔记·qt
编码小哥1 小时前
C++信号处理
c++