一.make/makefile
1.1make/makefile的概念
Make 是一个经典的构建自动化工具,主要用于管理源代码的编译过程。Makefile 是 Make 工具的配置文件,包含了一系列规则,告诉 Make 如何构建项目。简单来说,我们日常在写代码时,有时我们写的文件包含.h头文件和.cpp的源文件,但是最后我们程序运行的时候只会生成一个可执行文件,而整合这些文件并形成可执行文件的工作就是由make/makefile共同完成的。
而它们叫做自动化构建工具的原因是当makefile写好内容后,只需要一个make命令,整个⼯程就会完全⾃动编译,极⼤的提⾼了软件开发的效率。
1.2基本的make/makefile
下面我们来看一看基本的make/makefile是怎么写的:




我们要完成一个简单的make/makefile需要先创建一个makefile的文件,并在里面写出第二幅图所示的代码保存并退出,最后退出vim,执行make指令。
我们此时就会发现执行了gcc的那条指令,并且我们也看到了确实创建出了test.exe的可执行文件。
那么第二幅图所示的代码究竟是什么意思呢?为什么make指令执行后就会出现上述的现象呢?
要解决上面的问题我们首先要了解什么叫依赖关系和依赖方法:

依赖关系指的是目标文件(target)与它所依赖的文件(dependencies)之间的关系。在Makefile中,依赖关系描述了构建一个目标需要哪些前提条件。
依赖方法(也称为recipe或命令)是构建目标时需要执行的具体命令。它紧跟在依赖关系行之后,必须以Tab键开头。
第一行的就是依赖关系,也就是test.exe可执行文件依赖于test.c文件,而make指令会解析依赖关系,解析后就会执行依赖方法。

我们只写这一个程序并不能完全体现make是如何解析依赖关系的,我们用上一篇所讲的形成可执行文件的四个过程来体现:



我们用上面的四步操作同样实现了生成test.exe可执行文件的过程,但是执行make指令是我们发现它执行指令的顺序并不是我们所写顺序,这是为什么呢?
原因就是make指令在解析依赖关系时会形成推导栈,将所有的依赖方法都入栈,最后按照出栈顺序一个个执行指令,而栈的特性是后进先出,所以才会出现上面的现象,我们可以用图来展示这个个过程:

通过上图,想必都能理解make解析的过程,而推导栈就是依赖方法的集合。
1.3makefile的基本语法
1.3.1 .PHPNY


我们在日常运行代码时,会产生一些临时文件,而我们要再一次要运行程序时是需要将那些临时文件给清理掉,再重新生成新的临时文件,这个过程想必大家都清楚。
而在makefile中我们通过上面的语法:.PHONY来实现清理的操作,那么这个语法是什么意思呢?
.PHONY语法的作用就是声明一个符号为伪目标,而伪目标的本质是总是被执行的!
这是什么意思呢?我们来看一个对比:

上面的生成test.exe可执行文件的程序我们并没有用.PHONY来进行修饰,我们发现它的依赖方法只会执行一次,再次执行就会显示当前的可执行文件已经是最新的,而经过修饰的clean文件可以一直执行,这就是总是被执行的意思。
这里有人可能会有问题:清理操作多次执行我能理解,可以打扫多次,那么上面的生成.exe可执行文件为什么不用.PHONY来修饰呢?
这是因为我们上面的.exe文件生成之后内容并没有发生改变,编译一次即可,为什么要重复编译呢?
内容没有发生改变还重复编译,那么就会导致编译效率的降低,所以gcc检查文件内容没有发生改变的情况下就不会再让其再次编译。换句话说就是如果文件内容发生了改变,就可以再次编译。
如上图所示,我们在执行make clean后,文件整个都没了,那么就相当于文件内容发生了改变,那么也是可以执行的:

1.3.2 .PHONY的细节问题
在上面的操作中,我想不只有上面一个问题,还有很多细节问题,所以我们来总结一下: 细节一:依赖关系必须存在,依赖文件列表可以为空
细节二:依赖方法可以是任何的shell命令
细节三:clean目标,只是利用了make的自动推导能力让它执行了rm指令,在构建工程的视角,看起来就像是清理项目,本质就是删除不需要的临时文件。
细节四:make命令后面可以跟文件名,后面跟谁,就解析谁的依赖关系和依赖方法,而上面我们没有跟文件名依旧可以执行生成.exe文件的操作是因为make命令默认会解析推导第一个依赖关系和依赖命令。
并且make命令一次只会解析推导一条完整的依赖关系链,这就是为什么解析推导完生成.exe文件的操作后并没有解析推导clean的依赖关系和依赖方法。
细节五:.PHONY总是可执行的操作是怎么做到的?为什么上面的生成.exe文件的操作gcc只能执行一次,无法重复编译老代码?


我们先说后面的问题,其实gcc判断的标准就是看test.c和test.exe两个文件的Modify时间,Modify就是文件新建或最后修改的时间,我们从上图可以看到test.c的Modify时间相比test.exe要早,所以此时想要再次执行编译生成.exe文件的指令就无法做到。

我们可以看这个时间轴来更好地理解,只要src.c的Modify时间比src.exe要早,就无法执行编译生成.exe文件的指令,只有修改src.c的内容或者删除src.exe并再次新建,删除并重建的方式我们上面已经看过了,下面我们来看另一种方式:



我们上面对test.c内容做出了修改,可以看到Modify时间此时已经比test.exe的要晚了,那么此时我们make就没有问题。
注意执行完make指令后test.exe的Modify时间就会更新,时间就会再次比test.c要晚。
讲到这里想必就能知道为什么被.PHONY修饰的文件总是能被执行,就是因为被.PHONY修饰后,gcc就会忽略Modify时间的差异,这样就能做到总是被执行。
这里可能还有人会有疑问:为什么Change和Modify都是修改的意思,反而要看Modify时间而不看Change的时间呢?
我们要知道文件 = 文件内容 + 文件属性,而Modify指的是对文件内容的修改,而Change指的是对文件属性的修改,上面的操作没有针对文件属性进行修改,所以不能拿它作为标准来看。

上面我们对文件属性进行修改,此时的Change时间就发生了改变,并且有个有意思的现象:当Modify的时间修改时,Change时间也会同步修改,二者时间会变的一样,但是当Change时间修改时,Modify时间并不会发生变化,这是为什么呢?
这是因为Modify时间也是属于文件属性啊,文件属性发生修改Change时间当然要发生改变,反过来Change时间是文件属性,并不属于文件内容,所以当Change时间修改时,Modify时间并不会发生变化。
最后的Access时间指的是文件最后被访问的时间,通过cat,less等指令都可以刷新这个时间,但是我们重复通过指令来刷新这个时间我们会发现它没有被刷新,这个时间刷新的标准有的是根据时间的间隔,也就是一段时间后才能刷新,有的是重复多次使用指令才会更改,是按次数来算。
1.3.3makefile的其他语法
1. @符号的使用


我们在执行make相关的指令时,会将我们要执行的指令给回显出来,我们如果不想让其回显出来可以这样做:


我们在依赖方法前面加上@就可以隐藏我们要执行的相关指令。
2.变量名的使用
在makefile中也是可以用变量名来操作:

我们用BIN和SRC来代表两个文件,在依赖关系这一行就要写入()中,这是因为在Linux中所有的变量对它而言都是字符串,我们将其写入()就是提取里面的内容。
并且补充两个小技巧:我们在依赖方法中将test.c替换为了^,将test.exe替换为@,^代表的就是提取依赖文件列表,@代表的就是提取目标文件。
这里用变量名来进行操作可能有人觉得没有必要,那是因为现在只有一个文件,如果我们有100个文件,目标文件都是test.exe,但是我现在让你把文件名改为hello.exe,如果用变量名的话我们只需要改第一行的内容即可,不用变量名我们就只能一个一个修改,效率就会大大降低,所以用变量名来操作会提高我们编程的效率。
3.其他符号的使用


比如我们现在要将上面的所有文件整合,生成一个.exe可执行文件,我们这时肯定要利用变量名来解决问题,不然在依赖文件列表我们就要一个一个写出所有的.c文件,那太耗费时间了,所以我们可以这样做:

wildcard *.c的意思是当前目录下所有以.c为后缀的文件名,写入$()中就是提取这些文件名,这样我们就完美的解决了上面的问题,大大提高了效率。
我们再延伸一下:我们要生成最后的.exe文件,就要用到.o文件,那么该如何将所有的.c文件转化为.o文件呢?

首先我们要先提取所有.o文件名,上面第一行代码就是将SRC中的所有.c文件名全部改为.o。
而我们只提取了.o的文件名,但是实际上并没有.o文件,所以最后一步就是生成所有.c文件相应的.o文件。%.c的意思是展开当前目录下所有的.c文件,%.o是同时展开同名.o文件,$<的作用是对展开依赖的.c文件,一个一个执行gcc的操作,这样每一个.c文件都会生成相应的.o文件。

我们同时也可以对clean操作进行相应的修改:

这样clean时就可以删除所有的.o文件和.exe文件:

2.调试器gdb/cgdb
2.1gdb和cgdb的概念
GDB 是 GNU 项目的一部分,是一个功能强大的命令行调试工具,支持多种编程语言(如 C、C++、Rust 等)。它允许开发者在程序运行时检查变量、设置断点、单步执行代码等。
CGDB 是 GDB 的增强版,提供了一个基于终端的图形化界面(类似 Vim 的分屏模式)。它保留了 GDB 的全部功能,同时增加了代码浏览的便捷性。
2.2启动gdb



上面我们就是写了一个案例来进行调试,而最后一张图就是启动gdb的命令,要对一个可执行文件进行调试的命令就是:gdb + 文件。
但是此时我们还不能使用,为什么呢?

这里就提示我们没有找到debug的信息,debug模式相信大家应该都知道,在写代码时有debug和release两个模式,只有在debug模式下才能进行调试。
而上图的意思就我们不在debug模式下,后面输入调试相关的命令是无法使用的,也就是说我们所生成的可执行文件默认是relese模式。
那么该如何生成debug模式下的可执行文件呢?
其实很简单:

