【C语言】编译和链接

前言

编译和链接的内容非常复杂深奥,这里只是笼统的讲解。

翻译环境和运行环境

在ANSI C的任何一种实现中,存在两个不同的环境。

第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令(二进制指令)。

第2种是执行环境,它用于实际执行代码。

怎么理解? C语言的代码是文本信息,比如#include<stdio.h>,本质是字母组成的文本信息。要给计算机下达指令去执行,计算机只能识别二进制信息

就像现实中一个日本人只懂日语,一个韩国人只懂韩语,他们在交流时就需要翻译。这个翻译可以将日语翻译为韩语,也可以将韩语翻译为日语。

为了让计算机知道我们下达的是什么指令,需要将我们的代码翻译成二进制。这个过程我们依赖的环境就是翻译环境。在这个环境中源代码会被转换成可执行的机器指令。(二进制指令)

执行环境就在我们的计算机上。

每一个.c文件都要经过编译处理生成自己的目标文件,目标文件经过链接生成可执行程序,然后运行出来。可执行程序就是我们看到的**.exe**文件。

翻译环境

翻译环境是怎么将源代码转换为可执行的机器指令的呢?为了了解这一点,就展开讲一下翻译环境所做的事:

翻译环境是由编译和链接两个大的过程组成的,而++编译又可以分解为:预处理(或预编译)、编译、汇编三个过程。++

.obj 结尾的就是目标文件。 从test.c经过编译器处理生成.obj文件的过程就是编译过程,之后就是链接过程。每个.c文件都会单独经过编译器处理。

在vs中, 编译器叫cl.exe,链接器叫link.exe。在Everything中可以找到:

测试每个.c文件生成目标文件:

运行之后来到test_6_6>test_6_6>x64>Debug文件夹,发现确实多出了test.obj与add.obj两个目标文件。

这些目标文件再经过链接处理,生成可执行程序:来到test_6_6>x64>Debug文件夹,发现有.exe文件生成。

像visual studio这样的IDE,将这些细节隐藏了起来。所以我们不会去关注编译、链接等细节。

注意:在Windows环境下的目标文件的后缀是.obj,Linux环境下目标文件的后缀是.o。

多个目标文件和链接库一起经过链接器处理生成最终的可执行程序。

链接库是指运行时库(它是支持程序运行的基本函数集合)或者第三方库。

比如我们在使用printf这样的函数时,就需要链接库。

如果再把++编译器展开成3个过程++,就是下图的过程:

预处理

该阶段主要处理那些源文件中#开始的预编译指令。比如:#include,#define。处理的规则如下:

--将所有的#define删除,并展开所有的宏定义(替换)。

--处理所有的条件编译指令,如#if #ifdef #elif #else #endif

--处理#include预编译指令,将包含的头文件的内容插入到该预编译指令的位置。这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。

--删除所有的注释(变为空格)。

--添加行号和文件名标识,方便后续编译器生成调试信息等。

--保留所有的#pragma的编译器指令,编译器后续会使用。

总结下来都是一些++文本操作++。

经过预处理后的.i文件中不再包含宏定义,因为宏已经被展开,并且包含的头文件都被插入到.i文件中,所以当我们不知道宏定义或头文件是否包含正确时,可以查看预处理后的.i文件来确认。

编译

编译过程就是将预处理后的文件进行一系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。

词法分析

将源代码程序输入扫描器 ,扫描器的任务就是简单地进行词法分析,把代码分割成一系列的记号(关键字、标识符、字面量、特殊字符等)。

比如这段代码:

复制代码
 array[index] = (index+4)*(2+6);

进行词法分析后就是:

语法分析

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

语义分析

由语义分析器来完成语义分析,即对表达式的语法层面分析。编译器所能做的分析是语义的静态分析。静态语义分析通常包括声明和类型的匹配,类型的转换呢。这个阶段会报告错误的语法信息。

汇编

汇编器是将汇编代码转变成机器可执行的指令,每一个汇编语句几乎都对应一条机器指令。就是根据汇编指令和机器指令的++对照表++一一地进行翻译,也不做指令优化。

其实还会做一件事,就是形成一个符号表。也就是将全局的符号的名字汇总下来,举例:

对于这个.c文件,符号表中又Add 以及这个函数的地址0x100(假设的)。

而对于下面这个.c文件:

符号表中有main及其地址0x104(假设),Add及其地址0x000(无效地址)。局部变量是在程序运行起来才有的,这个阶段是根本没有a b c这些变量的。

最终完成的是将汇编代码翻译成二进制(机器)指令。最后生成目标文件,以.o为后缀。(每个.c文件都有一个)

链接

是一个相对复杂的过程,要将一堆文件链接在一起生成最后的可执行文件。

链接过程主要包括:地址和空间分配,符号决议和重定位这些步骤。

链接解决的是一个项目中多文件、多模块之间互相调用的问题。

记得吗,不同文件在链接后要生成++一个++ 可执行程序。所以在链接阶段,会有符号表的合并和重定位。回到上面两张图的例子,两个.c文件符号表里都有Add,首选哪个地址呢?刚才说了test.c里的Add的地址是无效地址,所以编译器决议后选择留下0x100地址。这就是合并,冲突时决议看保留哪个地址。这时生成的新的符号表就在可执行程序中。

当我们想要调用Add函数时就查这个表得到Add地址,就可以调用这个函数了。

假如这时我们在test.c中将Add误写为了add,那么在链接合并符号表时Add add地址都被保留,add的是无效地址,当我们调用时根据这个地址找不到我们要的函数。

所以,在外部文件中定义的全局变量,使用extern,也可以使用。

目标文件和可执行程序的格式其实是一样的,文件的组织形式是一样的,在Linux底下用的都叫elf的格式...

运行环境

(生成可执行程序后,要依赖运行环境将程序运行起来。这个过程其实也是比较复杂的,这里笼统地将。)

1.程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。(比如单片机的设备里面可能是没有操作系统的。)

2.程序的执行便开始,接着便调用main函数。

3.开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。

4.终止程序。正常终止main函数;也有可能是意外终止(断电了、程序卡死奔溃了等)。

(操作系统代劳)

到此,本文结束,祝阅读愉快^_^

相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨8 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区10 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai11 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜11 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa11 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio