Linux入门系列目录
《终端基本指令篇》
《yum 篇》
《vim 篇》
《gcc和makefile篇》
《Linux的第一个小程序——进度条》
《gdb 篇》
文章目录
前言
在上一篇博客《vim篇》中已经了解vim的功能,学习vim的相关命令,最后还配置了vim,为编写代码提供了一个好的环境。
但是,无法编译运行的代码是没有价值的。
接下来,就学习如何使用将写好的代码编译,如何将文件运行,如何编写基本的makefile文件以提高写代码效率。
程序的翻译
在ANSI C的任何一种实现中,存在两种环境
第一种是翻译环境:指源代码被翻译成可执行的机器指令。
第二种是执行环境:机器指令被实际执行起来。
计算机能够识别并执行机器语言(即二进制指令),但是我们写出来的C语言代码都是文本信息,计算机无法识别,因此,我们要将源代码文件翻译成机器指令,而帮助我们执行这个过程的工具就是gcc(g++的操作与gcc高度相似)。
想要理解好gcc的使用,就先要对程序的翻译过程有一个良好的认知。
翻译的步骤可以分为2个大步骤或者4个小步骤:
2个大步骤指的是,编译和链接,而编译又可以被分为预处理、编译、汇编3个小步骤,加上链接一共4个步骤。
简单过程如下图:
接下来就开始细讲翻译过程的每一步是如何操作的,讲解过程中用到的工具就是gcc,所以没装gcc的朋友可以先去装一个。
安装命令是sudo yum install -y gcc
预处理(预编译)阶段
C语言当中有一个知识点叫 “预处理指令”,指的是,以#
开头的代码行。
而预处理指令的功能包括宏定义、头文件包含、条件编译等。
在预处理阶段gcc做的事主要有:宏定义的符号的替换、头文件展开、去除注释、条件编译这几个。
接下来个人写一个简单的小程序,命名为hello.c
再用一条命令gcc –E hello.c –o hello.i
带你了解
选项-E
,该选项的作用是让 gcc 在预处理结束后停止编译过程。
选项-o
是指目标文件,-i
文件为已经过预处理的C原始程序。
以下是文件对比图
由图中可以看到:
第一:hello.i文件中#include <stdio.h>
不见了,取而代之的是一大段代码,这段代码正是头文件stdio.h的内容,这就是 “头文件的展开”。
第二:第4行的注释在hello.i文件中也消失了,这就是 “去注释”。
第三:宏定义的常量符号M
在hello.i中被替换成了100,这就是 “符号替换”。
第四:如果去仔细比对stdio.h的原文件内容和hello.i的头文件展开的内容,就会发现,有一些代码在hello.i中没有了,所谓条件编译就是,代码在满足某种条件的情况下才会留下,否则就会在预处理阶段被直接删去。
编译阶段
在这个阶段中,gcc 进行语法分析、词法分析、语义分析、符号汇总等操作(在《编译原理》中有详细介绍),在检查无误后,gcc 把代码翻译成汇编语言,即,生成汇编。
命令为
gcc –S hello.i –o hello.s
选项-S
让 gcc 把C语言代码翻译成汇编后立即停止。
以下为文件内容对比图:
左侧为新生成的hello.s
文件,右侧为原本的hello.i
文件,如果了解过汇编语言就会发现,hello.s
文件上的写的是汇编代码。
汇编阶段
在汇编阶段,gcc会进行两个操作:一、将汇编代码文件翻译成可被机器识别的代码(机器指令),新生成的文件叫 “目标文件”;二、每个文件都对应生成一个符号表,为链接阶段符号汇总做准备。
命令为
gcc –c hello.s –o hello.o
选项-c
是让gcc在将汇编文件翻译成机器指令后立即停止,
以下为文件内容对比图:
左侧为新生成的hello.o
文件,右侧为原本的hello.s
文件,一般我们的编辑器的编码方式为utf-8,在该编码方式下是不支持查看二进制文件内容的,能看到的只是一串乱码。
那么,有没有什么方法查看该文件内容呢?
答案是,有的。.o
文件的格式为ELF,Linux上有个工具叫 “readelf”,可以帮我们查看该文件。
以下为使用命令readelf -s hello.o
输入的内容。
虽然大多数的内容都不理解,但是右下角却有几个熟悉的符号 “global_val”、“main”、“printf”等等。这些就是 “符号表的内容”。
链接阶段
汇编阶段完成后,就进入翻译的最后阶段,“链接”。
链接就是把源文件中用到的库函数从函数库中拉取库函数实现代码,将其加载到一起,再合并段表和符号表以及重定位符号,最后形成一个可执行程序。
链接结果如下:
这里涉及一个重要的知识点,“函数库”。
函数库
我们写C程序时,并没有定义printf
的函数实现,且在预编译中包含的stdio.h
中也只有该函数的声明,而没有定义函数的实现,那么,是在哪里实现printf
函数的呢?
最后的答案是:系统把这些函数实现都被做到名为 libc.so.6 的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径usr/lib
下进行查找,也就是链接到 libc.so.6 库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。
静态库和动态库
-
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为
.a
。 -
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为
.so
,如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。完成了链接之后,gcc 就可以生成可执行文件,如下所示。gcc hello.o –o hello
。 -
gcc默认生成的二进制程序,是动态链接的,这点可以通过 file 命令验证。
程序的运行
一般而言,生成可执行程序之后用到命令./
+可执行程序文件名
,如./hello
,意思就是执行当前工作目录下的可执行程序hello
。
但是,在《yum 篇》时说过,指令、命令、工具等都是可执行程序,虽然叫法多,但他们是一个东西。而想要执行一个可执行程序,首先要找到这个可执行程序,然后再执行。
因此,实际上命令为文件所在的路径
,而./
+可执行程序文件名
是为了方便,毕竟我们的项目文件一般都在一个目录下
gcc的选项
程序的翻译和与运行大致讲完了,这里就总结一下 gcc 常用的选项和功能:
-E
只激活预处理,这个不生成文件,你需要把它重定向到一个输出文件里面-S
编译到汇编语言不进行汇编和链接-c
编译到目标代码-o
文件输出到文件-static
此选项对生成的文件采用静态链接-g
生成调试信息。GNU 调试器可利用该信息。-shared
此选项将尽量使用动态库,所以生成文件比较小,但是需要系统由动态库.-O0
、-O1
、-O2
、-O3
编译器的优化选项的4个级别
-O0表示没有优化,-O1
为缺省值,-O3
优化级别最高-w
不生成任何警告信息。-Wall
生成所有警告信息。
Linux项目自动构建工具 - make/makefile
当一个项目有多个源文件,如test1.c、test2.c、test3.c……时该怎么编译链接形成可执行程序?
方法一:
- 分别编译每一个源文件
gcc -c test1.c test2.c test3.c……
(自动生成对应.o
文件) - 再统一链接生成可执行程序
gcc test1.o test2.o test3.o…… -o mybin
但是,如果源文件过多,如果说上百个,这个方法还好用吗?因此就有了方法二:也就是这篇博客的第二个重点,Linux项目自动构建工具 - make/makefile。
背景
- 会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。
- 一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
- makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
- make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
- make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。
依赖关系与依赖方法
依赖关系,通常指文件之间存在强相关。比如翻译过程中的hello
可执行程序是有源文件hello.c
一步步转换过来的,那就说明hello
依赖hello.c
。
依赖方法,顾名思义,就是解决问题的方法。如果把源文件编译链接形成可执行程序这个过程当作是一件事,那么gcc test.c -o hello
这条命令就是完成这件事的方法。
make与makefile的原理
说了那么多,那么make和makefile到底长啥样?实际上的作用到底是什么?
【makefile的基本内容}】
【makefile内容解释】
$^
:依赖文件列表。$@
目标文件。%.o
:当前目录下的所有.o
文件的展开。%.c
:当前目录下的所有.c
文件的展开。$<
:%.c
多代表的源文件,一个一个的拿出来,用gcc进行编译,形成同名的.o
文件。
【make命令使用】
[LJH@hecs-66624 LinuxLearning]$ ll
total 16
-rw-rw-r-- 1 LJH LJH 92 Aug 22 11:13 makefile
-rw-rw-r-- 1 LJH LJH 1157 Aug 21 19:43 mybin.c
-rw-rw-r-- 1 LJH LJH 33 Aug 21 19:33 mybin.h
-rw-rw-r-- 1 LJH LJH 85 Aug 21 19:39 test.c
[LJH@hecs-66624 LinuxLearning]$ make
gcc -c mybin.c
gcc -c test.c
gcc mybin.o test.o -o mybin
[LJH@hecs-66624 LinuxLearning]$ ll
total 36
-rw-rw-r-- 1 LJH LJH 92 Aug 22 11:13 makefile
-rwxrwxr-x 1 LJH LJH 8472 Aug 22 11:14 mybin
-rw-rw-r-- 1 LJH LJH 1157 Aug 21 19:43 mybin.c
-rw-rw-r-- 1 LJH LJH 33 Aug 21 19:33 mybin.h
-rw-rw-r-- 1 LJH LJH 2248 Aug 22 11:14 mybin.o
-rw-rw-r-- 1 LJH LJH 85 Aug 21 19:39 test.c
-rw-rw-r-- 1 LJH LJH 1384 Aug 22 11:14 test.o
[LJH@hecs-66624 LinuxLearning]$ make clean
rm -f *.o mybin
[LJH@hecs-66624 LinuxLearning]$ ll
total 16
-rw-rw-r-- 1 LJH LJH 92 Aug 22 11:13 makefile
-rw-rw-r-- 1 LJH LJH 1157 Aug 21 19:43 mybin.c
-rw-rw-r-- 1 LJH LJH 33 Aug 21 19:33 mybin.h
-rw-rw-r-- 1 LJH LJH 85 Aug 21 19:39 test.c
make 命令的原理:
-
make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
-
如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到
mybin
这个文件,并把这个文件作为最终的目标文件。 -
如果
mybin
文件不存在,或是mybin
所依赖的后面的mybin.o
、test.o
文件的文件修改时间要比mybin
这个文件新(可以用 touch 测试),那么,他就会执行后面所定义的命令来生成mybin
这个文件。 -
如果
mybin
所依赖的mybin.o
、test.o
文件不存在,那么make会在当前文件中找目标为.o
文件的依赖性,如果找到则再根据那一个规则生成mybin.o
、test.o
文件。(这有点像一个堆栈的过程) -
当然,你的C文件和H文件是存在的啦,于是make会生成
mybin.o
、test.o
文件,然后再用mybin.o
、test.o
文件声明make的终极任务,也就是执行文件mybin
了。 -
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
-
在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。
-
make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。