只需要在正常的命令后加上 -g 即可。

此时我们的可执行文件已经在debug模式下了,所以最后一行就不会再提示我们找不到debug的信息了。
下面我们就可以执行调试相关的命令了:

上面就是利用 l 的命令来展示我们的代码,一次只能展示一部分,但是这样调试的话就太难受了,很不方便,所以我们一般不使用gdb来调试,而是使用cgdb来进行调试。

当我们使用使用cgdb调试时,进来就是上图所示的画面,通过分屏操作,上面就是我们的代码,下面就是调试的窗口,可以输入调试的相关命令,所以下面我们就以cgdb来掩饰,gdb和cgdb的命令相同的。
2.3gdb/cgdb的相关命令
2.3.1 list/l命令
list/l的相关命令有三个:
list/l:显⽰源代码,从上次位置开始,每次列出10⾏
list/l + 函数名:列出指定函数的源代码
list/l + 行号:列出指定⽂件的源代码,这个行号的意思是列出以输入的行号为中心的部分代码


上面就是后面两个命令操作后的结果,list/l的相关命令在gdb使用比较多,毕竟看不见代码,在cgdb就用的没那么多了,此时我们就能看见自己写的代码了,没必要在展示代码。
2.3.2 break/b命令
我们调试必不可少的就是打断点,所以break/b命令就是设置断点,相关的命令有两个:
break/b:在指定行号设置断点
info break/b:查看当前所有的断点信息


上图我们使用b命令设置断点,被设置断点的行号会高亮,便于我们看到。

我们通过info b命令就可以看到我们所设置的断点信息,包括断点所处的行号等相关信息。
2.3.3 delete/d命令
既然能设置断点,就能删除断点,d命令就是用来删除断点的,相关命令有两个:
d:删除所有断点
d + n:删除序号为n的断点,这个n可不是行号,而是我们在设置每个断点时前面会有一个Num,这个数字才是n,就如上面我们通过info b查看断点的信息时,头一个信息就是Num。


2.3.4 run/r命令
r命令就是让程序开始连续执行,就是直接让代码跑起来:

上图我们通过r命令直接让程序执行了一遍,后两行就可以看到我们程序执行之后所打印出的内容。
2.3.5 next/n和step/s命令
这两个命令就是我们常用的逐过程(F10)和逐语句(F11),这两个的作用想必大家都知道,并且这两个命令要配合着断点来进行使用,我们来看:



首先我们要打个断点,然后通过r命令让程序运行到的断点位置处,此时我们就可以使用n或者s命令来进行调试,并且我们输入一次命令之后,后面就不用再输了,直接回车就能接着上一步的操作。
如果我们没有打断点,就会出现下面的情况:

这句话的意思就是说程序没有执行起来,就没有办法在使用n或者s命令。
2.3.6 print/p命令
我们调试中也很关键的一部分就是观察我们所定义的变量的变化,而p命令的作用就是如此:

使用方法就是在p后面加上我们要观察的变量,但是一次只能观察一个变量,并且不能便调试边观察,就如上图所示我们看完当前循环的i和result,要想接着调试,就得重新输入n或者s。
这种观察变量的方式很不方便,所以下面就有更好用的命令。
2.3.7 display/undisplay命令
上面我们说了比p命令好用,那么好用在哪儿呢?我们来看:


我们用过display来展示我们要观察的变量,再通过s或者n调试时,我们只需要按下回车,不但能让程序往下走,同时每次回车都能看到我们要观察的变量的变化,比之上面的p命令要方便的多。
那既然都能展示我们要观察的变量,自然也能删除我们不需要观察的变量,undisplay作用就和它的意思一样,不展示某个变量。
但是和display不同的是,display后面跟的是变量名,undisplay后面跟的不是变量名,而是编号,和断点一样。
如上图所示,我们在调试时,我们观察的变量前面会有1,2等编号,undisplay后面跟的就是这个编号,我们来看:

此时我们undisplay 1,也就是不展示i变量,如上图所示,后面我们再进行调试,就不再显示i变量了。
2.3.8 until命令
这个命令的作用就是直接让程序执行到某一行,我们来看:

如上图所示,我们如果不想一次一次循环来观察变量的变化,可以直接使用until + 行号的命令,直接就运行到了循环结束的部分,可以直接观察到变量最终的结果。
但是until毕竟是全局的变化,我们要先观察局部的变化,比如:我们的代码此时出问题了,经过排查是在某个函数里面出的问题,但是我们不想让程序直接运行到函数结束的部分,而是单单只想观察这个函数执行完的结果,那么就可以用下面的命令。
2.3.9 finish命令


我们只需要输入finish命令,就可以直接执行到当前函数返回,然后停止,finish相比until就是观察局部的变化。
2.3.10 continue命令
continue在循环中的作用想必大家都清楚,跳过某次循环,而在调试中continue的作用就是从一个断点跳到另一个断点,我们来看:






如上图所示,我们通过continue命令,可以实现从当前位置开始连续执行程序,也就是从一个断点跳到下一个断点的位置。
以上就是基础开发工具(二)的全部内容。