【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函数;也有可能是意外终止(断电了、程序卡死奔溃了等)。

(操作系统代劳)

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

相关推荐
秃头佛爷2 分钟前
Python学习大纲总结及注意事项
开发语言·python·学习
待磨的钝刨3 分钟前
【格式化查看JSON文件】coco的json文件内容都在一行如何按照json格式查看
开发语言·javascript·json
XiaoLeisj2 小时前
【JavaEE初阶 — 多线程】单例模式 & 指令重排序问题
java·开发语言·java-ee
励志成为嵌入式工程师3 小时前
c语言简单编程练习9
c语言·开发语言·算法·vim
捕鲸叉3 小时前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer4 小时前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq4 小时前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
记录成长java5 小时前
ServletContext,Cookie,HttpSession的使用
java·开发语言·servlet
前端青山5 小时前
Node.js-增强 API 安全性和性能优化
开发语言·前端·javascript·性能优化·前端框架·node.js
hikktn5 小时前
如何在 Rust 中实现内存安全:与 C/C++ 的对比分析
c语言·安全·rust