众所周知,GNU Make 是地球上最好的构建工具,它的可读性非常好,完全不需要调试。
但是笔者眼拙,对着稍显复杂的项目没看明白,还得依靠第三方工具 remake 才能勉强梳理。
错误的调试方式
标题强调“正确地调试”,是因为我觉得 GNU Make 本身提供了错误的调试方式:
- 使用
$(info)
、$(warn)
和$(error)
等注入手段。 - 使用
make [OPTION]
,它确实提供了-n
(dry run 模式)和-d
(debug 模式)选项。
第一种做法的介绍可看这里,这种方式无异于在一个 C 程序里每次插入 printf()
。侵入式的缺陷就不多说了,只能说可以快速处理有限的问题。
第二种做法看似调试,其实是 dump 出内部的环境以及寻找依赖的过程。思路上似乎没啥问题,可以品鉴下图 dry run 模式输出的信息。
也许该买一个 100 寸的屏幕
我是根本看不懂啊!
如果你有耐心,这种办法应该能足够解决所有问题,因为它提供的信息足够详尽。但我希望有一种更好的办法,能做到指哪打哪。
正确的调试方式
正确的调试方式就应该用调试器,它可以给人运行时的、非侵入式的调试方式。
而 remake
正是如此。
简单点说它就是构建期的 gdb,语法也类似,相信各位看一眼就能找到熟悉的感觉。
NOTE: remake
不只是调试器,虽然功能不算特别多,但还涵盖了日志跟踪以及性能分析,以及它本身是一个 GNU Make 的 fork 版本,这意味着 make
已有的选项都大概能用。
Getting started
我这里简单介绍调试功能,并且用 Linux 内核的 Makefile 作为示例。
要启动调试器只需 remake -X [target]
,关闭就是 quit
。
断点
使用 break
进行断点:
break <line>
:指定当前 Makefile 行数断点。break <target>
:指定目标断点。delete <id>
:删除指定断点。
使用 step
、continue
、next
、finish
可完成日常的调试任务。
一个好使的办法是 continue <target>
,可以直接运行并停在对应的目标上。
一个例子
比如这里用到数千行的 Makefile,省略了单步过程,直接跳到目标 vmlinux_o
,此时对应于 Makefile:1230
。然后 step 两遍,由于有 prepare: prepare0
的依赖关系,最终定位到 prepare0
。
NOTE1: 常用的命令缩写也适用于 remake
,比如 b
、s
、n
等等。
NOTE2: 也可以使用 $(debugger)
在 Makefile 文件内生成断点。
NOTE3: 图里多次提示文件不存在是正常的,因为现在就是构建期。
定位
使用 backtrace
查看当前回溯信息。
使用 list
,查看当前定位的行数。
使用 frame
,切换栈帧。
刚才的例子·续
这些都挺直观的,没啥好解释。
观测
使用 print <variable>
输出变量。这个命令非常有用,比如 Makefile:536 行存在空变量 $(LDFLAGS_vmlinux)
,这个变量后续会按条件进行拼接,分析很麻烦。这个时候调试只需要 print LDFLAGS_vmlinux
即可;同时对于宏也可以直接输出,比如 print CONFIG_MODULES
可以得知这个配置是否打开:
使用 target <target>
查看指定目标的信息。注意前面的 print
并不能输出目标,因此用 target
命令。这个命令方便在于可辅助分析自动变量(比如 ^?+@
这些符号)和隐式规则。
还有 info
。子命令很多,见下表。
命令 | 说明 |
---|---|
info break | List all Breakpoints |
info files | Show Read-in Files |
info frame | Show Target Stack Frame |
info line | Show the Current Line |
info program | Show Makefile Information |
info rules | Show Implicit or Pattern Rules |
info target | Show Target Name |
info targets | Show Targets found in Makefiles |
info tasks | Show Targets with Descriptions |
info variables | List all Variables |
更多
更多可参考文档:Debugger Commands。
调试以外
正如前面所说,remake
的功能不止于调试。这里简单列下 trace 和 profile 用法。
更友好的 trace
假设存在一个极其简单的 Makefile 文件:
all: hello world
hello:
@echo "Hello"
world:
@echo "World"
使用 remake --trace
可输出清晰的依赖关系和执行顺序:
Reading makefiles...
Updating makefiles...
Updating goal targets...
File 'all' does not exist.
File 'hello' does not exist.
Must remake target 'hello'.
Makefile:4: target 'hello' does not exist
##>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
echo "Hello"
##<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
Hello
Successfully remade target file 'hello'.
File 'world' does not exist.
Must remake target 'world'.
Makefile:7: target 'world' does not exist
##>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
echo "World"
##<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
World
Successfully remade target file 'world'.
Must remake target 'all'.
Successfully remade target file 'all'.
即使是大型项目,只要用游标卡尺确认缩进也能得出依赖的嵌套关系。
个人体验和 make -d
差不多,只是后者更加复杂,有 800+ 行的日志。
简单的 profile
使用 remake --profile
即可进行性能分析,生成 callgrind 调用图。
没用过,看着挺方便的,就从 github 搬了张演示图。
就这样吧
本文就简单分享一下也许好用的工具。remake
稳定性可能不算理想,我在实测过程中发现 .mod
依赖识别出错,还需要自己动手修复才能完整调试。但不管怎么说,整体体验是更易于梳理 Makefile。
最后愿天堂没有 GNU Make。
本文已转发到知乎。