文章目录
- 🍇0. 前言
- 🍈1. 背景知识
- 🍉2. gcc/g++使用
- 🍊2.1 预处理操作
- 🍋去注释
- 🍋头文件展开
- 🍋条件编译 & 宏展开
- 🍊2.2 编译操作
- 🍊2.3 汇编操作
- 🍊2.4 链接
- 🍍3. 库的介绍
- 🍏3.1 动态库
- 🍏3.2 静态库
- 🍏3.3 动态库&静态库对比
- 🥕4. gcc/g++选项
- 🍒5. release & debug 介绍
- 🍓6. sudo权限提升
- 🥥结语
🍇0. 前言
Linux环境中,写好了C/C++的代码,要将其运行起来,我们采用的是gcc/g++ 这两个工具,gcc专门用来编译C语言的代码,g++专门用来编译C++的代码(C++兼容C语言,g++也可以用来编译C语言代码)。
这个操作固然简单,但是gcc/g++是如何将这种源文件生成我们的可执行程序的呢?本篇文章将重点讲解这其中的过程。
🍈1. 背景知识
一个源文件要生成可执行文件,需要经过四个阶段:
- 预处理: 插入头文件内容,进行宏展开
- 编译: 源代码转换为汇编语言
- 汇编: 将汇编语言代码转换为机器语言指令
- 链接: 将多个目标文件和库文件组合在一起,生成最终的可执行文件
而gcc/g++可以根据需求,生成对应的文件。
Tips:
之前有篇文章讲过这些知识,在本篇文章中就不再过多赘述,想了解的可查看——被隐藏的过程
🍉2. gcc/g++使用
🍊2.1 预处理操作
指令示例:gcc -E code.c -o code.i
-E
,该选项作用是让gcc在预处理结束之后,停止编译
-o
,生成后的目标文件,一般采用i
为后缀表示经过预处理的C原始程序。如果不跟-o,这直接将内容输出到屏幕上。
预处理阶段主要进行四个处理:
- 去注释
- 头文件展开
- 条件编译
- 宏替换
🍋去注释
注释只是方便我们编写的人或者要查看这个代码的人知道这一块是用来干嘛的或者进行了哪些操作,而对于计算机,并不需要这些注释。
🍋头文件展开
在Windows或者Linux环境下,要进行C/C++或者其他形式的开发,我们会安装配置对应的开发环境,这些是开发的前提。
以C/C++开发为例,开发环境不仅仅是VS、VsCode、gcc这些软件,更重要的是语言本身的头文件和库文件。
Linux默认为我们安装好了对应的环境
在Windows上,安装Vs20xx时,在安装的时候我们还选择的对应的开发包,这些开发包里面就包含了其中的头文件和库文件。
我们最开始学习C语言的时候,接触的第一行应该就是#include<stdio.h>
,这就是把头文件包含进来,里面有我们需要使用的各种函数声明,在预处理阶段,就会将我们引用的头文件所展开:
🍋条件编译 & 宏展开
条件编译根据当前宏定义的存在与否,决定了相应的代码块是否会被编译到最终的可执行文件中。通过条件编译,可以根据不同的条件选择性地编译不同的代码,以适应不同的编译环境或配置需求。
这里我们发现,我们所定义的在预处理阶段直接被替换,而用于判断的宏条件,也进行了裁剪。
当然了gcc在预测阶段,既然可以去注释、展开头文件、宏替换,这就说明了gcc具有直接修改代码的能力,那我们在预处理的时候,也可也手动的修改:
我们用的一些软件例如:Vs2022,分为社区版和专业版。
那么通过这个软件的公司,他们需要维护两份代码吗?
那当然不是,分开维护所需要的成本会大大提升,公司肯定不希望这样。
这时候条件编译就能起到作用:只需要维护专业版,然后在根据不同的编译条件,裁剪掉社区版不需要的功能即可。
🍊2.2 编译操作
指令示例:gcc -S code.c -o code.s
-S
,将预处理之后的文件,翻译成汇编语言之后停止操作一般以
.s
表示汇编代码文件
汇编指令,是最底层的编程语言,我们可以具体查看这些代码是如何操作的。
🍊2.3 汇编操作
指令示例:gcc -c code.c -o code.o
c
,进行程序的翻译,汇编操作执行完毕之后就停止汇编阶段是把编译阶段生成的
.s
(直接源文件转化也行)文件转成目标文件目标文件是二进制表示,全称:可重定位二进制目标文件。Windows环境下后缀名为
.obj
🍊2.4 链接
指令示例:gcc code.o -o code
将目标文件与所需库文件进行关联,最终生成可执行程序
指令 | 文件后缀 |
---|---|
-E | .i 预处理之后 |
-S | .s 编译之后 |
c | .o 汇编之后 |
🍍3. 库的介绍
我们所写的代码printf
,scanf
类似这些函数,都是由库来进行实现,而链接阶段就是链接的这些库。
对应C语言就是链接的C标准库
这个库本质上就是一个文件,由于环境的不同,库有各种格式:
Linux:
.so
(动态库),.a
(静态库)Windows:
.dll
(动态库),.lib
(静态库)
这个库有自己的命名规则:lib
name.so.XXX
,我们在看这个库的时候,只需要看这中间的一块就行。
在服务器上,默认只有动态库,静态库默认是没有安装的。
这也对应上面讲的,我们进行开之前安装的开发包,下载安装的就是对应的头文件和库文件。
在库文件中有我们需要使用的各种函数的实现,这些函数的实现,也是由程序员写的,只不过都打包到库文件中,这样我们在使用的时候就不需要到处去找,不需要我们自己去“造轮子”;另一个方面,这样也可以达到隐藏源文件的目的,用可以,怎么造出来的,保密。
这样整体流程下来就是:
头文件提供方法的声明、库文件提供方法的实现 + 自己写的代码 = 最终的可执行文件
库的链接分为静态链接和动态链接。
🍏3.1 动态库
打一个直白的比方:
大学里面一般有实操课,以计算机专业的为例。
假如今天上午没课,下午有一下午的上机课,知道了今天的课程安排,那我们就可以列一个今天的计划:
洗澡->洗衣服->打扫卫生->看一部电影->点个外卖吃饭->上机课->回宿舍打游戏…
这些计划里面,除了上机课要出宿舍楼,其他的都可以在宿舍里面进行。
那么这里的去机房这个行为,就类似于动态库,而我们就相当于可执行程序,当我们发现上机课,这个行为无法在宿舍完成时,我们就得机房,这就是程序跳转到库中执行,执行完毕之后,返回代码调用处,在继续完成之后的命令。
那我们是如何知道机房在哪儿呢?我们可以提供查看课表得知,这个过程就相当于编译器,在我们的大脑中注册了这个“信息”。
这个机房当然不只是属于我自己或者是我们班,这个是全校有上机课安排的学生共享使用的,所以这个动态库一般也被称为共享库。
如果这个动态库消失,那么将会导致很多程序都无法运行。
如图我们可以发现,不仅仅是我们自己写的程序依赖库,而且Linux里面的一些指令,也是依赖于C语言的库,这是因为Linux的底层就是用C语言实现的。
🍏3.2 静态库
接着上面的比方:
如果机房维修或者我们平时用到电脑情况较多,那我们需要有自己的电脑,这样我们就会有买电脑的需求,我们从商家那里购买电脑,从而达到操作电脑的目的。这个过程商家就好比是静态库,我们从他那里买了电脑,将电脑放在自己的桌子上,从而我们能够操作电脑,这就是静态链接。就算这个商家倒闭了,也不影响我们继续使用。但这样也占据了我们宿舍的一部分实际空间。
在编译器调用静态库进行静态链接的时候,会将静态库中的方法直接拷贝到目标程序中,该程序就不再依赖静态库了,但随之程序的内存也变大了
🍏3.3 动态库&静态库对比
在Linux中,编译形成的可执行程序,默认采用的是动态链接,如果需要采用静态链接,则需要我们手动进行。
示例:gcc code.c -o code-static -static
,后面加上-static
就是提示系统进行静态链接。
Tips:
服务器默认是没有安装静态库的,我们需要手动安装:
C语言静态库:
sudo yum install -y glibc-static
C++静态库:
sudo yum install -y libstdc++-static
这里普通用户安装需要权限提升,文章末尾有说明
如果我们没有静态库,是无法进行静态链接的;
如果没有动态库,只有静态库,gcc也是可以找到的,只是gcc默认优先寻找动态库。这里也变相说明了-static
的本质是改变寻找的优先级,但是这只适配一次,意思就是如果没有对应的静态库,则报错。
计算机专业的学生,可能平时玩的比较欢快,但是到期末的时候,要交结课作业了,可能是一个xx系统,这时候就慌了,于是就去各种开源网站上面找啊找啊找啊,好不容易找到一个,拿来一用,发现报错栏通红通红的。然后编译器可能就提示缺少什么什么东西,要下载,下载的这些东西里面就包含了一些动态库。好啦,下载完毕了,一运行发现还是有一堆通红的报错,这可能就是缺少了静态库(报错并不一定仅仅是由于缺少动态库或静态库引起的。还可能存在其他问题,例如代码本身的错误、版本不兼容、编译选项配置不正确等,我这里仅仅是举例)。
这就说明了,我们的一些程序,不一定全部是动态链接或者静态链接,可能是混合的。
提供这些了解,我们就可知道一些动态库和静态库的优缺点:
-
动态库:
优点:共享库,节省资源
缺点:一旦缺省,会导致许多程序无法运行
-
静态库:
优点:不依赖库,程序可独立运行
缺点:体积大,消耗资源较大
🥕4. gcc/g++选项
选项 | 说明 |
---|---|
-c | 仅编译源文件,生成目标文件(.o文件),不进行链接操作。 |
-o <file> | 指定生成的可执行文件的名称。 |
-g | 包含调试信息,以便进行源代码级别的调试。 |
-Wall | 启用所有警告信息。 |
-Werror | 将警告视为错误,导致编译失败。 |
-O<level> | 启用优化级别。 |
-std=<standard> | 指定使用的语言标准。 |
-I<dir> | 添加包含文件的搜索路径。 |
-L<dir> | 添加库文件的搜索路径。 |
-l<library> | 链接指定的库。 |
-D<macro> | 定义预处理宏。 |
-U<macro> | 取消定义预处理宏。 |
这只是一些常见的选项示例,gcc和g++支持的选项非常多,可以根据需要查看官方文档以获取更详细的信息。可以使用gcc --help
或g++ --help
命令查看完整的选项列表和说明。
🍒5. release & debug 介绍
我们写的程序一般分为两个版本:release和debug。
-
release:
发布版本,无法调试,编译器会进行各种优化,以提高程序的性能和效率。使用Release配置编译生成的可执行程序通常会具有较高的性能和较小的体积。但由于优化的关系,对于调试和错误定位可能不太友好,因为很多调试信息被削减或省略。
-
debug:
用于开发和调试阶段的版本,使用Debug配置编译生成的可执行程序可能会比较慢,并且占用更多的存储空间。但它们提供了更多的调试信息,使得在开发和调试过程中能够更方便地定位和修复问题。
gcc默认的是release版本,可使用指令gcc code.c -o code_debug -d
生成debug
版本:
🍓6. sudo权限提升
当普通用户想要执行一些操作时(例如下载一些软件),权限不够,这时候就需要进行权限提升。
sudo
指令就能将用户权限暂时提升。
我们发现,当我们使用sudo
提升权限的时候,被拒绝了,原因是我们没有被添加进信任白名单。这个操作是需要root用户来进行添加的。
切换成root用户,然后用vim编辑器进入这个文件vim /etc/sudoers
大概在100行左右(如果没有行号,指令: set nu
,尽量不要使用鼠标滚动,vim有自己的上下键),就能看到我们的信任白名单。
复制粘贴root
这一行,然后再更改用户名即可。因为这个文件只有读权限,所以在最后退出到时候,强制写入保存退出: wq!
。
这时候我们就可以进行权限提升了:
当然了,如果这个用户提升权限之后喜欢搞破坏,root用户也是有权限将其从白名单剔除的。
就算将root从这个白名单剔除,root也还是不受约束,因为root就是Linux中的最高权限。
🥥结语
本篇文章基本上是已C语言为例,采用的是gcc;对应C++的编译工具g++是同理直接用就行。另外除了介绍gcc/g++的使用,还提到了一个程序是如何从源代码最终生成可执行文件的皮毛内容,如果大家有兴趣,可以去查阅书籍了解这其中的详细过程。
那么本次的分享就到这里啦,如果有帮助的话希望三连支持一下,我们下期再见,如果还有下期的话。