开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 18 天,点击查看活动详情
一、对程序的认知 && 初识gcc
1、程序是如何诞生的?
对于一个程序来说,从【编辑】——>【编译】——>【链接】——>【运行】总共要经过这些步骤算是一个程序完成的诞生过程。如果我们要去细谈其中的过程,又可以像下面这样分化:point_down:
- 我们需要将这个【源程序】首先转变为【可执行程序】,中间的这一流程就被成为==翻译环境==,这一环境也就是我们本文所学习的
gcc
所要做的事情。在下一模块我就会对预编译
、编译
、汇编
、链接
这四个小模块展开做讲解:book:
2、gcc的初步认识
:dart: gcc英文全名为
GNU Compiler Collection
,早期的gcc编译器主要用于C语言编译,但是经过几十年的发展,gcc编译器可以用于多种语言的编译,例如C++、Go等目前较为主流的语言。 :dart: 熟悉gcc编译器是对于我们开发C/C++程序的底层基本功,虽然目前各厂商的开发IDE已经非常智能,从某种程度上已经把程序员从底层代码的编译、部署等工作解放出来,但是如果需要开发大型C++项目或者对于编译过程进行优化,那么gcc编译器是需要进行了解和深入的:mag:
- 对于gcc而言,它是一款Linux中的编译器,通常和我们之前所讲的Linux 编辑器vim配合使用。
- 我们对【编辑】【编译】应该要有一个很清晰的认识,我们平时在写C/C++代码的时候,大多都是在一些集成开发环境上(IDE)进行,就像VS、VC。这些软件屏蔽了底层的细节,使用户可以不需要关注这些繁琐复杂的东西,只需要关注程序本身即可,但是却==淡化了我们对程序的演变和形成==
- 这些软件通常是包含了
编辑
、编译
、链接
、调试
这些功能 - 对于
编辑
功能来说有【编辑器】 - 对于
编译
功能来说有【编译器】,VS2019为cl.exe
- 对于
链接
功能来说有【链接器】,VS2019为link.exe
- 对于
调试
功能来说有【调试器】
相信有了这些,你对程序的诞生过程一定有了一个更加深入的理解
3、如何使用gcc
讲了这么多,要如何去使用gcc呢?来看看吧👀
格式 gcc [选项] 要编译的文件 [选项] [目标文件]
具体如何使用,请看下一模块:point_down:
二、gcc逐步分析程序的翻译环境
好,接下去我们就来谈谈如何如何使用gcc来观察程序在翻译环境下的各个步骤。一下的讲解可能比较简略一些,如果想了解得更加深入,可以看看我的另一篇文章——> C生万物 | 程序的翻译环境和执行环境。很多内容也是从这篇文章中归纳出来的
- 下面是从源文件【.c】到可执行文件【.exe】的整体过程
1、预编译【进行宏替换】
在预编译阶段会执行以下四件事
- 头文件的展开 —— 编译器第一件做的事
- 宏定义的替换
- 去注释
- 条件编译的执行
gcc需要执行的指令
gcc -E file.c -o file.i
-E
表示是让 gcc 在预处理结束后停止编译过程-o
表示输出到指定文件【-o 后面紧跟的永远都是你要形成文件的名称】.c
结尾的文件表示源程序;.i
结尾的文件表示已经过预处理的C原始程序
2、编译【C语言——>汇编语言】
在编译阶段会执行以下四件事
- 语法分析
- 词法分析
- 语义分析
- 符号汇总
以上其实就是在将C语言的代码转化为汇编代码的一个流程
gcc需要执行的指令
gcc -S file.i -o file.s
-S
表示只进行编译而不进行汇编,生成汇编代码.i
结尾的文件表示源程序;.s
结尾的文件表示经过编译生成的汇编代码
3、汇编【汇编语言——>可重定位目标二进制文件】
在汇编阶段会执行以下两件事
- 将汇编指令转换为二进制指令【需要特定的文本阅读器readelf】
- 形成符号表
gcc需要执行的指令
gcc -c file.s -o file.o
-c
表示是gcc编译到目标代码.s
结尾的文件表示经过编译生成的汇编代码;.o
结尾的文件表示经过编译生成的汇编代码
- 在汇编这一块,就是将汇编语言翻译成可以重定位的二进制文件,里面的二进制代码我们人眼是看不到的,要特定的阅读器才可以看得明白
- 有几个源文件【.c】就会生成几个目标文件【.o】
此时我们可以来试试执行一下这个目标文件,不过发现没有【x】可执行的权限,于是使用chmod
做一个提权的操作。但是系统却说cannot execute binary file
,上面看到过这是一个二进制文件,对于二进制文件来说是不可以被执行的
对于上面的这三步【预编译】、【编译】、【汇编】都是对一个源文件中的代码进行操作的过程,没有引入其他人的代码。但是到了链接这一步,可能就要与别人的代码或者是库中的代码一起执行
4、链接【生成可执行文件或库文件】
在链接阶段会执行以下两件事
- 合并段表
- 符号表的合并和重定位
gcc需要执行的指令
gcc -E file.c -o file.i
-E
表示是让 gcc 在预处理结束后停止编译过程;-o
表示输出到指定文件.c
表示源程序;.i
表示已经过预处理的C原始程序
- 这里便不展示与其余文件的合并了,这里导一个我之前做过的GIF
- 可以来看看这个可执行文件
my_out
,它也是一个内容也是二进制代码,可供机器识别
- 最后的总结可以看看这张图
5、巧记gcc命令选项与生成文件后缀【⭐】
📚 命令选项:-E
-S
-c
【键盘左上角的Esc,不过中间的S要大写】 📚 文件后缀:.i
.s
.o
【.iso为镜像文件的后缀,ISO也为国际标准化组织】
三、gcc与g++的区别
看了这么久gcc,我们来看看g++,其实这两个编译器的用法是一模一样的,只是对于gcc才说是用来编译C语言代码,对于g++来说是用来编译C++的代码
- 不过这只是笼统的说法,其实对于gcc而言也是可以编译C++代码的,对于g++而言是可以编译C语言代码的
- 但是实际上在一些具体的语法规则上,C++在编译过程中的语法检查会更加严格。此外,C++语言本身在编译 过程中也会引入C++的标准库,如果使用gcc编译器直接编译C++语言会在编译过程中添加额外的参数,这样会显得编译过程较为繁琐(因为大部分情况下我们希望标准库可以直接引入,而不是再需要手动指定,否则对于初级使用者会带来额外的学习负担)。为了更方便使用编译器,我们选择g++来编译C++代码。
- 总结一下,gcc可以完成C++语言的编译,但是使用过程会较为繁琐,而g++就是简化后的编译指令
四、谈谈链接的过程【动静态库的理解】
在上面,我们说到了对于一个程序而言分为四步,预编译 –> 编译 –> 汇编 –> 链接。对于前面三步而言我给出了对应的记忆方法。但是对于最后一步的链接,却不是那么好理解,所以我们专门来谈谈这个链接🔗
1、 库的初步认识
首先要说一些必不可少的前言小知识
- 问:我们为什么能够在Linux在进行C、C++代码的编写和编译呢?
对于上面这个问题相信你也是非常得困惑,在上面的演示过程中,我也往file.c
里写了一些C语言的代码,其实你把后缀改为.cc
就可以写一些C++的代码了
- 这究竟是为何呢?答:因为Linux大部分都是用C写的,少量使用汇编,而C++则是兼容C,所以在Linux系统默认已经携带了语言级别的头文件和语言对应的库!
- 可以先来看看系统中的头文件所在目录,然后就可以看到我们熟悉的
stdio.h
、stdlib.h
等等
- 然后在这些头文件中包含了对应的库函数实现,我们可以进去看看
- 然后在里面就可以看到我们在C语言中使用的
printf()
和scanf()
这些库函数,这下你应该知道为什么我们在代码的时候引入stdio.h
这个头文件就可以使用里面的库函数了
所以可以得出一个结论:只要有头文件就必有源文件
- 其实我们在安装VS2019、VS2022的时候。最重要的一个工作是什么呢?其实在这么几十分钟的安装时间里,这个自动化程序更多的是在帮我们下载并安装语言的头文件和库文件【编译需要的两个东西】
📚拓展【ldd】指令
接下去我们来介绍一个指令,因为在后面的讲解中会使用到
格式:
ldd filename —— 检测可执行程序形成的时候都依赖了哪些库
-
l -> list 【列出】
-
d -> dynamic 【动态的】
-
d -> dependencies【依赖关系】
-
——> 列出动态依赖关系
- 然后我们就可以去看看每次gcc最终链接后形成的
a.out
这个可执行程序都依赖了哪些库。然后可以看到对于我框起来的这个就是标准的C语言库
解释:
- => 左边的是程序需要连接的共享库
- => 右边的是由 Linux 的共享库系统找到的对应的共享库在文件系统中的具体位置
知道了这些其实就可以回答上一个小节我所提出的问题了:mag: 【最后的答案是】:==系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径“/usr/lib”下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是⭐链接⭐的作用==
这样就又衍生出了一个问题:
- Linux中的这些指令从何而来,为什么使用这些指令可以完成对应的操作?
接下去我列举几个常见的指令
- 通过列出上面三个指令所依赖的动态库,相信你应该知道为什么这些指令完成它们对应的操作,也可以回答出我提的这个问题
【最后的答案是】:==程序、工具、指令是一回事【都是用C语言写的,都是经过编译形成的,都和C语言标准库有关】==
2、 动态库与静态库
看完了上面这些,对Linux中的库有了一个基本概念后,我们便可以来谈谈【动态库】与【静态库】,可能现今我的水平有限,讲不了很深入,后续弥补~
- 因为对于动静态库而言Windows也是有的,所以我会做一个对比
-
动态库
- Linux下:以
lib
作为前缀,.so
作为后缀【libXXXXXX.so】 - Windows下:以
.dll
作为后缀
- Linux下:以
-
静态库
- Linux下:以
lib
作为前缀,.a
作为后缀【libXXXXXX.so】 上面的这些是在/lib64
下找到的 - Windows下:以
.lib
作为后缀
- Linux下:以
但是光这么看动静态库就非常晦涩难懂了,不用急,在下一模块,我将用更加形象的方式来介绍它们
3、动静态库的感性理解【小蚂蚁网吧🐜】
本故事均为虚构,如有雷同纯虚巧合U•ェ•*U
故事背景:你是一个初三的学生,在学校被老师管,在家里又被父母管,于是你就想着在上了高中之后要住校,然后就可以无拘无束地玩了,你呢,很喜欢玩游戏,于是就找到你心仪哪所高中的学长询问附近有没有对应的网吧,提前做好打听; 一年后你也考上了自己心仪的高中,终于可以挣脱父母的管教了
- 在高中的学校待了两个多礼拜之后呢,来到了周末,老师也布置了很多的作业,所以你本来想要去。你呢前一天晚上躺在床上就想自己原本要出去网吧玩的计划可能很难实现,因为作业比较多,于是呢就给自己列了一个清单,明天要完成哪些学习任务
- 然后到了第二天早上八点,你起来了,要先做一小时数学作业,然后写会作文,叫做《我的父亲👨》,因为文笔不是很好,所以可能需要写两个小时,那这个时候来到了中午,要吃饭了,你呢可以点外卖可以不用去食堂,可以边吃外卖🍚边看看教学视频📺,然后在吃完饭后就练习练习配化学方程式。
- 然后这个时候时间来到了下午两点,室友都在午休,然后你呢就想着也写了一上午+中午的作业了,应该要休息一下,做到“==劳逸结合==”,于是呢就顺序着自己的计划找到了学校的北门,恰好保安也没有在岗,你就溜了出去,此时呢就看到你学长和你说的 #小蚂蚁网吧🐜#,于是就进去找到网管,网管给你分配了一个座位,3块钱一个小时,你买了两个小时,然后就开始自己疯狂的游戏💻
- 然后两个小时到了之后,你只能忍痛割爱离开你的游戏o(╥﹏╥)o
:dart:我们可以将这个网吧当做的是系统中的库,然后把你所列的这个清单当做是一段程序。那对于做数学作业、写文章这些就相当于是你在写一些自己的算法或者是简单的循环、条件判断语句,不需要调用库中的内容。但是当程序执行到了【上网】阶段后,就无法再自己执行下去了,只能去调用库函数 :dart:为什么呢?就相当于是 我们在写代码的时候可能有一段逻辑写不下了,因为这段逻辑很复杂。但是要是实现这个逻辑刚好有这个库函数,所以你就去会调用这个库函数。这个库函数就相当于是网吧 :dart:但是你怎么知道这个库函数在哪里的呢?——>你学长告诉你的呗。也就相当于你知道了库函数中这个函数的地址在哪里,你只需要顺着北门然后根据这个地址找过去就可以了
:dart:那这么来说你的学长就相当于是一个链接器,网吧就是一个动态库,你知道了动态库中所需要的这个函数的地址,然后你就顺藤摸瓜🍈地找到了这个地方,你的学长就做了一个链接的功能,还 :dart:记得我在一开始放出的那张图吗,你就可以把自己想做是目标文件。那个可执行程序也就是最后你可以打游戏的那台机子。你在打游戏的这个过程其实就相当于是在调用库函数的一个过程了。最后你回到学校那也就是调用函数结束,然后要继续往下执行你的程序
但是事情完了嘛,当然没有,才刚刚开始。。。
- 你们学校呢也有很多同学喜欢玩游戏,也通过他们各自的学长知道了这个【小蚂蚁网吧】,于是周末也来玩,但是呢这个网吧似乎不太感谢,突然警察👮造访,说他们涉嫌非法经营🈲,然后这个网吧就被封了,你和你的同校同学都玩不了,知道默默回学校
:dart:上面有说到,这个是网吧就是动态库,我们在上面有说到过这个很多指令都在使用这个C语言的标准动态库 :dart:这个警局你可以认为是恶意入侵的黑客,然后将你系统的库搞坏了,那网吧中的点电脑就可以算作是指令,若是库没了,那你的指令就无法执行了,所以我们不要去随便动系统中的已经写好的库,否则你的Linux就无法正常使用了
👉因此我们其实可以初步看出来这个动态库其实是不安全的,因为是共享的,所以大家都可以使用,但是只要它被破坏了,那大家都没得玩了
故事还在继续。。。
- 一个学期结束了,你放假回到家里,这次的期末考试你考到了班级第一,虽然你也有在玩游戏,但是你也有自己的学习规划。真正地做到了#劳逸结合#,而且你的作文《我的父亲》获得了校园征文大赛一等奖,这让你的父亲非常得开心,于是就说:“儿子,干的不错。说吧,想要什么都可以满足你”
- 那这个时候你就觉得学校外面的网吧真的不是不太安全,如果我自己有一台电脑就好了:heart_eyes:
- 于是这个时候你就和你爸爸说想买一台电脑——>为了可以更好地学习。那这个时候你爸就觉得也很有道理,于是就答应你了
- 在新学期开学后一个礼拜,此时你正在宿舍学习,然后你爸突然就到进到宿舍然后搬进来一台电脑,接着你仔细看了看这台电脑好熟悉的感觉,Σ(っ °Д °;)っ这不是网吧的电脑💻,于是你爸就说:“儿子,怎么样,这台电脑喜欢吗,我不知道你想买怎样的电脑,于是在学校附近逛的时候就发现了一家【大象网吧】,问了里面的老板电脑的配置,然后直接从他那里买了一台,这种你们年轻人的电脑你应该用的惯吧”
- 于是你就说:“哦哦,谢谢爸爸,可以的。大象网吧(ˉ▽ˉ;)…。。。”
- 于是到了下个周末之后你同学约你一起去网吧,你说:“我都有电脑了,不需要去了”。然后你的同学回来之后就说:“那个大象网吧好像被封了,说是黑网吧”。此时你就笑笑不说话:blush:心想:封了还更好,这样就不会影响大家学习了。。这样的网吧真的是太没有原则了,毫无底线呀,不过里面的电脑💻是没错的,不要可惜了呀。
:dart:可以看到,其实对于从网吧中拿来的电脑,就是库中的那个函数,也就是将库中的整个函数直接放到你的程序中来,这样你完全不需要再到库中去调用函数了,直接在你的程序中使用即可 :dart:当你的同学去到黑网吧,然后得知被封了之后无奈返回后,这个时候你有这自己的电脑,完全不需要再去考虑是否有电脑可用的安全性了,但就是这个电脑放在宿舍可能有些占位子,原本比较宽阔的空间就要被一台电脑给霸占了,那确实是没办法
讲完了上面这些,相信你对【动态库】和【静态库】一定有了一个自己的理解,但是可能还有有点模糊,在下一小节我将对他们做一个进一步的分析:mag:
4、动态链接与静态链接的区分
接下去我们对【动态库】和【静态库】做一个分析梳理,更好地理解动静态库的原理
✔【动态链接】 —— 仅仅是把库中的你所用的方法的==地址拷贝==到程序里 网吧
- 动态库优点:因为可以做到被大家共享方法,所以真正的实现永远都是在库中,程序内部只有地址,比较节省空间
- 动态库缺点:我们的程序还是依赖任何库,一旦动态库缺失,我们的程序便无法运行
:memo:可以联想到,对于小蚂蚁网吧🐜来说,大家都可以去玩,只需要知道网吧的地址在哪里,然后直接去即可,也就和函数一样直接进行一个调用,对自己的程序而言不需要消耗任何的空间 :memo:但是呢一旦这个网吧被封了,也就是动态库丢失了,那此时我们再去调用库中的函数就会失败,这是动态库比较致命的一点:scream:
✔【静态链接】 —— 自己代码当中用到的库中的方法==直接拷贝==到程序里 台式电脑
- 静态库优点:我们的程序不依赖任何库,自己就可以独立运行
- 静态库缺点:因为自身的拷贝问题比较浪费空间
:memo:对于静态库来说,确实是比较方便,不需要再去库里面调函数了,直接把这个函数整体拷贝到我们的程序中即可。也就是我们不需要依赖任何的库,也不用去大象网吧🐘 :memo:但是呢这也意味着我们的程序会变得非常庞大,举个最简单的例子就是你完全不写任何自己的函数,所有功能和实现都放在main函数里,这就会显得非常浪费空间
5、做做实验【动静态链接的使用】
- 在Linux命令汇总中有说到过
file
指令可以用来查看一个文件的类型,那我们就可以来看看这个动态的可执行文件
- 最重要的就是这个
dynamically linked
动态的链接,这也就意味着这个可执行文件是经过动态链接生成的(gcc编译完默认是动态链接) - 那我们要如何使一个文件进行静态链接呢,很简单,只需要在使用gcc编译的时候在后面加上一个
-static
的选项即可
- 可以看到,当我们使用静态链接时,再使用
ldd
去列出动态的依赖库,就会报错说not a dynamic exexutable
不是动态可执行文件,所以就进一步可以看到ldd
只能列出动态依赖库,对于静态库是没有办法列出来的 - 但是我们可以使用
file
指令去查看这个文件的文件类型,于是就可以看到【statically linked】静态链接就可以进一步验证这是一个使用静态链接成的可执行文件
不过你在使用静态链接的时候可能会报错,因为对于我们的云服务器,默认就只装了【动态库】,而静态库是没有的,所以要安装一下静态库
- C静态库安装:
(sudo) yum -y glibc-static
- C++静态库安装:
(sudo) yum -y install libstdc++-static
安装之后应该就可以了。然后我们再来看一个地方
- 可以看到对于动态链接和静态链接所产生的可执行文件大小不太一样,差了可是80倍,这是一个惊人的数字w(゚Д゚)w
- 所以这就是为什么云服务器默认就是以动态链接记性编译了,因为静态链接虽然是无需调用库函数,但是呢形成的程序体积会非常庞大;而且其中有一部分库的代码,然后系统中本身又带有库,这就==造成了磁盘空间的浪费==
看完了上面这些,相信你对动静态库有了一个初步的了解,不过我没有讲得很深入,如果后续有更好的内容会继续补充
五、总结与提炼
最后,我们来总结一下本文所学习的内容
- 在一开始,我们了解了一个程序是如何诞生的,然后初步接触到了Linux中的编译器gcc
- 然后我们使用gcc针对程序的翻译环境进行了逐步展开分析,看到了一个
.c
的源文件究竟是怎样一步步变成一个.exe
可执行程序的,其中要经过【预编译】【编译】【汇编】【链接】四个步骤 - 最后我们对最后一个
链接
做了详细的介绍,谈到了Linux中库的概念,然后了解到了【动静态库】,我用了一个小故事让你更好地感受什么是动静态库,相信你在看了这个故事后一定也会有所收获。了解了什么是动静态库后就逐步上手使用他们进行动态链接与静态链接
,进一步感受到了linux的纯粹😄
以上就是本文所要介绍的所有内容,感谢您的观看
今天的文章Linux | 编译器gcc/g++的使用【动静态库的认识】分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/15833.html