????博客主页: 小镇敲码人
????代码仓库,欢迎访问
???? 欢迎关注:????点赞 ????????留言 ????收藏
???? 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。????????????
❤️ 什么?你问我答案,少年你看,下一个十年又来了 ???? ???? ????
Linux学习工具篇之make与gdb
- Makefile/make
- Makefile/make是什么
- 为什么要有Makefile/make
- 如何编写Makefile文件
- Makefile文件的基本内容
- Makefile文件的基本格式
- 编写简单的`makefile`文件
- 变量和伪目标
- make是如何工作的
- 认识一下时间
- gdb调试工具
- 安装gdb
- 使用gdb
- 总结gdb常用的指令
- Release版本和Debug版本
- 指令的使用
前言:上篇博客,我们学习了Linux中的编辑器
vim
和编译器gcc/g++
,今天这篇博客,我们来介绍一下项目自动化构建工具make
/Makefile
。
Makefile/make
Makefile/make是什么
-
make
make
是一个项目构建工具,主要用于方便地编译、链接多个源代码文件。它能够自动决定哪些源文件需要重新编译,从而高效地构建项目。make
普遍用于处理C/C++
项目,但也可以用于其他编程语言的项目。make
工具通过读取和执行Makefile
中的指令来完成项目的构建过程。 -
Makefile
是一个文件的名称,它可以被
make
工具识别,执行这个文件中的命令。- 它定义了一系列的规则来指定哪些文件需要先编译,哪些文件需要后编译,以及哪些文件需要重新编译等。这些规则使得整个项目的编译过程变得自动化。
-
Makefile
的命名规则:Makefile
的命名不区分大小写,但通常使用“Makefile”作为文件名,以便与GNU make的默认查找顺序一致。-
make
命令会在当前目录下自动查找名为“GNUmakefile”、“makefile”或“Makefile”的文件。
-
为什么要有Makefile/make
下面是Makefile/make
的常见优势:
- 自动化编译:一旦写好
Makefile
,只需要一个make
命令,整个工程就可以完全自动编译,极大地提高了软件开发的效率。 - 结构化脚本:
Makefile
把很多行命令分成了若干个target
,使得脚本更加结构化。 - 选择性更新:
make
能够判断哪些文件需要重新编译,从而避免无意义的重复编译。 - 跨平台兼容性:虽然
make
主要用于Unix-like
系统,但也可以通过配置在不同平台上使用。
如何编写Makefile文件
Makefile文件的基本内容
通常
Makefile
文件由若干条规则组成,每条规则包括了目标、依赖和更新方法。
- 目标:通常是文件名,也可以是伪目标(如
clean
),代表要生成或执行的文件或任务。- 依赖:生成目标所需要的文件或其他目标。
- 更新方法:生成目标的命令序列,每条命令必须以制表符(Tab键)开始。
Makefile文件的基本格式
我们已经知道Makefile文件是由一条条的规则组成,每条规则由三部分组成:目标、依赖和更新方法。
目标文件(target): 依赖文件(prerequisites)...
执行命令(command)
- 这上面的规则的第一行是依赖关系,第二行是依赖方法。依赖关系告诉
make
为什么要帮你生成目标文件,因为存在依赖关系。而依赖方法告诉make
应该如何生成目标文件,要执行相应的指令。
编写简单的makefile
文件
-
生成简单的单个可执行文件。
伪目标是不对应实际文件的目标,它们通常用于执行特定的命令,如清理构建文件。为了避免与同名文件冲突,你可以使用
.PHONY
声明伪目标:main: main.o gcc -o main main.o main.o: main.c gcc -c main.c .PHONY:clean clean: rm *.o main
-
在上面的例子中
main
是最终要生成的目标文件,它依赖于main.o
,main.o
又依赖于main.c
,clean
是一个伪目标,用于删除构建过程中生成的文件。 -
使用
make
指令后,会先执行下面的方法生成main.o
,最后执行上面的方法生成可执行文件main
。 -
如果不清理旧文件,我们的文件也没有更新内容,执行
make
指令不会做任何事情。只要可执行文件的生成时间比所有的源文件的最近修改时间都要新,就证明它的最新的可执行文件。-
make clean
:清理构建项目中生成的文件。
-
-
-
生成多个可执行文件。
如果你的项目中有多个源文件,并且你想生成多个可执行文件,你可以这样编写Makefile:
prog1: prog1.o utils.o gcc -o prog1 prog1.o utils.o prog1.o: prog1.c gcc -c prog1.c utils.o: utils.c gcc -c utils.c prog2: prog2.o utils.o gcc -o prog2 prog2.o utils.o prog2.o: prog2.c gcc -c prog2.c .PHONY:clean clean: rm *.o prog1 prog2
-
make
之后,我们发现make
工具只构建了一个目标。这是因为make工具从上到下扫描makefile
文件,默认形成的是第一个目标文件,默认只形成一个,但它为了形成这个目标文件会把它依赖的文件都形成。 -
我们可以定义一个伪目标
all
,它依赖prog1
和prog2
,make会帮助我们构建第一个出现的目标,这样我们就能构建prog1
和prog2
了,在先前的makefile
文件的首部增加下面的规则。.PHONY:all all:prog1 prog2
-
make clean
后再make
这次同时生成了两个可执行文件:
-
-
使用自动变量
$@
和$^
来写Makefile
文件。-
$@
:表示当前规则中的目标文件。 -
$^
:表示当前规则中所有的依赖文件列表,这些文件是构建目标文件所必需的。修改刚刚的
Makefile
文件如下:.PHONY:all all:prog1 prog2 prog1: prog1.o utils.o gcc -o $@ $^ prog1.o: prog1.c gcc -c $^ utils.o: utils.c gcc -c $^ prog2: prog2.o utils.o gcc -o $@ $^ prog2.o: prog2.c gcc -c $^ .PHONY:clean clean: rm *.o prog1 prog2
-
make clean
后再make
,自动化构建结果一致: -
从 上面的
Makefile
我们还能发现一个问题,我们给每一个.o
文件都单独设立了规则,其实这是没必要的,我们其实不需要为每个.o
文件都定义一个规则,因为make
可以自动推断出如何构建它们(只要它知道如何编译.c
文件到.o
文件)。因此,我们可以简化Makefile
如下:.PHONY:all all:prog1 prog2 prog1: prog1.o utils.o gcc -o $@ $^ prog2: prog2.o utils.o gcc -o $@ $^ # 使用模式规则来编译所有的 .c 文件到 .o 文件 %.o: %.c gcc -c $^ -o $@ # gcc -c $< # gcc -c $^ .PHONY:clean clean: rm *.o prog1 prog2
-
$<
是一个自动变量,它代表规则中的第一个依赖文件名(在这个例子中是.c
文件)。
-
-
生成之后,此时我们执行
make
不会帮我们执行,但是如果我们修改了某个源文件,就不同了,但make
会根据时间选择性更新,避免重复编译:
-
变量和伪目标
Makefile
文件是可以设置变量的,上面我们介绍了自动变量。Makefile
文件中的变量的值用来存储字符串值,这些值可以在其它地方被引用。相当于给字符串起别名:
-
定义变量:使用
变量名=值
的形式来定义变量,=
两边不能有空格。 -
引用变量:使用
$变量名
就可以引用变量,快速使用一下:G=gcc main: main.o $G -o $@ $^ main.o: main.c $G -c $^ .PHONY:clean clean: rm *.o main
伪目标也称虚拟目标或者假目标,它们虽然是目标,但是不会生成任何文件,只会用于执行一系列命令。伪目标通常通过
.PHONY
特殊目标来声明,这告诉make
这些目标不是真正的文件名,因此即使它们与文件名冲突,make
也会执行它们后面的命令。
- 伪目标常用于以下场景:
- 清理构建目标过程出现的文件,如
.o
文件,可执行文件。 - 生成多个可执行文件。
- 清理构建目标过程出现的文件,如
make是如何工作的
-
查找
Makefile
文件,使用make
工具后,它首先会查找当前目录下有没有Makefile
文件,如果没有就会报错。 -
make
工具会查找Makefile
文件中的第一个目标(通常是最终的可执行文件或库文件),并将其作为最终的目标文件。然后,它会检查这个目标文件是否存在,以及它的依赖文件是否是最新的。 -
编译依赖文件:如果这个目标文件不存在,或者它的依赖文件中有任何一个比目标文件新(即依赖文件被修改过),那么
make
工具会按照Makefile
文件中定义的规则来编译这些依赖文件。这通常涉及到将源文件(如.c或.cpp文件)编译成目标文件(.o文件)(只会编译那些没有或者更需要更新的)。 -
一旦所有的依赖文件都被编译成目标文件,
make
工具就会将这些目标文件链接在一起,生成最终的可执行文件或库文件。 -
除了第一个目标之外,
Makefile
文件中可能还定义了其他目标(如清理生成的文件、测试等)。这些目标可以通过在make
命令后面加上相应的目标名来执行,例如“make clean”用于清理生成的文件。
认识一下时间
在Linux系统中,每个文件都有三种时间戳,分别是:
- Access Time (atime): 文件最后一次被访问的时间。这通常意味着读取文件内容的时间。
- Modify Time (mtime): 文件内容最后一次被修改的时间。这包括向文件中写入数据或者修改文件内容。
- Change Time (ctime): 文件元数据(metadata)最后一次被修改的时间。这包括文件的权限、所有权、链接数等属性的变化。注意,修改文件内容也会导致ctime的更新,因为修改内容本质上也会修改文件的元数据(例如文件大小)。
-
我们可以通过指令
stat filename
查看某个文件的三种时间,stat指令在Linux中用于显示文件和文件系统的详细状态信息,包括文件的属性。- 这个创建时间有些系统不存储,我们不用管。
-
我们可以通过指令
touch
修改文件的时间。-
touch -t [[CC]YY]MMDDhhmm[.ss] filename
:指定特定的时间更新文件的访问时间和修改时间。其中,
[[CC]YY]MMDDhhmm[.ss]
是时间的指定格式,例如202310011234.56
表示 2023 年 10 月 1 日 12 时 34 分 56 秒。注意,年份可以是两位数或四位数,但如果是两位数,则 69-99 表示 1969-1999 年,00-68 表示 2000-2068 年。 -
由于我们改变了文件的修改时间,所以文件的属性发生变化,文件的
ctime
就会更新,我们修改文件的访问时间,也是一种对文件元数据的修改,ctime
也会更新。 -
touch -a -t [[CC]YY]MMDDhhmm[.ss] filename
:仅更新文件的访问时间。 -
touch -m -t [[CC]YY]MMDDhhmm[.ss] filename
:仅更新文件的修改时间。 -
touch filename
:更新文件的访问时间和修改时间为当前时间。文件的ctime,不是touch
直接控制的,而是因为atime
和ctime
改变,由文件系统在底层自动处理的。
-
-
验证我们修改最近一次某个目标文件的源文件的最近修改时间,使其比目标文件的要新,
make
就会重新编译这个源文件:-
修改其ctime,执行
make
不会重新编译: -
修改
mtime
,执行make
会重新编译:-
touch -m filename
是把文件的mtime时间变为当前时间。
-
-
gdb调试工具
上面介绍项目中常用到的自动化构建工具,下面来介绍一下
Linux
中常用的调试工具gdb
。
安装gdb
ubuntu系统:
apt install -y gdb
,我们已经安装了:
使用gdb
总结gdb常用的指令
下面表格是对
gdb
常用指令的总结:
GDB常用指令表格(按功能分类)
功能分类 | 指令/缩写 | 描述 |
---|---|---|
查看源代码 | list (l) | 显示源代码的一部分或整个文件 |
设置断点 | break (b) | 在指定的行或函数上设置断点 |
管理断点 | info b | 显示当前所有断点的信息 |
delete (d) | 删除指定的断点或观察点 | |
disable | 禁用指定的断点或观察点 | |
enable | 启用之前禁用的断点或观察点 | |
单步执行 | step (s) | 执行下一行代码,并进入函数内部 |
next (n) | 执行下一行代码,但不进入函数内部 | |
运行控制 | run | 运行程序,直到遇到断点或程序结束 |
stop | 停止当前运行的程序 | |
continue © | 继续运行程序,直到遇到下一个断点或程序结束 | |
查看变量和表达式 | print § | 打印变量或表达式的值 |
调用栈管理 | backtrace (bt) | 显示当前调用栈的回溯信息 |
自动显示 | display | 设置在每次程序停止时自动显示的变量或表达式 |
undisplay | 取消之前设置的自动显示 | |
程序控制 | finish | 运行直到当前函数返回 |
until | 运行程序直到指定的行号或地址 | |
变量修改 | set var name=value | 设置变量的值 |
Release版本和Debug版本
我们都知道Windows中的集成开发环境中,可执行程序有两个版本,一个是
DEBUG
,另外一个是release
版本,只有处于Debug
版本下才能进行调试,Linux的gcc
工具,默认不带选项就是release
模式。
- 要使用
gdb
调试,必须在源代码生成.o
文件的时候加上-g
选项。在链接步骤中,通常不需要再次指定-g
选项,因为链接器会保留已编译对象文件中的调试信息。然而,如果在链接时再次指定-g
选项,也不会导致错误。
以下是一个关于Release版本的可执行程序和Debug版本的可执行程序区别的表格总结:
项目 | Debug版本 | Release版本 |
---|---|---|
主要用途 | 开发和调试阶段使用 | 部署和分发,供最终用户使用 |
调试信息 | 包含详细的调试信息,如符号表、行号等 | 通常不包含调试信息,以减小文件大小 |
优化级别 | 针对可读性进行优化,便于调试 | 经过编译器优化,以提高代码的执行速度和效率 |
性能 | 运行速度较慢,占用存储空间较多 | 运行速度较快,占用存储空间较少 |
变量初始化 | 未初始化的变量可能会被自动初始化 | 未使用的变量通常不会被自动初始化 |
断言检查 | 断言被激活,用于检测逻辑错误 | 断言检查通常被禁用,以提高性能 |
功能完整性 | 保留所有功能,包括测试用的功能 | 可能省略某些调试用的功能,以提升安全性和性能 |
安全性 | 较低,因为包含调试信息和未优化的代码 | 较高,因为去除了调试信息并进行了优化 |
稳定性 | 可能存在未经过充分测试的代码 | 经过详细测试,确保在各种条件下稳定运行 |
- 因为
Deubg
版本下的可执行文件包含调试信息,它的大小比Release
版本下的可执行文件要大一些:
-
编译器在生成目标文件的同时也会生成调试信息,这些信息包括了源代码与机器码之间的映射关系,如变量名、函数名、行号等,它们以特定的格式(如DWARF)编码,并存储在可执行文件或专门的调试信息文件中。
指令的使用
gdb
中有很多命令用来帮助我们调试,我们下面来一一介绍一下:-
gdb 可执行程序
,进入调试界面。 -
list/l 行号
显示源代码指定行之后的代码。 -
程序运行起来才会开始调试,在程序运行前,我们需要先打断点。打断点,可以用指令
break/b 行号/函数名/文件名
: -
可以用指令
info b/break
,查看断点的情况:-
Disp
列:Disp
列表示断点的部署(disposition)状态,它有三种可能的状态:-
keep
:表示这是一个普通的断点,不会自动删除。
-
del
:表示这是一个临时断点(通常通过设置tbreak
命令创建),当程序在断点处停住一次后,断点会自动删除。 -
dis
:表示断点当前被禁用(disabled),即断点存在但不会触发程序停住。
-
-
Enb
列:Enb
列表示断点的启用(enable)状态。它有两种状态:y
和n
。-
y
:表示断点当前是启用的,即当程序运行到该断点时,会触发程序停住。 -
n
:表示断点当前是禁用的,即断点存在但不会触发程序停住(与disp
列的dis
状态类似,但Enb
列更专注于启用/禁用状态,而disp
列更侧重于断点的生命周期管理)。
-
-
前面是断点编号,
d 断点编号
删除一个断点。
-
-
使用指令
disable
/enable
,禁用/使能断点: -
使用指令
run
,程序运行起来才能开始调试,这和VS2019
的机制是一样的:- 由于我们把第10行的断点,禁用了,所以程序直接跳到了第11行。
-
使用指令
n
逐过程调试,s
逐语句调试。逐过程调试会把每一句语句看成一个整体,类似于VS2019
中的F10,而s
会进入函数内部,类似于VS2019
中的F11
。 -
在调试的运行过程中,我们也可以使用指令
p 变量名/变量地址
,查看它的值:-
但是它只会显示一次,不会一直显示,我们可以使用指令
display 变量名/变量取地址
,查看变量的值或者它的地址,它会一直更新:-
前面是它的编号,我们使用指令
undisplay 编号
可以取消这个对这个变量或者地址的查看:
-
-
-
-
在调试过程我们还可以查看程序此时调用的堆栈信息,使用指令
bp
: -
除了
n
逐过程调试,s
逐语句调试,还有一些指令与过程调试有关:-
c
:从一个断点,直接运行到下一个断点: -
finish
:执行完一个函数后会停下来。- 只有在函数体内执行这个指令才有效,当函数中有其它有效断点时,执行这个指令,会先跳到断点处,如果没有断点,执行执行完函数才会停下。
-
until
:在一定范围内,直接跳转到指定行。- 中间行也不能有有效的断点,如果有
until
会停在第一个断点处。
- 中间行也不能有有效的断点,如果有
-
-
指令
set var name=value
修改变量的值。- 如果程序由于某些输入数据而崩溃或行为异常,你可以通过修改这些输入数据(即相关变量的值)来绕过错误数据,继续调试其他部分的代码。
- 当你怀疑某个变量导致问题时,可以通过修改它的值来验证你的假设。如果修改后的值解决了问题,那么你就更接近问题的根源了。
- 在某些情况下,程序的状态可能非常复杂,难以通过正常输入达到。通过修改变量值,你可以模拟这些复杂状态,以测试代码在这些情况下的行为。
- 本人知识、能力有限,若有错漏,烦请指正,非常非常感谢!!!
- 转发或者引用需标明来源。