Linux相关概念和重要知识点(6)(make、makefile、gdb)
1.make、makefile
(1)什么是make、makefile?
在我们写完代码后,要编译运行,如果有多个.c文件就需要每次都自己用gcc -o来处理,这十分麻烦。当我们想要自定义多个文件的处理时,我们会浪费很多时间在重复的事情上,我们需要一种自动化的、可自定义文件处理方式的程序,make和makefile就是为此而生的。大型项目一定离不开make和makefile。
make和makefile联系紧密,make是一个命令,makefile是一个文件,这个文件以某种格式来自定义可执行程序的方式,当使用make时就调用这个makefile文件来处理。
(2)makefile结构
makefile本质是依赖关系和依赖方法的集合。举个例子,完成老师向学生下达任务这件事需要两个条件:首先就是这两个人得是师生关系,即依赖关系;其次就是下达任务这个动作,这叫依赖方法。要办成一件事,依赖方法和依赖关系缺一不可,首先建立依赖关系,其次传递依赖方法。
格式:(目标文件):(依赖文件) -> 依赖关系
(tab) (依赖方法)
类似于函数的传参和返回值,依赖文件类似传递的参数,而目标文件则是返回值,或者说是计划生成的文件。隔行后就可以开始写我们的依赖方法了,类似函数体。但是要注意每行依赖方法指令必须要顶格使用tab,就像python的函数一样,对缩进的要求很高。
下面是一个简单的示例。
当我们执行make时,就会根据makefile在当前目录下找test.c文件,类似函数的参数,目标生成的文件就是test。在依赖方法中我们显式写了目标文件和依赖文件,名字也符合规范。按照我们的指令一行行执行,最终生成指定文件。
我们发现,我们不仅将指令的结果打印出来了,还将指令的内容打印出来了。当我们在依赖方法每行前面加上@时,就能只打印结果而没有指令本身,这叫关闭回显。
(3)依赖关系
我们需要进一步理解依赖文件和目标文件的作用,根据上面的例子,有人肯定会觉得目标文件和依赖文件就等同于函数的参数和返回值,但事实并不是,它们和函数有很大区别
看一下下面的依赖关系和结果
我们发现,目标文件是a,没有依赖文件,但是实际执行指令时用到了test.c,生成的文件又是test,和目标文件a完全不沾边,这应当如何解释?
目标文件理解成一种建议生成的文件或是一种规范,规范更好,不规范也不会强制终止。因为在有的时候不需要生成文件,而目标文件一定要写,所以这是一种妥协。但是我们必须保证依赖文件必须能够找到,因为不需要依赖文件时我们可以不写,不会存在目标文件的那种情况。当执行依赖方法时,每一行指令其实都被展开到当前工作目录下执行,上面的gcc -o test test.c是一句完整的指令,它不需要依赖任何的依赖文件,也能合理地生成自己的文件,和目标文件也没有任何关系。
那么依赖文件和目标文件存在有何意义?
①声明依赖关系,实现不同功能的区分
在makefile我们可以写很多指令,实现不同功能,不同功能的指令都需要先声明依赖关系,再执行依赖方法。依赖文件和目标文件存在就是为了声明依赖关系。在目标文件和依赖文件没有实际意义时它更像一种标识,标志这个功能将要开始实现,也可以和其它功能的指令作出区分。
当我们使用make时默认执行第一个依赖关系下的依赖方法。要执行其它的需要make (目标文件)
在makefile里,目标文件名不能重复,否则会导致歧义。
②自动推导
在很多情况下依赖文件和目标文件是一种建议,是一种规范,规范必定带来好处,其中有一个就是自动推导。
当我们想要通过.c先生成一个.o,再生成一个可执行文件,那就可以利用自动推导来处理。
当make之后会自动执行第一个依赖关系,按照我们写的依赖文件在目录中查找,如果找不到,会将这个依赖关系先放入栈,再找目标文件是刚刚依赖文件的依赖关系,找到了如果发现依赖文件还是找不到,那就继续入栈,以此类推。如果一个依赖关系的目标文件和依赖文件都能在当前目录找到,那就依次出栈,执行指令。
注意推导到最后的依赖关系时依赖文件一定找得到,否则就报错,和前面的规则一样。
根据上面的规则,看一下下面的makefile,在make和make clean时结果是什么
结果
当make时,会根据目标文件和依赖文件入栈出栈,当我们递归生成文件时,就需要好好规范一下目标文件和依赖文件的写法,保证能够推导到其它依赖关系上。
③通配符
当我们不写依赖文件时,依赖方法里依然可以用到目录里面的文件,因为依赖方法的指令会被展开执行,既然显式写出了调用的文件(即构成了一句完整的指令),就能正常执行。
当我们有多个.c时,我们需要逐个对所有.c进行编译,当要处理的文件都有一个相同特征时,通配符就派上用场了,在makefile里,百分号%就是通配符的意思,和指令里的*一样
注意通配符对应的依赖关系很特殊,要么整个依赖关系都用通配符,要么都不用。并且make时是无法定位由通配符组成的通配符的依赖关系的,就算名义上它是第一个依赖关系。需要我们写一个自动推导的依赖关系。
④变量和自动变量
如果我们想要依赖关系列出来的文件和实际依赖方法使用的文件有较强相关性,可以使用自动变量。
$@代表目标文件,$^代表全部依赖文件,$<代表逐个依赖文件
定义的变量可以以$(变量)形式使用,变量和自动变量的使用可以帮助我们更统一的管理文件调用,生成的文件,极大降低了重复的工作
(4).PHONY伪目标
常规的依赖关系都会根据依赖文件和目标文件的Modify来决定是否需要执行。如果依赖关系里面的文件都有且目标文件的Modify时间更晚,那就不会执行。
.PHONY:(伪目标)声明伪目标,会自动忽视伪目标的时间比对。后续直接使用(伪目标):(依赖文件)来定义依赖关系即可。我们也可以用touch显式修改时间来绕过检查。
一个.PHONY声明一个伪目标
伪目标的作用就是为了执行一些指令,不要求生成文件生成
2.gdb
gdb是调试代码的工具,我们利用gdb调试的本质是帮助我们快速找出问题,然后自己解决。这里分享一些指令和注意事项
cgdb:我们可用cgdb来替代gdb,操作一致但代码有展示,更方便。
检查是否安装:gdb --version
调试前提:编译选项加上-g(debug,添加调试信息)。默认情况都是release,无调试信息
重复操作:gdb中直接回车默认执行上一条指令
显示代码:l (num)将第num行居中显示,l (函数名)居中显示当前函数名所在行,l (源文件):(函数名/行号)查看指定文件中的代码,注意每次回车都会执行l(翻页,每次10行)
打断点:b (行数/函数名)给指定位置打断点,info b可以查看自己打的断点,每个断点都有一个编号Num
打条件断点:b (行数) if (变量) == (值)打一个条件断点,只在if成立时截停。condition (断点编号) (条件) 可以给已有的断点添加条件
暂时禁用断点:enable (断点编号)打开断点,disable (断点编号)关闭断点
删除断点:d (断点编号)删除断点,要通过info b查看自己打的断点的编号Num。注意每个断点编号对应一次断点操作,不会复用已删除的断点编号
执行代码:r在没有断点时直接将代码运行完,有断点时执行至第一个断点。调试过程中再按r意味着重新启动调试
逐语句和逐过程:n逐过程(不会进入函数),s逐语句(会进入函数)
执行至下一次断点:c直接执行到当前断点的下一个断点
执行至指定行数:until (行数)跳过中间的代码,直接执行到指定行
按函数运行:finish能把当前函数跑完,如果期间有断点会被截停,main函数不行(最外围函数不支持)。断点 + finish + until + c这几个指令组合起来很有用,能帮助我们查找bug发生点,进而实现对大的代码块进行区间debug
调试中修改:set bar (变量)=(新值),直接修改代码中的变量的值,帮助我们在调试时能根据猜想及时检验问题,如果错了也可以继续修改,而不必退出gdb检验
监视:display (变量)每次执行都会监视变量,display (监视编号)关闭显示,p (表达式)临时显示当前变量或表达式的值。监视编号也是不可复用的,每次打印监视变量就会打印其监视编号
检测变量:watch (变量)检测变量的值是否变化,它是watchpoint,能用info b查看,也能d删除。如果变量数值变了,就会显示出来,而不像display那样每次都显示
查看当前函数栈帧:bt显示逐语句位置,显示每层函数调用位置
自动变量:info locals显示当前栈帧定义的变量信息