一、前言
在上一篇 Linux 博客中,我们讲解了 vim 编辑器的使用,那么在 Linux 上写代码就没问题。但是写的代码如何编译?这就要用到我们今天讲的内容 —— gcc
编译器。
在 Linux 中,C 语言用 gcc 编译;C++ 用 g++ 编译。
我们今天的目标就是学会如何使用 gcc ,了解程序经过翻译环境形成可执行程序的过程,并且讲解动静态链接库的知识。
二、gcc 演示翻译环境
对于一个 C 程序,从源文件到形成可执行程序一共要进行四步:预处理、编译、汇编、链接 。这四步过程被称为 翻译环境 。
接下来,我们用 gcc 分别演示这四个过程。
1、预处理
在预处理中,需要完成头文件的展开、宏替换、去注释、条件编译等工作。
经过预处理后,当前文件还是 C 语言,只不过变得更加精简。
预处理指令 :
gcc -E file.c -o file.i
-E
选项:让 gcc 进行程序的预处理,预处理之后就停下来。-o
选项:-o
选项紧跟目标生成文件的名称。gcc -o file.i file.c 或者上方的指令都是可以的。
样例程序 :
#include <stdio.h> #define FOO // #define 后定义内容可为空 #define MAX 100 int main() { int max = MAX; printf("%d\n", MAX); printf("hello PRINT %d\n", MAX); printf("hello PRINT %d\n", MAX); printf("hello PRINT %d\n", MAX); printf("hello PRINT %d\n", MAX); printf("hello PRINT %d\n", MAX); printf("hello PRINT %d\n", MAX); #ifdef FOO printf("yes"); #else printf("no"); #endif return 0; }
进行预处理 :
预处理之后生成 file.i
文件,分别打开 .c 和 .i 文件进行对比 :
我们发现代码变为八百多行,这是因为头文件 #include <stdio.h>
被展开,同时预处理的其他过程也都进行了。
2、编译
当程序在编译时,gcc 会检查代码是否有语法错误,了解代码基本内容,检查无误就会把代码翻译为汇编语言。
汇编语言中包含源文件的部分内容,所以也可以说该文件为 C 语言汇编代码 。
编译指令 :
gcc -S file.i -o file.s
-S 选项:让 gcc 进行程序的编译,编译之后就停下来。
-o 选项的位置可以改变,但是一定要记住 -o 选项的规则。
.s :生成汇编代码的后缀
通过源文件 .c 也可以形成编译后的文件,指令为 gcc -s file.c -o file.s,但是我们为了演示过程的连续性,我们统一从上次生成的文件开始执行指令。
进行编译 :
观察 file.s
文件内容:
上部分框起来的语句含有 c 语言的样子;而下部分框起来的语句则是汇编代码,由此证明在编译过后生成的文件为 C 语言汇编代码。
3、汇编
汇编之后会把汇编语言转换为二进制文件。
这里的二进制文件可以说是 可重定位二进制文件 。该文件不能被执行,类似于 windows 上的 .obj 文件。
这一步知识把我们自己的代码翻译为二进制目标文件。
由于机器只认识二进制文件,所以换言之,这一步就是把人能看懂的文件,翻译成机器能看懂的,就是 生成机器可识别代码 。
汇编指令 :
gcc -c file.s -o test.o
-c
选项:让 gcc 进行程序的汇编,汇编之后就停下来。.o
:生成二进制文件的后缀。
进行汇编 :
观察 file.o
内容:
打开文件发现什么都看不懂。
这就是二进制文件,计算机可以识别该文件。
那么该文件可以执行吗?
不可以执行,报错信息为权限被拒绝,通常为权限不够。那这怎么证明该文件不能执行?
我们将其提权试试:
然后再次 ./file.o
执行该文件:
仍然执行失败,并报错:无法执行该二进制文件。所以这就证明我们的结论:汇编生成的二进制文件不可执行!
4、链接
链接过程就是将程序和对应的库链接起来,编译器会自动识别语言。
链接指令 :
gcc file.o -o myfile
- 生成文件为可执行程序
gcc file.o
也可以完成链接
进行链接 :
再执行生成的可执行程序 file
:
5、总结
我们这边将翻译过程按步执行只是为了让小伙伴们理解程序由源代码到可执行程序的一个过程。
实际上,在我们日常编译代码只需要 gcc file.c 就可以一步生成可执行文件。
再提供一个记忆方式:
对于 预处理、编译、汇编 三步的选项分别为 ESc ,可以与键盘上的 esc 键来帮助记忆,区别前两个字母是大写。而生成的文件后缀分别为 iso 可以利用国际标准化组织(ISO)来记忆。
三、动静态链接库
1、库的认识
首先清楚一点,我们写的代码里面经常会用库函数,这些调用接口的方式是我们写的,但是这些库函数的底层实现不是我们写的,而这中间就有一个调用库的过程。
在链接的过程中,需要对形成的二进制文件和库文件进行合并从而形成可执行程序,这边就调用了库。库是能成功编译的必要条件。
我们一直在使用库,至少是语言上的库。
比如上方链接生成的可执行程序 file ,它就依赖了库。
通过 ldd file ,就可以查看该程序依赖了哪些库:
不仅程序依赖库,我们在 linux 下能进行代码编写也是因为内置了库。linux 默认有语言级别的头文件,和语言对应的库。
通过 ls /usr/include/
就可以看到系统的内置的头文件,我们通过头文件就可以根据库函数,找到对应的方法,从而链接到正确的库:
在linux 下,库分两种 :
- 静态库,格式为
libXXX.a
。 - 动态库,格式为
libXXX.so
。
基于上面的格式,不同的库还可能会有版本,例如 file
程序就依赖了libc.so.6
,后面的 .6 就是版本:
并且我们发现可执行程序依赖的就是 .so 动态库。
对于动静态库,以lib 为前缀,.a/.so 为后缀,掐头去尾就是名称。例如 libc.so.6 ,它的名字就是 c ,是 c 语言内置库。
而以上动静态库的后缀为 linux 上特有的后缀划分;如果是 windows 下静态库为 lib ,动态库为 dll 。
不止可执行程序,就连指令也依赖于库,我们观察几个指令:
常用的 ls, tar 它们都依赖于库,甚至还依赖于 c 库。
由此发现,指令和 c 程序一样,都依赖与库。所以我们可以将指令看待为:指令是程序,是工具,也是“指令(字面意思)” 。
虽然从编译角度来说,指令和我们写的 c 程序没有任何区别,但是从功能上来说确是天差地别。
讲了这么多,实际上也就是一句话,库是必要的,并且库分为动态库和静态库 。
2、链接方式
Linux 的链接方式就两种:
- 动态链接
- 静态链接
而 Linux 默认的链接方式为 动态链接 ,我们来证明一下:
用 file
指令查看 file
文件的类型:
我们发现生成的可执行程序默认就是采用的 动态链接 。
那么为什么采用静态链接而不采用动态链接?这里面有什么原因?
我们先了解一下动静态库。
3、动态库与静态库
动态库对应的链接方式为动态链接;静态库对应的链接方式为静态链接。
动态链接必须使用动态库,静态链接必须使用静态库。
动态库优缺点:
动态库也可以说是共享库。动态库在链接的时候,并没有把相应的库文件加载到可执行程序中,而是在运行的时候,拷贝库中所需代码的地址到可执行程序中相关的位置。
通过这种链接方式,使用动态库就大大节省了内存损耗。
但是这也带来一个缺点,由于动态库链接时,是通过地址链接的。所以只要我们的动态库缺失,程序便无法运行。
静态库优缺点:
静态库则是在编译链接时,把库文件的代码全部加载到可执行程序中。
这种链接方式导致生成文件体积庞大。若文件数目一多,到时候内存就会被占用很多。
但是这也有一个优点,这样就使得程序不依赖于库了。即使库出了问题,也没事,因为我们已经将代码加载到程序中了。
通过上面,我们发现,动态库可以大大节省内存开销,并且一般对应的动态库并不会出问题,再衡量可执行程序很多的情况,动态链接其实是最好的链接方式。
所以 Linux 默认是采用动态链接的链接方式。
4、两种链接方式的使用
对于动态链接,我们已经使用过了,比如上文生成的 file
文件,就是动态链接生成的。
而静态链接则需要通过特定方式来使用,并且 Linux 默认只装了动态库,静态库是没装的,所以先安装一下:
sudo yum install -y glibc-static
安装完毕后,就可以使用了。
静态链接的使用方式为:
gcc -o file.c -o file-static -static
- -o 后面还是自定义名称,这里只是为了做区别
- 结尾的
-static
代表以静态链接方式编译
我们发现,静态链接后生成的可执行程序的体积非常大。
再用 file file-static
查看一下文件类型:
就变成 statically linked:静态链接了。
四、gcc 选项汇总
gcc 常用的选项和操作也就是 -o 选项还有 gcc file 直接生成可执行程序。
下面对其他选项做出一些补充:
-E :只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面
-S :编译到汇编语言不进行汇编和链接
-c :编译到目标代码
-o :文件输出到文件
-static :此选项对生成的文件采用静态链接
-g :生成调试信息。GNU 调试器可利用该信息
-shared :此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库
-O0 :不做任何优化,这是默认的编译选项。
-O1 : 对程序做部分编译优化
-O2 :是比O1更高级的选项,进行更多的优化
-O3 :比O2更进一步的进行优化
-w :不生成任何警告信息
-Wall: 生成所有警告信息
五、结语
到这里,本篇文章就到此结束了。
gcc 的选项很多,但是我们实际上用的多的也就只有 -o 选项而已。且编译程序也可以 gcc file 一步到位。但是对于程序编译的过程用到的选项,还有这些步骤我们还是有必要了解的。
而今天的重点 a n d u i n anduin anduin 认为是动静态库的认识。实际上动静态库还有很多知识,这些得到以后的基础 I/O 讲,这边我们就是初步认识。本篇博客只要搞清楚两种动静态库和两种链接方式即可。
如果觉得 a n d u i n anduin anduin 写的不错的话,可以三连支持一下哦!我们下期见~