第五部分:Linux中的gcc/g++以及gdb和makefile
目录
1、编译器gcc和g++
1.1、预处理(进行宏替换)
1.2、编译(生成汇编)
1.3、汇编(生成机器可识别代码)
1.4、连接(生成可执行文件或库文件)
1.5、gcc编译器的使用
2、Linux调试器-gdb使用
2.1、debug和release
2.2、gdb的使用
3、自动化构建工具make/Makefile
3.1、背景
3.2、makefile/Makefile编写
3.3、make的使用
3.4、原理
4、一些补充
1、编译器gcc和g++
gcc和g++分别是C语言和C++语言的编译器,这两个编译器的使用基本上是一样的,所以在下面只讲gcc,也就是C语言的编译器。
一个写好的C语言程序,并不能立马被执行,需要经过以下几个过程,才能生成可执行程序。编译器gcc的作用就是将写好的C程序变成可执行程序。
1. 预处理(进行宏替换)
2. 编译(生成汇编)
3. 汇编(生成机器可识别代码)
4. 链接(生成可执行文件或库文件)
1.1、预处理(进行宏替换)
预处理功能主要包括宏定义,文件包含,条件编译,去注释等。
预处理指令是以#号开头的代码行,这个阶段不会对语法进行检测。
例如:
gcc -E test01.c -o test01.i
其中以.i为后缀的文件为已经过预处理的C原始程序。
我们可以使用vim打开test01.i文件,来查看经过预处理后代码发生了什么变化。
1.2、编译(生成汇编)
在这个阶段中,gcc 首先要检查代码的规范性、是否有语法错误等,以确定代码的实际要做的工作,在检查无误后,gcc 把代码翻译成汇编语言。
例如:
gcc -S test01.i -o test01.s
其中以.s为后缀的文件就是经过预处理和编译的C语言程序。
我们也是可以使用vim打开test.s文件,来查看代码发生的变化。
1.3、汇编(生成机器可识别代码)
汇编阶段是把编译阶段生成的“.s”文件转成目标文件。
例如:
gcc -c test01.s -o test01.o
其中以.o为后缀的文件就是经过预处理、编译、汇编后生成的目标文件。
此时,如果我们使用vim打开test01.o文件的话就会看到一堆乱码,因为里面都是二进制,所以打开后会是乱码。我们可以使用od命令来查看二进制文件。
语法:od 文件
功能:将其内容以八进制字码呈现出来。
注意:虽然目标文件已经是二进制的文件了,但是仍然需要经过链接才能执行。
1.4、链接(生成可执行文件或库文件)
在成功编译之后,就该进入链接阶段。
例如:
gcc test01.o -o test01
生成的test01文件就是可执行文件。可以使用:
./test01
来执行该可执行程序。
一个重要的概念:函数库(关于细节后面的内容再详细讲)
我们的C程序中,并没有定义“printf”的函数实现,且在预编译中包含的“stdio.h”中也只有该函数的声明,而没有定义函数的实现,那么是在哪里实“printf”函数的呢?(gcc和g++默认的头文件搜索路径就在/usr/include/)。
答案是:头文件提供方法的声明,函数库提供方法的实现。系统把这些函数实现都放到了名为 libc.so.6的库文件中去了,在没有特别指定时,gcc 会到系统默认的搜索路径/usr/lib或者/usr/lib64下进行查找(/usr/lib主要包含32位库,而 /usr/lib64则包含64位库),也就是链接到 libc.so.6库函数中去,这样就能实现函数“printf”了,而这也就是链接的作用。
函数库一般分为静态库和动态库两种:
在Linux中动态库一般是以.so为后缀的,静态库是以.a为后缀的,在Windows中动态库一般以.dll为后缀,静态库一般以.lib为后缀。
静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。
静态链接的程序优点是不依赖库,程序可以独立进行运行,缺点是体积比较大,比较消耗资源。
注意:我们的云服务器上默认只会安装动态库,静态库默认是没有的。可以使用下面的命令安装静态库。
下载C语言静态库:
sudo yum install -y glibc-static
下载C++静态库:
sudo yum install -y libstdc++-static
动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库。
动态链接的程序更加节省资源(磁盘资源、内存资源、网络资源等),缺点是动态库一旦缺失会导致各个程序都无法运行,因为会影响很多程序,包括Linux系统里面很多命令都没法运行。
如前面所述的 libc.so.6 就是动态库。gcc 在编译时默认使用动态库。
gcc默认生成的二进制程序,是动态链接的,这点可以通过之前讲的file命令验证。
例如:
file test01
运行结果中“dynamically linked”就是指动态链接。
除了file命令外,还可以使用ldd命令来查看可执行程序所依赖的动态库,例如:
ldd test01
从运行结果便可知道是静态链接还是动态链接。
1.5、gcc编译器的使用
语法:gcc [选项] 要编译的文件 [选项] [目标文件]
功能:编译C语言程序。
选项:
-E:只激活预处理。也就是让 gcc 在预处理结束后停止编译过程。
-S :该选项只进行编译而不进行汇编,生成汇编代码。
-c :编译到目标代码。使用选项“-c”就可看到汇编代码已转化为“.o”的二进制目标代码了。
-o :后面跟目标文件,指处理后输出到哪一个文件。
-static : 此选项对生成的可执行程序采用静态链接。
注意:如果没有动态库,只有静态库,也是可以使用gcc的,gcc是可以找到静态库的,其实默认情况下,形成的程序既不是纯的动态链接也不是纯粹的静态链接,而是混合式的。如果加上了-static选项就会变成纯的静态链接。
-g :生成调试信息。GNU 调试器可利用该信息。(该选项在下面的调试部分讲)
-w :不生成任何警告信息。
-Wall: 生成所有警告信息。
2、Linux调试器-gdb使用
2.1、debug和release
程序的发布方式有两种,debug模式和release模式
Linux gcc/g++编译出来的二进制程序,默认是release模式
要使用gdb调试,必须在源代码生成二进制程序的时候, 加上-g选项。例如:
gcc test01.c -o test01 -g
也就是说在生成可执行程序时,要带上调试信息,才能使用gdb进行调试。
扩展:可执行程序有自己的二进制格式,叫做ELF格式,我们可以使用readelf命令查看这种格式,例如:
readelf -S test01
2.2、gdb的使用
语法:gdb 可执行文件
功能:调试可执行程序。
退出: ctrl + d 或 quit 或 q。
调试命令:
注意:gdb会自动记住历史命令,如果想要执行上一个命令,直接按enter键即可。
list 或 l +行号:显示可执行程序行号上下共10行源代码,默认每次接着上次的位置往下列,每次列10行。
list 或 l +函数名:列出某个函数的源代码。
r 或 run:运行程序。
break 或 b + 行号:在某一行设置断点。
break 或 b + 函数名:在某个函数开头设置断点。
注意:还可以使用下面的方式进行打断点,表示给mycode.c文件的第19行打断点,通过这种方式就可以操作多文件的调试了,例如:
b mycode.c:19
n 或 next:也就是逐过程执行。
s 或 step:也就是逐语句执行。
info break 或 info b或i b或i break:查看所有断点信息。
info break或 info b 或i b或i break+断点的编号 :查看某断点信息。
注意:断点的编号是指下图的Num下面所的序号。
finish:执行到当前函数返回,然后挺下来等待命令。当我们使用step进入某个函数后,通过观察发现这个函数没啥问题,现在想要从这个函数跳出去,而这个函数比较长,如果使用next命令单步执行到函数末尾的话就有点太慢了。 此时可以使用 finish 命令直接执行完这个函数,返回到被调用的地方。
print 或 p +变量:打印表达式的值。
例如:
p i
表示打印变量i的值。再比如:
p &i
打印变量i的地址。
set var + 变量名=值:修改变量的值。
例如:
set var i=10
把 i 变量的值改为10。
注意:这个修改变量的值是在调试的过程中修改的,不会改变源代码和可执行程序。
continue 或 c:也就是从一个断点执行到另一个断点。
delete breakpoints:删除所有断点。
delete breakpoints +断点的编号n:删除序号为n的断点。
disable breakpoints 或 enable breakpoints:禁用或启用所有断点。
disable breakpoints 或 enable breakpoints +断点编号:禁用或启用某个断点。
注意:禁用断点和删除断点不同,禁用只是不让断点发挥作用,但是断点还在;删除断点,断点就没了。
display 变量名:跟踪查看一个变量,每次停下来都显示它的值。
undisplay 序号:取消对先前设置的那些变量的跟踪。
注意:这个序号指的是变量名前面的序号,比如下面图片中的1。
until +行号:跳至某行,只能向后跳,不能向前跳。
bt:查看各级函数调用及参数,也就是查看函数的调用链。
info locals或i locals:查看当前栈帧局部变量的值。
3、自动化构建工具make/Makefile
3.1、背景
make是一个命令,makefile或Makefile是当前目录的一个文件,会不会写makefile或Makefile,从一个侧面说明了一个人是否具备完成大型工程的能力,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile或Makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作。
makefile带来的好处就是“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编 译,极大的提高了软件开发的效率。
make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE(集成开发环境)都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile或Makefile都成为了一种在工程方面的编译方法。
make是一条命令,makefile是一个文件,两个搭配使用,完成项目自动化构建。
3.2、makefile/Makefile编写
首先先创建一个makefile或Makefile文件,然后使用vim打开该文件进行编写。
例如:此处只是举个例子,实际编写完全可以写的更精简一些。
#定义变量
cc=gcc
option=-Wall -g -std=c99
#定义目标
target=test01
object=test01.o
#默认目标
all:$(target)
#依赖关系和依赖方法
$(target):$(object)
$(cc) $(option) -o $@ $^
%.o:%.c
$(cc) $(option) -c $<
#清理目标
#.PHONY:clean
clean:
rm -f test01
变量定义:
Makefile 中的变量可以在整个文件中使用,便于管理和修改编译器等等
使用=来定义变量,使用 $(NAME)
来引用变量。
例如:
CC = gcc # 编译器
option=-Wall -g -std=c99 # 编译选项
再比如:
target=test01 #目标文件
object=test01.o #依赖文件
依赖关系和依赖方法
目标是你希望 make 命令帮助你生成的文件,如可执行文件等。
目标后面可以跟着一个冒号(:
)和依赖项。
例如:
all: $(target) # 默认目标
规则的格式为:
target: dependencies
command
特殊变量:
$@:目标名称。
$^
:所有依赖项的名称(以空格分隔)。
$<
:第一个依赖项的名称。
例如:
$(target): $(object)
$(CC) $(option) -o $@ $^
%
是一个通配符,表示相同的前缀。可以减少重复的代码,适用于一类文件的编译。
例如:
%.o: %.c
$(CC) $(CFLAGS) -c $<
clean是自定义的目标,不生成任何文件,通常用于清理构建过程生成的文件。
使用 -f
选项可避免错误提示。
例如:
clean:
rm -f $(TARGET) $(OBJECTS)
包含其他文件
你可以使用include 指令包含其他 Makefile,这样可以组织更复杂的项目。
例如:
include common.mk
伪目标
伪目标是指不生成文件的目标,通常用于执行清理操作等。
定义伪目标clean时可以用 .PHONY,这样即使该目录下有同名文件也不会出现混淆。
此外,.PHONY还可以用来修饰其他地方,表示总是可以被执行的(也就是说,不管源文件和目标文件的新旧关系如何,总是要执行的,具体内容看下面的原理部分)。
例如:
.PHONY:clean
3.3、make的使用
将makefile或Makefile编写好后,就可以使用make命令了。
make会自顶而下扫描makefile或Makefile文件,把所要形成的第一个目标文件充当为make的默认动作,也就是谁在前面,默认的动作就是谁。当然,我们也是可以使用make + 目标文件的这种方法来显示指定make的动作。
例如:
make
表示执行第一个目标文件的动作。
例如:
make clean
表示执行清理动作。
3.4、原理
一般而言,一定是源文件形成可执行文件,先有源文件,再有可执行文件,所以一般而言,源文件的最近修改时间比可执行文件要老。如果我们更改了源文件,历史上曾经还有对应的可执行文件,那么源文件的最近修改时间一定比可执行程序新。make会根据源文件和目标文件的新旧,来判断是否需要重新执行对应的依赖关系和依赖方法。
简单来讲就是:如果源文件老于可执行文件,则不需要重新编译,make不会执行;如果源文件新于可执行文件,则需要重新编译,make会执行。
我们可以使用stat 文件名的方式来查看一个文件的相关时间,比如最近修改时间或最近访问时间,以及文件按最近变化的时间,例如:
stat test01.c
注意:文件=文件内容+文件属性
在上面命令的运行结果中,其中modify所对应的时间是指文件内容的最近修改时间;change所对应的时间是指文件属性最近改变的时间;其中access对应的是最新的访问时间,但是受效率的影响,文件的访问时间并不会因为一次访问而改变,而是有着某种修改策略。
我们可以使用touch 已存在文件的方式把上面的三种时间都改为当前时间,例如:
touch test01.c
关于touch更多的使用方式,可以自行查阅。
1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
2. 如果找到,它会找文件中的第一个目标文件(target)。
3. 如果目标文件不存在,或是所依赖的文件的文件修改时间要比目标文件新,那么,他就会执行后面所定义的命令来生成目标文件。
4. 如果目标文件所依赖的文件不存在,那么make会在当前文件中找目标文件所依赖的文件的依赖性,如果找到则再根据那一个规则生成目标文件的依赖文件。
5. 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。
6. 在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错, 而对于所定义的命令的错误,或是编译不成功,make根本不管。
7. make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起, 我就不工作啦。
8.此外,工程是需要被清理的,像clean这种,没有与第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。如果我们把clean放在了makefile或Makefile的开头的话,也是可以直接使用make来执行清理工作的。
注意:make执行的时候,会回显执行的命令,如果不想要回显的话,可以在依赖方法前加上@符号,这样就不会回显所执行的方法了。
注意:.PHONY修饰clean是为了避免该目录下存在同名文件而导致make clean无法执行,修饰其他地方是为了make可以重复执行,而不考虑时间问题,也就是说总是可以执行的。
4、一些补充
回车和换行不是一个概念:
回车是指光标移动到当前行的最左侧。
换行是指光标移动到下一行。
在C语言中,'\n' 其实执行的是回车+换行;'\r' 执行的是回车。
文件缓冲区概念(缓冲区的具体概念后面再讲),所谓的文件缓冲区就是内存的一块区域,从内存向磁盘输出的数据会先到缓冲区,缓冲区装满后或者刷新缓冲区再把数据送到磁盘上。(之前的C语言文章提过一点,可以去看C语言文件操作)。
例如:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world");
sleep(10);
return 0;
}
编译运行后,我们会看到hello world并未立即输出到显示屏上,而是等待了10秒后,才显示出来。我们可以使用fflush()函数来刷新缓冲区,例如:
#include<stdio.h>
#include<unistd.h>
int main()
{
printf("hello world");
fflush(stdout);
sleep(10);
return 0;
}
编译运行后,便会观察到hello world会立马输出出来。
其中sleep和fflush函数可以通过我们之前学过的man命令来查看如何使用。
注意:我们现在写的一些程序用的是linux的接口,这就导致我们写的一些程序是没法在Windows上的VS上面运行的,要注意这一点,今后有关Linux的文章基本都是如此。