如何调试makefile变量
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何调试makefile变量相关的知识,希望对你有一定的参考价值。
回顾,这几年来大家问题我的问题,其实很多时候是makefile的调试问题。所以,就像我在之前的那篇关于GDB的技巧的文章中做的一样,在这里向大家介绍一个小小的调试变量的技巧。相信一定对你有用。 对于Makefile中的各种变量,可能是我们比较头痛的事了。我们要查看他们并不是很方便,需要修改makefile加入echo命令。这有时候很不方便。其实我们可以制作下面一个专门用来输出变量的makefile(假设名字叫:vars.mk) %: @echo '$*=$($*)' d-%: @echo '$*=$($*)' @echo ' origin = $(origin $*)' @echo ' value = $(value $*)' @echo ' flavor = $(flavor $*)' 这样一来,我们可以使用make命令的-f参数来查看makefile中的相关变量(包括make的内建变量,比如:COMPILE.c或MAKE_VERSION之类的)。注意:第二个以“d-”为前缀的目标可以用来打印关于这个变量更为详细的东西 (后面有详细说明)假设我们的makefile是这个样子(test.mk) OBJDIR := objdir OBJS := $(addprefix $(OBJDIR)/,foo.o bar.o baz.o) foo = $(bar)bar = $(ugh)ugh = Huh? CFLAGS = $(include_dirs) -O include_dirs = -Ifoo -Ibar CFLAGS := $(CFLAGS) -Wall MYOBJ := a.o b.o c.o MYSRC := $(MYOBJ:.o=.c) 那么,我们可以这样进行调试: [hchen@RHELSVR5]$ make -f test.mk -f var.mk OBJS OBJS=objdir/foo.o objdir/bar.o objdir/baz.o [hchen@RHELSVR5]$ make -f test.mk -f var.mk d-foo foo=Huh? origin = file value = $(bar) flavor = recursive [hchen@RHELSVR5]$ make -f test.mk -f var.mk d-CFLAGS CFLAGS=-Ifoo -Ibar -O -O origin = file value = -Ifoo -Ibar -O -O flavor = simple [hchen@RHELSVR5]$ make -f test.mk -f var.mk d-COMPILE.c COMPILE.c=cc -Ifoo -Ibar -O -Wall -c origin = default flavor = recursive value = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c 我们可以看到:make的第一个-f后是要测试的makefile,第二个是我们的debug makefile。后面直接跟变量名,如果在变量名前加”d-”,则输出更为详细的东西。说一说”d-” 前缀(其意为details),其中调用了下面三个参数。$(origin):告诉你这个变量是来自哪儿,file表示文件,environment表示环境变量,还有environment override,command line,override,automatic等。$(value):打出这个变量没有被展开的样子。比如上述示例中的 foo 变量。 参考技术A对于Makefile中的各种变量,要查看并不是很方便,需要修改makefile加入echo命令。制作下面一个专门用来输出变量的makefile(假设名字叫:vars.mk)
vars.mk@echo '$*=$($*)'
d-%:
@echo '$*=$($*)'
@echo ' origin = $(origin $*)'
@echo ' value = $(value $*)'
@echo ' flavor = $(flavor $*)'
@echo '$*=$($*)'
可以使用make命令的-f参数来查看makefile中的相关变量(包括make的内建变量,比如:COMPILE.c或MAKE_VERSION之类的)。注意:第二个以"d-"为前缀的目标可以用来打印关于这个变量更为详细的东西
UNIX 环境编程GCC 编译器 | Makefile 基础入门 | GDB 调试教学
💭 写在前面:本文将介绍如何使用 GCC 编译器编译,并详细介绍了 Makefile 的基本构造、创建Makefile 文件以及 Makefile 变量,以提高编译效率。此外,本文还将探讨GDB调试器的使用,包括调试前的准备、readelf 读取 ELF 文件信息、显示代码、断点、调试、监视、跳转等内容。
📜 本章目录:
0x03 显示代码 gcb(list) (List file)
Ⅰ. GCC 编译器
0x00 gcc 的使用
编译顺序:C 预处理阶段 → C编译 → 汇编 (Assemble) → 链接 (Linking)
gcc,即 GNU Compiler Collection,是一个编译器套件。
gcc 不仅可以编译 C 语言,还可以编译其他语言。
在 C 语言中生成可执行文件的过程如下:
- 编译(.c -> .o):由 cc1 完成。
- 链接(.o -> 可执行文件 a.out):由名为 ld 的链接器完成。
gcc 通过调用这些编译器和链接器来生成可执行文件。
输入 gcc -v 可以确认 gcc 版本:
$ gcc -v
❓ 那么如何使用 gcc 呢?很简单:
- 创建一个 C 文件
- 编译:gcc 文件名.c
- 如果编译成功,则会自动生成一个名为 a.out 的可执行文件。
- 如果想要查看结果,可以使用 ./a.out 命令来运行它。
点表示当前目录,a.out 是 Unix 中默认的 C 编译器生成的二进制文件。
$ ./a.out
如果想要创建其他名称的可执行文件,则可以使用:
gcc -o 可执行文件名 源文件名.c
gcc 源文件名.c -o 可执行文件
💭 例如:gcc -o mytest test.c,注意,-o 后面必须跟输出的文件名!
* 我们可以通过 man 手册去查询 gcc 更多的信息:
$ man gcc
0x01 GCC 选项
-v 选项:可以显示编译过程
$ gcc -v 源文件名.c
可以查看当前使用的 gcc 版本以及插入到源代码中的 stdio.h 所在的目录等信息。gcc编译器版本为 5.4.0,gcc 访问头文件(如stdio.h)的路径为 /usr/lib/gcc/x86 64-linux-gnu/5/include 等。
-D 选项:在外部定义预处理器宏
$ gcc -D test.c
-c 选项:仅编译选项(当需要编译多个源文件时需要)
$ gcc -c test.c
有以下有两种方法:
1. gcc –o main main.c fun1.c fun2.c
2. gcc –c main.c
gcc –c fun1.c
gcc –c fun2.c
gcc –o main main.o fun1.o fun2.o
虽然第二种方法看起来更麻烦,但是当fun1的源代码被修改时,通过使用目标文件,我们可以只重新编译已更改的文件,然后将其与现有文件链接,而不进行内部低效的操作。
-I 选项:指定 #include 语句中头文件所在的目录。用于链接库文件的
如果使用 #include <stdio.h>,则以系统标准目录 /usr/include 为基础查找文件并包含它。 如果使用 #include "stdio.h",则以当前正在执行编译器的目录为基础查找头文件并包含它。 如果不是这两个目录,则明确指定为 -I<目录>。
Ⅱ. 快速入门 Makefile
0x00 为什么需要 Makefile?
❓ 我们为什么需要 Makefile?举个最简单的例子:
如果需要跑的工程,源文件有十几个甚至上百个呢?难不成我们要:
gcc –c main.c
gcc –c fun1.c
gcc –c fun2.c
…
gcc –c fun100.c
gcc –o program main.o fun1.o fun2.o … fun100.o
键盘寿命疯狂 -1,CV C 到冒烟?不用担心!Makefile 解君愁!
0x01 Makefile 的基本构造
target ... : prerequisites ...
recipe
....
target (目标):文件的名称,如可执行文件,目标文件等等。最先出现的目标文件称为默认目标(通常是可执行文件) ,目标文件是 make 的最终目标。
prerequisites (先决条件):为了生成目标文件所需的材料。
recipe (配方):Make 执行的操作。当先决条件发生变化时,按照规则生成目标文件。只有需要更新的目标才会进行更新。
* 如果先决条件发生变化 按照规则生成目标文件 (只有需要更新的目标才会进行更新)
0x02 创建 Makefile 文件
Makefile 是用于在 Linux 系统中简化重复编译过程的配置文件。
通过 Makefile 可以管理库和编译环境,我们推荐将文件命名为 Makefile。
CC = gcc
target1 : dependency1 dependency2
command1
command2
target2 : dependency3 dependency4 dependency5
command3
command4
* 注意:command 前必须要加一个 tab!
什么是 Makefile 中的宏定义?在 Makefile 中一个预定义的环境变量。
一个 Makefile 由 目标 (target) 、依赖关系 (Dependency) 和 命令 (command) 组成。
0x03 Makefile 和普通的编译过程的对比
Makefile 和一般的编译过程的区别在于:
Makefile 能够通过自动化针对每个文件的重复命令,节省时间。
能够让你快速掌握程序的依赖结构,易于管理,最大限度地减少了简单的重复性工作和重写工作。
我们来做个对比,如果使用普通编译过程:
$ gcc -c -o director.o director.c
$ gcc -c -o file.o file.c
$ gcc -c -o main.o main.c
如果使用 Makefile:
$ gcc -o exefile main.o director.o file.
🔨 动手尝试:vi Makefile 并输入以下内容
exefile2: file.o director.o main.o
gcc -o exefile2 file.o director.o main.o
file.o: file.c
gcc -c -o file.o file.c
director.o: director.c
gcc -c -o director.o director.c
main.o: main.c
gcc -c -o main.o main.c
clean:
rm *.o exefile2 # 清除所有扩展名为 .o 的文件和名为 exefile2 的可执行文件。
* 使用 make clean 命令:清除所有 扩展名为 .o 的文件和名为 exefile2 的可执行文件。
0x04 Makefile 变量
你可以使用变量,简化你的 Makefile!编写起来会更简单:
cc :用于指定 C 语言编译器的名称,通常为 gcc,可以它来指定编译器:
cc = gcc # 指定 C 编译器 gcc
cc = g++ # 指定 C++ 编译器 g++
target :设置 Makefile 最终要生成的目标文件
target=movie
也可以定义多个 target 生成多个目标文件,并为每个 target 指定依赖关系和构建规则。
object :用于指定所有的目标文件(object file),通常以“.o”为扩展名,使编写 prerequisites 和 recipe 更容易。
object=main.o file.o director.o
❓ 一些问题:
为什么没有 main.o film.o director.o 的 recipe?
make 会推导 recipe,因此没有必要写出每一个 recipe 来编译每个 c 文件。
提到了.o 文件,但没有为它制定规则?因为 make 使用了一个隐含的规则。
隐含规则:如果你需要创建或更新一个新的.o文件,找到一个同名的 .c 文件并使用 cc -c 命令。
需要 film.o -> film.o 不存在?-> 找到 film.c -> 执行:cc -c film.c -o film.o
cc 的含义和 Linux 上的 gcc 相同:
$(objects) : film.h
即使头文件改变了,它也会创建一个新的对象文件。
.PHONY : clean :伪目标
把它看成是 recipe 的简单表示,而不是实际文件的名称。这里的 recipe:
rm $(target) $(objects)
删除目标文件和当前目录下的所有对象文件。
为什么使用 PHONY?为了避免意外情况!比如:如果你的一个文件叫 clean 怎么办?
【一个经常出现的错误】
Makefile:17: ***缺少分隔符。 停止。
在第一章中,我们强调了在编写Makefile时,命令部分应该以TAB字符开始。上面的错误是因为我们没有使用TAB字符,所以我们无法判断make是否是一个命令。
解决方法:在第17行(接近结尾处)将命令改为以TAB字符开始。
make。*** 没有规则来制作目标 "io.h","read.o "需要。停止。
上述错误是由于依赖关系的问题:read.c被定义为依赖io.h,但io.h没有找到。
行动:调查依赖关系中定义的io.h是否真的存在。如果不存在,想想为什么不存在。你也可以尝试再次运行make dep来重新创建依赖关系。
Makefile:10: ***命令在第一个目标前开始。 停止。
上面的错误是一个模糊的错误信息,说 "命令在第一个目标之前开始"。根据我的经验,造成这个错误的原因似乎是在使用'\\'标记多行长句时误用了'\\':'\\'部分应该是该行的最后一个字符,但如果你不小心在'\\'后面加了几个空格,你就不可避免地会出现上述错误。
解决方法:在第10行(接近结尾处),如果有一个'\\'字符,确保它是该行的最后一个字符,即删除'\\'字符之后的一切(主要是空格)。
如果你运行make,你将不会得到你想要的可执行文件,而且它的行为会很奇怪。例如,它的行为将与make clean相同。
你必须记住,make并不是一个天才。当 make 读取 Makefile 的内容时,它认为它看到的第一个目标就是它应该产生的结果文件。 所以,如果你把 Makefile 的 clean 部分作为 Makefile 的第一个目标,你会得到上述结果。
解决方法:在例7.1中,我们创建了一个不必要的目标,叫做all。所以,要么把你想生成的结果文件作为第一个目标,要么像例7.1那样,创建一个像all一样的假目标。把 make clean 和 make dep 这样的东西放在 Makefile 的最后是安全的。
重新编译一个先前编译过的文件而不去修正它。
这种行为是因为make不知道依赖关系,也就是你没有设置它们。结果,make认为它的工作是编译所有文件并创建一个可执行文件。
解决方法:你需要设置目标文件、源文件和头文件的依赖关系。说gccmakedep *.c会在Makefile的末尾自动创建依赖关系。对于所有其他文件,你需要适当地设置依赖关系。
main.o : main.c io.h read.o : read.c io.h write.o : write.c io.h使用gcc时可能出现的错误信息!
Ⅲ. GDB 调试
0x00 调试前的准备
程序在运行时展现程序内部发生了什么?出现 BUG 的时候便于我们知道在哪。
为了找到 BUG,gdb 做四种事:
① 程序运行时,各种条件设定后,程序开始。
② 遇到特定的条件时使程序暂停
③ 程序暂停时,检查发生了什么事
④ 在程序内部把 BUG 修改完后,继续运行找其他出现的 BUG
具有上面的特性的 GDB 是找到程序中 BUG 的利器!
运行 GDB 前编译方法及实行:
编译 (Compile) :
Debug 之前,编译将要 debug 的程序的 debugging 信息。这样就能使用 GDB 运行使用的变量和运行的函数了。举个例子:
gcc -g test.c -o test
运行 GDB:
GDB 运行程序名
为了在 GDB 中查看 C 的调试信息,需要使用 -g 选项编译 C 程序:
$ gcc -g test
要使用 GDB 调试编译后的 C 程序,按照以下步骤进行:
$ gdb program
此时就能看到用于输入 GDB 命令的提示符了:
(gdb)
我们先来创建一个用来演示 GCD 调试功能的目录:
既然要调试,我们就必须要有个代码,我们这里写一个数字累加的代码:
🚩 运行结果:
结果是5050,没有问题。如果我们代码出现了一些问题需要我们调试,我们就可以使用 gdb。
如果此时你出现了报错,说什么不支持 for 循环里面定义变量,你可以输入:
$ gcc hello.c -o hello -std=c99
0x01 Linux 默认集成环境
在你当前的代码目录下直接执行 gcb + 形成的可执行程序:
$ gdb [可执行程序]
此时就进入了 gdb 的调试命令行中:
(如果想退出,直接按 quit 就可以退出了)
gcb 读取我们的 hello 程序时出现了 "没有调试符号被发现" 的警告:
这是什么意思呢?
我们的 gdb 中有一条命令叫做 list(简写成 l),但是我们输入后出现以下提示:
因为 —— 默认形成的可执行程序无法调试!!!
相信大家都知道,C语言的发布方式有两种,一种是 debug 一种是 release。
我们在 VS 里面可以直接调的原因是,VS2019 的默认集成环境是 debug。
而在我们的 Linux 中的默认集成环境是 release,换言之,
在我们 Linux 中如果你想发布一个程序,可以直接发布,无需加任何选项。
但是如果你 想调试,以 debug 形式发布,那么你就需要在编译时在后面添加一个 -g 选项:
$ gcc hello.c -o hello.g _g
🔺 总结:Linux 默认形成的可执行程序是动态链接且是 release 方式发布。
- 如果想静态链接,加 -static
- 如果想动态链接,加 -g
0x02 readelf 读取 ELF 文件信息
release 和 debug 的区别:你的可执行程序里本来就有调试信息, 只是 debug 中才有。
首先,这两个版本也都是可以运行的:
并且我要告诉你的是:debug 版本比 release 版本多几千个字节,这是什么?
毫无疑问,这些就是一个可执行程序的调试信息,它在 debug 版本中有所显现。
📚 如果你想看调试信息,你可以输入:
$ readelf -S [可执行程序] # 以段的形式读取可执行程序,用于显示读取ELF文件中信息
💭 我们先看看 release 版的:
💭 我们再来读一读 debug 版的:
因为 debug 版本是能给你的可执行程序添加调试信息的,所以体积自然比 release 版本要大些。
所以我们调试的得是 debug 版本的可执行程序,预备工作全部做好,下面我们来正式学习 gdb。
0x03 显示代码 gcb(list) (List file)
现在我们是 debug 版本了,我们也顺理成章地能够使用前面我们说的 list 了。
(gdb) list [n] # 显示代码,可带行号
(gdb) list [function] # 显示带某函数的代码块
(gdb) list [begin, end] # 显示区间内的代码
...
💭 操作演示:
一般在 VS 下调试的时候,除了让你看到代码,还会让你看到进行到了哪里,这里也是一样的。
你按下回车后,gdb 会自动记录你的上一条指令,直接按回车就是上一条命令:
(这么做就能把代码从第一行开始,将所有代码块逐个显示出来了)
0x04 断点
💭 假设我们想在下面代码的第15行处打个断点:
这要是放在 VS 下我们直接滑鼠选中对应行然后无脑按 F11 就行了。
而在 gdb 下打断点的方式是用 breakpoint:
(gdb) breakpoint [n] # 在第n行处打一个断点
💭 操作演示:我们在代码第15行打个断点看看:
此时如果你想查看断点,可以使用 info 查看你打的断点:
(gdb) info b # 查看断点
💭 操作演示:查看断点信息
我们再在第17行新增一个断点,此时我们就能看到两个断点了:
如果想要删除断点,在 VS 下我们直接再点以下小红点就搞定了:
(图形化界面无疑是成功的)
但是在 gdb 中,我们需要知道要删除的断点的编号:
(gdb) d [Num] # 删除Num号断点
💭 操作演示:删除1号断点(记不得要删除的断点的编号可以 info b)
此时 1 号断点已被成功删除,再次删除则会显示已经没有这个断点:
0x05 调试
准备开始调试,记得把刚才删除的断点再打回去,调试的指令如下:
(gdb) run # 开始调试
💭 操作演示:输入完 r 按回车开始调试:
(此时就悬停在了第15行)
如果我们把场上断点全部干掉了,此时按 r 调试程序就会直接跑完:
(这和 VS 也是一样的)
如果你想查看变量的内容,我们可以使用 print 命令:
(gdb) print [val]
💭 操作演示:查看变量内容
💭 操作演示:逐语句
如果想逐语句执行(逐语句即一步一步往后走),逐语句指令如下:
(gdb) step # 逐语句
我们 s 两次后,此时走到了函数的调用处。此时如果你不想进入该函数,就不要再按 s 逐语句了。
此时我们应该逐过程执行,我们可以使用 next 命令:
(gdb) next # 逐过程
💭 操作演示:逐过程
0x06 监视
我们在 VS 中调试代码的时候,有时候要 细 ♂ 细 ♂ 观 ♂ 察 某个变量时,我们会打开监视窗口:
在 gdb 下我们就可以使用 display 常显示来监视:
$ display [val] # 监视一个变量,每次停下来都显示它的值
$ display [v1, v2, v3...] # 同时添加多个变量,用括号隔开
💭 操作演示:常显示 i 变量
当然,我们也可以同时监视多个值:
(同时常显示三个变量)
直接输入 display 可以查看监视列表:
$ display # 查看当前监视列表
💭 操作演示:查看监视列表
如果想把某个变量从监视窗口移除,我们可以使用 undisplay:
$ undisplay [n] # 删除n号变量
💭 操作演示:删除3号变量
0x07 跳转(until & c & finish)
我们调试的时候在文本特别大的时候我们有时候会跳转,VS 里我们可以直接拖动箭头跳转。
gdb 调试下我们可以使用 until 指令跳转到指定行:
$ until [n] # 跳转到指定行
💭 操作演示:跳转到20行
如果想从一个断点跳转至另一个断点,我们可以使用 c:
$ c # 直接跳转到另一个断点
有时候难免手贱不小心进了不想进入了函数,就比如不小心逐语句进了 printf 函数。
这个在 VS 下逐语句是不会进去的,但是在 Linux 下会进入,此时如果你反悔了象出来,
就可以输入 finish,它可以做到直接执行完成一个函数就停下来。
$ finish # 执行到当前函数返回,然后停下来等待命令
0x08 对于 gdb 的态度
掌握上面单独介绍的 b、d、l、s、n、display、until、r、c、finish 其实就差不多了。
还有一些 gdb 的指令我们上面没有介绍,这里做一个整合:
- list/l 行号:显示binFile源代码,接着上次的位置往下列,每次列10行。
- list/l 函数名:列出某个函数的源代码。
- r 或 run:运行程序。
- n 或 next:单条执行。
- s或step:进入函数调用。
- break(b) 行号:在某一行设置断点。
- break 函数名:在某个函数开头设置断点。
- info break :查看断点信息。
- finish:执行到当前函数返回,然后挺下来等待命令。
- print(p):打印表达式的值,通过表达式可以修改变量的值或者调用函数。
- p 变量:打印变量值。
- set var:修改变量的值。
- continue(或c):从当前位置开始连续而非单步执行程序。
- run(或r):从开始连续而非单步执行程序。
- delete breakpoints:删除所有断点。
- delete breakpoints n:删除序号为n的断点。
- disable breakpoints:禁用断点。
- enable breakpoints:启用断点。
- info(或i) breakpoints:参看当前设置了哪些断点。
- display 变量名:跟踪查看一个变量,每次停下来都显示它的值。
- undisplay:取消对先前设置的那些变量的跟踪。
- until X行号:跳至X行。
- breaktrace(或bt):查看各级函数调用及参数。
- info(i) locals:查看当前栈帧局部变量的值。
- quit:退出gdb。
📌 [ 笔者 ] 王亦优
📃 [ 更新 ] 2023.3.21
❌ [ 勘误 ] /* 暂无 */
📜 [ 声明 ] 由于作者水平有限,本文有错误和不准确之处在所难免,
本人也很想知道这些错误,恳望读者批评指正!
📜 参考资料 Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. . 百度百科[EB/OL]. []. https://baike.baidu.com/. GCC 컴파일러 에러 메세지 리스트(Error Message List) :: Block Busting |
以上是关于如何调试makefile变量的主要内容,如果未能解决你的问题,请参考以下文章