系列文章目录
第三章 开发工具的认识与使用
文章目录
- 系列文章目录
- 前言
- 一、程序的翻译
-
- [1.1 预处理](#1.1 预处理)
- [1.2 编译](#1.2 编译)
- [1.3 汇编](#1.3 汇编)
- [1.4 链接](#1.4 链接)
-
- [1.4.1 推荐使用 .o文件 链接原因](#1.4.1 推荐使用 .o文件 链接原因)
- [1.5 记忆技巧](#1.5 记忆技巧)
- 二、深入了解编译
-
- [2.1 编译使用的深入了解](#2.1 编译使用的深入了解)
-
- [2.1.1 编译本质与命令宏定义](#2.1.1 编译本质与命令宏定义)
- [2.1.2 条件编译的使用与意义](#2.1.2 条件编译的使用与意义)
- [2.2 编程语言](#2.2 编程语言)
-
- [2.2.1 编程语言的发展](#2.2.1 编程语言的发展)
- [2.2.2 c语言与汇编语言的交互](#2.2.2 c语言与汇编语言的交互)
- 三、库与链接
-
- [3.1 库的说明与使用](#3.1 库的说明与使用)
-
- [3.1.1 库是什么](#3.1.1 库是什么)
- [3.1.2 查看使用库](#3.1.2 查看使用库)
-
- [3.1.2.1 ldd 命令](#3.1.2.1 ldd 命令)
- [3.2 链接](#3.2 链接)
-
- [3.2.1 链接是什么](#3.2.1 链接是什么)
- [3.2.2 动态链接](#3.2.2 动态链接)
- [3.2.3 静态链接](#3.2.3 静态链接)
- [3.2.4 动/静 链接对比](#3.2.4 动/静 链接对比)
- [四、 库与链接的理论验证](#四、 库与链接的理论验证)
-
- [4.0 file 命令](#4.0 file 命令)
- [4.1 动态链接验证](#4.1 动态链接验证)
- [4.2 静态链接验证](#4.2 静态链接验证)
- [4.3 周边问题:C++的动/静 态链接情况](#4.3 周边问题:C++的动/静 态链接情况)
-
- [4.3.1 动态链接](#4.3.1 动态链接)
- [4.3.2 静态链接](#4.3.2 静态链接)
- [五、 库的内存加载和技术](#五、 库的内存加载和技术)
-
- [5.1 内存加载](#5.1 内存加载)
- [5.2 技术理解](#5.2 技术理解)
- 总结
前言
本节将围绕文件的编译过程和编译指令的使用展开,最终目的为理解文件如何编译以及熟练使用命令编译文件,接下来让我们开始今天的学习吧。
一、程序的翻译
一个写完的代码到可运行要经过的步骤即为------翻译。
程序翻译:是指将用高级语言、汇编语言等编写的源程序,等价转换为计算机可直接识别执行的目标程序或机器语言程序的过程。
而翻译又具体地分为四个过程: 预处理 、编译 、汇编 、链接。在Linux系统中这四个过程形成的的文件都是可以通过具体指令查看到。
1.1 预处理
预处理:编译之前,由预处理器对源程序进行文本替换、指令处理的前期处理阶段,不做语法检查。
预处理阶段会执行宏替换、条件编译等操作,预处理指令是以#号开头的代码行,对于不同的情况有不同的处理方法。
通过指令gcc --E code.c --o code.i,就可以形成中间文件

打开预处理文件后我们可以发现,它处理的就是#开头的代码处理方式如下:
- 宏定义: 宏替换,在源代码位置直接替换
- 条件编译: 如同if语句一般,但不符合条件的语句会直接删除
- 头文件: 对头文件展开,把头文件内的内容直接拷贝到目标文件当中。(Linux中储存着c语言的开发环境,在/usr/include/ 目录下)
- 注释: 删除注释内容

1.2 编译
编译:将预处理完成后的源代码,经过一系列语法、语义分析,整体翻译成机器语言目标代码的过程,由编译器完成。
编译文件可由源文件或预处理文件处理产生,将原来的文本语言翻译成 汇编语言 或 机器语言(二进制指令) 。
通过指令gcc --S code.i --o code.s,就可以形成中间文件

-S选项就是告诉系统执行完编译操作后就停止翻译,并将翻译的信息放入目标文件当中。
1.3 汇编
汇编:将编译器生成的汇编语言目标文件(.s),翻译成 纯二进制机器语言目标文件(.o/.obj)的过程,由汇编器(Assembler) 完成(比如 gas、nasm)。
汇编文件可由.c 到.s 文件任意一个文件处理形成,实现将**二进制指令(如汇编指令)**转化为 CPU 能直接执行的机器指令(二进制 / 十六进制编码)
通过指令gcc --c code.s --o code.o,就可以形成中间文件

此时的文件信息已经对我们来说是乱码了 (原因是将二进制信息进行了解析,如按照:ASCII/UTF-8 字符) ,c虽然已经是二进制文件了但还不能够执行,但还没有方法的实现,即将printf等函数的代码导入。

1.4 链接
链接:将汇编生成的二进制目标文件(.o),与系统库文件、其他目标文件合并,生成最终可直接运行的可执行文件的过程。
链接文件实现了将目标二进制文件与导入的头文件库的函数代码实现链接在一起,最终形成可执行程序。
通过指令gcc code.o --o code,就可以形成可执行文件,同时链接也可以多个.o文件操作,实现类如代码实现、头文件、测试三大部分的测试示例 。

进行链接后的文件就可以执行了,当然必须还需要x的执行权限,才能实现运行。
1.4.1 推荐使用 .o文件 链接原因
在实际学习开发时,我们会发现都是将源文件先翻译成 .o 文件,最后再链接成可执行文件,而不是直接链接成执行文件,这是为什么呢?有如下几个主要原因:
- 用途多元,适配不同产出 : 编译不只是为了生成可执行程序,还需要制作静态库、动态库;.o是打包库文件的基础原料,直接用.c无法灵活制作库文件。
- 分离编译,提升效率: 在处理多文件工程时,如果修改其中一个源文件只需把该文件重新汇编,而不需要全部重写链接,从而提升效率
- 保护源代码: 防止源文件暴露,保护代码的私密性
当使用vs时也会产生有此步骤,即产生 .obj文件
1.5 记忆技巧
综上,翻译的过程就是 使用 -ESc 选项,实现 .iso 的文件转换的过程。这个选项正是我们键盘上的"ESc"键盘,文件后缀符合了镜像文件的后缀
二、深入了解编译
2.1 编译使用的深入了解
2.1.1 编译本质与命令宏定义
我们会发现编译后的文件与源文件之间并没有非常大的差异,这是因为编译其实就是对源文件进行符合规则的增删和裁剪 。 除了我们在文件当中使用#define定义宏 ,我们还也可以直接通过编译时增添宏定义 ,操作通过-D选项实现,示例如下:

该操作等于在编译代码时将value的值插入我们的代码当中,当然宏的定义不要追加空格,否则会出现以下错误:

2.1.2 条件编译的使用与意义
从例子入手:我们通常使用的编译器分为社区版和企业版,这两者的功能不同,作为开发者需要维护两份源代码吗?其实不然,开发者通过条件编译区分用户的使用就完成了整个软件的修改。这个就是条件编译的使用,实现代码的动态裁剪,还有以下的作用:。
- 版本控制: 对于软件或系统具有不同的使用权,通过条件编译就可以实现对其的详细控制
- 环境适配: 实现统一系统在不同环境下的正常运行,通过动态裁剪不属于该环境的代码部分
2.2 编程语言
2.2.1 编程语言的发展
编程语言的发展是实现繁琐到便捷的体现,由最初的开关控制,到现在的c语言一系列语言,让我们了解期间的发展吧。
- 机器语言:二进制语言,通过机器能够直接识别的二进制制袋与计算机实现交互

- 汇编语言:通过英文助记符的方式,来对计算机进行交互,但需要汇编软件

- 高级语言: 分为面向过程和面向对象的两种语言,现在最好懂的语言
2.2.2 c语言与汇编语言的交互
作为最底层、运用最广泛的c语言,它本身是高级语言需要转为二进制语言,它当时无非两种路径:
直接制定规则转为二进制或通过转为汇编语言再使用汇编器转为二进制。
我们可想而知文本语言到文本语言 与 文本语言到二进制之间的难度差异,因此c语言选择了汇编的路子,但汇编语言是怎么转为二进制的呢?现在我们会说是汇编器,那汇编器是汇编语言写的又怎么转为计算机可懂的二进制呢?
这就陷入了先有鸡还是现有蛋的哲学问题了,其实它选择了二进制的方法
即选择先用二进制写出汇编器,再用汇编语言 + 二进制汇编器 做出 汇编语言汇编器,这样的行为有个名称就是------编译器的自举。
三、库与链接
3.1 库的说明与使用
3.1.1 库是什么
库:是预先编译完成、封装了各类通用功能程序代码的集合文件,可供源程序在链接阶段调用,实现代码复用,简化程序开发。
简单来说,库就是将提前编译好、封装好的常用功能代码的合集,是一套方法或数据集,为开发提供便捷保障。并且库还分为两种库:
- 动态库(共享库): 链接时仅记录函数调用地址,不复制代码;程序运行阶段才加载动态库执行,后缀:
Linux:.soWindows:.dll - 静态库: 程序链接阶段,将静态库中用到的代码完整复制拷贝到可执行文件中,后缀:
Linux:.aWindows:.dll
在Linux中动态库存储于 /usr/lib64 目录当中,Windows中动态库则存在每个可执行的文件夹中


Linux系统中库命名规则:lib + 命名 + .so/ .a
3.1.2 查看使用库
我们链接时说它使用库了,那体现在哪里呢? 我们可以通过指令来查看,某一可执行文件使用了哪些库。
3.1.2.1 ldd 命令
ldd是查看可执行文件依赖什么库的常用命令。
- 功能: 查看可执行文件依赖的动态库文件
- 格式: ldd [参数] 可执行文件名
- 常用选项: 无高频常用递归类选项,基础直接使用即可

这个就是c语言常用的库,我们使用的printf函数就是在该文件中实现的,可执行文件在使用该函数时会先跳转到该库中使用之后再返回文件。

3.2 链接
3.2.1 链接是什么
链接:把编译汇编生成的目标文件、库文件进行合并,完成地址重定位与符号解析,生成可执行文件的过程。
在我们的实际开发中,不可能将所有代码放在⼀个源⽂件中,所以会出现多个源⽂件 ,⽽且多个源⽂件之间不是独⽴的,⽽会存在多种依赖关系:如⼀个源⽂件可能要调⽤另⼀个源⽂件中定义的函数。
但是每个源⽂件都是独⽴编译的,即每个*.c⽂件会形成⼀个*.o⽂件,为了满⾜前⾯说的依赖关系,则需要将这些源⽂件产⽣的⽬标⽂件进⾏链接,从⽽形成⼀个可以执⾏的程序。
3.2.2 动态链接
独属于动态库的链接方法,将自己的程序与库中的方法链接。本质是:让程序找到库中方法的地址,从而链接使用,并且在执行完目标方法后就跳转回到调用代码位置。

以printf方法的调用为例我们现有程序,如下:
c
#include <stdio.h>
int main()
{
printf("hello world");
return 0;
}
printf是怎么执行的呢?大家可能以为是引入了头文件所以执行,其实不然,让我们分段讲解:
- 程序编写阶段:
#include <stdio.h>只引入了 printf 的声明 (告诉编译器:有个叫 printf 的函数,参数是什么) 没有它的实现代码;并且printf只是函数调用,告诉要在这里使用并不知道函数地址 - 预处理 + 编译 + 汇编阶段: 预处理 时展开头文件里所有声明;编译 时将代码翻译成汇编指令,此时printf还只是调用仍然没有地址;汇编 时生成目标文件
.o,.o文件里有一个符号表,里面标记了 printf 是一个 "未定义符号",并用一个临时地址占位 (objdump -t main.o可以查看)。 - 动态链接阶段: 链接器知道 printf 定义在系统动态库
libc.so里,所以只在可执行文件里记录:程序依赖哪个库、要去哪个库寻找 。然后生成重定位表 和动态符号表 ,分别记录哪些是地址时占位符和动态符号的名字以查用 - 程序运行阶段: 首先 ,加载文件到内存。其次 ,读取文件中的依赖信息(即需要什么库),将依赖信息加载入内存。然后 ,动态链接器遍历库的符号表,找到调用函数的真实内存地址。最后 ,根据重定位表将占位地址改为真实地址,此时成功指向。实现运行到printf,跳转库中执行字节码。

总的来说,printf的地址实在执行阶段切换的,然后进入库中调用,并非在头文件时就可以找到实现并运行。
3.2.3 静态链接
属于静态库的链接方法,直接将库中的实现方法直接拷贝到调用代码处。因此静态库只有在链接时有用,一旦形成可执行程序,静态库可以不再需求。
以动态链接讲解不同点有两个:
- 静态链接阶段: 链接器找到静态库 libc.a,直接把 printf 的实现代码从静态库中复制到可执行文件内部,并立刻完成符号解析和地址重定位,把占位地址直接替换成本程序内部的真实地址,最终生成一个完全独立、不依赖任何外部库的可执行文件。
- 程序运行阶段: 首先 ,操作系统把可执行文件加载到内存(不需要加载任何库);其次 ,程序执行到 printf 时,直接跳转到程序内部自带的 printf 代码执行;全程不需要依赖外部库文件、不需要动态链接器、不需要查找地址,直接执行自身包含的机器码,完成输出。

3.2.4 动/静 链接对比
动/静 态链接各有自己的优缺点,对于空间存储、加载速度、库依赖性这三点来进行对比。
- 空间存储: 动态链接形成的文件是依靠地址来进行调用函数的,而静态则需要将调用函数的实现复制到文件当中,因此前者节省空间。
- 加载速度: 无论动静形成的文件都需要加载到内存中才能运行,对于单个文件 :动态需要查找 + 加载共享库,首次启动慢,而静态启动运行单次启动快;对于大量文件:动态会缓存共享库,大量使用时具有效率,而静态相同代码重复占用内存。
- 库依赖: 动态文件每次都需要库的链接才能使用一旦都是就无法运行,而静态则需要一次链接就可以一直使用,直到修改代码或库更新,就需要重新编译。
总的来看,还是动态库比较占据优势,因此系统默认使用的是动态库 + 动态链接。
四、 库与链接的理论验证
继续使用上方的简短代码来验证我们所说的理论,当然还需要两个指令进行验证:一个是上文所提到的ldd命令,另一个是file命令,我们已经知道ldd是查看文件库依赖的命令,那我们本处直接了解file吧。
4.0 file 命令
- 功能: 查看文件类型
- 格式: ldd [参数] 可执行文件名
- 常用选项: 无高频常用递归类选项,基础直接使用即可

4.1 动态链接验证
我们将使用ldd命令和file命令联合验证动态链接。


通过ldd和file查询,发现code分别具有 .so 和 dynamically linked 、shared libs 这说明了默认的文件就是使用的动态链接。

通过查询发现这个动态库是真实存在于Linux的环境当中的。
4.2 静态链接验证
因为文件默认使用的是动态链接,所以想要验证静态就必须使用强制静态编译,即关键选项-static来强制使用静态编译。

命令gcc -static code1 -o code.c,可是出现报错了,这是因为系统中没有静态库所以不能正常执行,安装静态库即可。(centos :sudo yum install -y glibc-static)

安装后执行发现,code1的文件大小确实比code大,这就是因为将静态库代码复制到该文件当中。

同时查看文件依赖库和属性,也可以发现都是无依赖库的。
4.3 周边问题:C++的动/静 态链接情况
C语言是这样,那么C++文件是怎么样的?其实两者原理是相同的,只是依赖的库有所不同,让我们接着使用ldd和file命令进行查看吧。
4.3.1 动态链接
使用ldd指令查看c++程序会发现,它依赖了多个库,且库的后缀都为.so,因此也是默认动态链接

file 指令同理,发现dynamically linked 、shared libs

4.3.2 静态链接
与c的静态链接相同,也需要下载C++的静态链接库,这里不做演示了 (本质相同)
五、 库的内存加载和技术
5.1 内存加载
无论动态还是静态链接实现的可执行文件,在运行时还是得依靠内存实现。即将磁盘中的文件加载到内存当中,当然对于两者的加载方式是不同的。

当对于依赖静态的文件,是直接将库实现复制后加入的;而依赖动态的是先加载文件,后再把依赖的库加入,并使用时访问完成后返回。这样的设计实现了:
- 动态库的共享性: 所有动态链接的文件可直接链接使用同一个动态库,这是因为一旦加载后就不会消失,可供其他文件使用。
- 内存空间: 静态链接的文件会出现多份重复实现代码浪费空间,而动态只需要加载一份动态库即可节约空间。
- 命令的可执行: Linux中大量的指令都是通过C语言实现的,而加载该动态库就在可使用大量命令的同时节省了资源。
5.2 技术理解
库在技术理解上,可以说是:库 = 一堆 .o 文件打包合并而成,它的本质就是一堆汇编后的函数实现代码的合集,因此它是需要在链接时参与的,如与我们的.h 和 .c 形成的 .o 文件,从而形成可执行文件。
那它为什么不设置为.c 文件呢?这样还可以单独使用。那我们思考一个问题:我们用手机的时候,还得知道手机是怎么打开文件、交互文件的吗?并且暴露出来你修改我的文件怎么办?因此,设置为.o 文件,即保障了库代码的稳定性与私密性,又便捷了用户的使用。
总结
本节的讲解就到此为止了,感谢您的阅读,我们下节再见。