remake调试器
国外的开发人员对GNU Make进行了一定程度上的重构,增加了Makefile的动态调试功能,命名为remake。调试器的操作界面类似于GDB的命令行,开发者为其编写了详细的文档,有兴趣的可以参考。GNU Make自身也支持若干个调试选项,但其调试功能比较有限,remake
的作者对此很不满。不过,remake
工具对递归的调试(即一个Makefile脚本中调用$(MAKE)
执行新的Makefile)支持不够完善,对单个Makefile的调试功能则相对完整。本文记录了笔者通过remake
工具调试openwrt工程中的Makefile的过程,对一些复杂工程的构建脚本做一些初步的了解。
openwrt对编译脚本的更新检测
openwrt是一个优秀的开源嵌入式软件项目,虽然常用于做网关、路由等设备的系统软件,但其前身之一LEDE确实尝试作为“通用”的嵌入式Linux开发环境(Linux Embedded Development Environment)来设计的。完整的工程代码仓库带有上千个开源软件包。作为一名嵌入式底层开发人员,我们了解到,系统级的开源软件包之间会有复杂的依赖关系。在openwrt中,大部分开源软件包之前的依赖关系是通过各个Makefile中的DEPENDS
变量来指定的,这个依赖关系需要经过处理,生成mconf
(用于显示工程配置的菜单界面的可执行文件,menuconfig
)可读取的依赖信息,这样在menuconfig
选择界面就可以自动推断、处理各个软件包间的依赖关系,如选择一个软件包会自动选择其依赖的另一个软件包等。
不过,在执行make menuconfig
时,openwrt
不会每次都进行这样的转换:读取上千个Makefile中的依赖关系并生成mconf
可读取的依赖信息:
$ make V=s -j1 menuconfig
make[1]: Entering directory '/home/yejq/program/openwrt/scripts/config'
set -e; mkdir -p ./; trap "rm -f ./.mconf-cfg.tmp" EXIT; { /bin/sh mconf-cfg.sh; } > ./.mconf-cfg.tmp; if [ ! -r mconf-cfg ] || ! cmp -s mconf-cfg ./.mconf-cfg.tmp; then true ' UPD mconf-cfg'; mv -f ./.mconf-cfg.tmp mconf-cfg; fi
make[1]: Leaving directory '/home/yejq/program/openwrt/scripts/config'
*** End of the configuration.
*** Execute 'make' to start the build or try 'make help'.
$ touch package/system/ubus/Makefile
$ make V=s -j1 menuconfig
make[1]: Entering directory '/home/yejq/program/openwrt/scripts/config'
set -e; mkdir -p ./; trap "rm -f ./.mconf-cfg.tmp" EXIT; { /bin/sh mconf-cfg.sh; } > ./.mconf-cfg.tmp; if [ ! -r mconf-cfg ] || ! cmp -s mconf-cfg ./.mconf-cfg.tmp; then true ' UPD mconf-cfg'; mv -f ./.mconf-cfg.tmp mconf-cfg; fi
make[1]: Leaving directory '/home/yejq/program/openwrt/scripts/config'
Collecting package info: done
*** End of the configuration.
*** Execute 'make' to start the build or try 'make help'.
对比以上两次make menuconfig
,二者间通过touch
命令行工具更新了package/system/ubus/Makefile
,第二次多了一行Collecting package info: done
,下面就会使用remake
工具协助调试,openwrt是如何检测到ubus的Makefile被更新了,需要重新更新软件包信息的(Collect package info
)。
调试检索软件包的Makefile
输出Collecting package info
信息的Makefile脚本工程路径为include/scan.mk
,相关的内容如下:
$(TMP_DIR)/.$(SCAN_TARGET): $(TARGET_STAMP)
$(call progress,Collecting $(SCAN_NAME) info: merging...)
-cat $(FILELIST) | awk '{gsub(/\//, "_", $$0);print "$(TMP_DIR)/info/.$(SCAN_TARGET)-" $$0}' | xargs cat > $@ 2>/dev/null
$(call progress,Collecting $(SCAN_NAME) info: done)
echo
上面的规则依赖$(TARGET_STAMP)
,修改include/scan.mk
,增加陷入remake
的操作指令:
diff --git a/include/scan.mk b/include/scan.mk
index aee24cb3e5..e6f17cc18e 100644
--- a/include/scan.mk
+++ b/include/scan.mk
@@ -99,6 +99,7 @@ $(TMP_DIR)/info/.files-$(SCAN_TARGET).mk: $(FILELIST)
-include $(TMP_DIR)/info/.files-$(SCAN_TARGET).mk
$(TARGET_STAMP)::
+ $(debugger "trap into remake debugger")
+( \
$(NO_TRACE_MAKE) $(FILELIST); \
MD5SUM=$$(cat $(FILELIST) $(OVERRIDELIST) | $(MKHASH) md5 | awk '{print $$1}'); \
新增debugger
的一下行以加号"+"开始,会影响到$(NO_TRACE_MAKE)
的一些参数,详见官方文档的说明。因该规则有两个冒号,且无依赖,那么该规则总是会执行。之后使用touch
再次更新package/system/ubus/Makefile
,找到上面的规则对应的行号,以remake V=s -j1 menuconfig
命行进入调试操作界面:
$ remake V=s -j1 menuconfig
remake[1]: Entering directory '/home/yejq/program/openwrt/scripts/config'
set -e; mkdir -p ./; trap "rm -f ./.mconf-cfg.tmp" EXIT; { /bin/sh mconf-cfg.sh; } > ./.mconf-cfg.tmp; if [ ! -r mconf-cfg ] || ! cmp -s mconf-cfg ./.mconf-cfg.tmp; then true ' UPD mconf-cfg'; mv -f ./.mconf-cfg.tmp mconf-cfg; fi
remake[1]: Leaving directory '/home/yejq/program/openwrt/scripts/config'
debugger() function called with parameter "trap into remake debugger"
:o (/home/yejq/program/openwrt/include/scan.mk:101)
/home/yejq/program/openwrt/tmp/info/.files-packageinfo.stamp
remake<<0>> where
=>#0 /home/yejq/program/openwrt/tmp/info/.files-packageinfo.stamp at /home/yejq/program/openwrt/include/scan.mk:101
#1 /home/yejq/program/openwrt/tmp/.packageinfo at /home/yejq/program/openwrt/include/scan.mk:113
#2 all at /home/yejq/program/openwrt/include/scan.mk:5
Most-recent (level 1) invocation:
remake V=ss -j1 -r -s -f include/scan.mk SCAN_TARGET=packageinfo SCAN_DIR=package SCAN_NAME=package SCAN_DEPTH=5 SCAN_EXTRA=
remake<<1>> bt
=>#0 /home/yejq/program/openwrt/tmp/info/.files-packageinfo.stamp at /home/yejq/program/openwrt/include/scan.mk:101
#1 /home/yejq/program/openwrt/tmp/.packageinfo at /home/yejq/program/openwrt/include/scan.mk:113
#2 all at /home/yejq/program/openwrt/include/scan.mk:5
Most-recent (level 1) invocation:
remake V=ss -j1 -r -s -f include/scan.mk SCAN_TARGET=packageinfo SCAN_DIR=package SCAN_NAME=package SCAN_DEPTH=5 SCAN_EXTRA=
remake
提供的where
命令与栈回溯指令bt
功能相似,反映了Makefile中的规则间依赖的关系。通过print
指令可以查看变量的值,通过expand
命令(简写为x
)可以对字符串展开:
remake<<2>> print TARGET_STAMP
include/scan.mk:10 (origin: makefile) TARGET_STAMP = /home/yejq/program/openwrt/tmp/info/.files-packageinfo.stamp
remake<<3>> expand $(NO_TRACE_MAKE) $(FILELIST)
remake V=ss /home/yejq/program/openwrt/tmp/info/.files-packageinfo-7109
remake<<4>> x $$(cat $(FILELIST) $(OVERRIDELIST) | $(MKHASH) md5 | awk '{print $$1}')
$(cat /home/yejq/program/openwrt/tmp/info/.files-packageinfo-7109 /home/yejq/program/openwrt/tmp/info/.overrides-packageinfo-7109 | /home/yejq/program/openwrt/staging_dir/host/bin/mkhash md5 | awk '{print $1}')
可见,该规则会调用remake
生成$(FILELIST)
,之后会将其内空导出进行MD5的较验。如果生成了新的较验值的文件($@.$$MD5SUM
,即较验值改变了),就会通过touch
命令更新$(TARGET_STAMP)
;当$(TARGET_STAMP)
被更新了,下面的规则就会被执行(否则不会被执行),从而重新计算openwrt工程中的软件包依赖关系:
$(TMP_DIR)/.$(SCAN_TARGET): $(TARGET_STAMP)
$(call progress,Collecting $(SCAN_NAME) info: merging...)
-cat $(FILELIST) | awk '{gsub(/\//, "_", $$0);print "$(TMP_DIR)/info/.$(SCAN_TARGET)-" $$0}' | xargs cat > $@ 2>/dev/null
$(call progress,Collecting $(SCAN_NAME) info: done)
echo
那么,更新了package/system/ubus/Makefile
文件的时间,如何导致$(FILELIST)
文件的较验值发生改变呢?实际上,其较验值没有发生改变,仅当软件包列表发生变化时才会改变。在include/scan.mk
中可以找到$(FILELIST)
的规则:
$(FILELIST): $(OVERRIDELIST)
rm -f $(TMP_DIR)/info/.files-$(SCAN_TARGET)-*
find -L $(SCAN_DIR) $(SCAN_EXTRA) -mindepth 1 $(if $(SCAN_DEPTH),-maxdepth $(SCAN_DEPTH)) -name Makefile | xargs grep -aHE 'call $(GREP_STRING)' | sed -e 's#^$(SCAN_DIR)/##' -e 's#/Makefile:.*##' | uniq | awk -v of=$(OVERRIDELIST) -f include/scan.awk > $@
可以肯定更新package/system/ubus/Makefile
的文件时间,通过find
指令生成的文件是相同的。关键在于include/scan.mk
下面的操作:
$(TMP_DIR)/info/.files-$(SCAN_TARGET).mk: $(FILELIST)
( \
cat $< | awk '{print "$(SCAN_DIR)/" $$0 "/Makefile" }' | xargs grep -HE '^ *SCAN_DEPS *= *' | awk -F: '{ gsub(/^.*DEPS *= */, "", $$2); print "DEPS_" $$1 "=" $$2 }'; \
awk -F/ -v deps="$$DEPS" -v of="$(OVERRIDELIST)" ' \
BEGIN { \
while (getline < (of)) \
override[$$NF]=$$0; \
close(of) \
} \
{ \
info=$$0; \
gsub(/\//, "_", info); \
dir=$$0; \
pkg=""; \
if($$NF in override) \
pkg=override[$$NF]; \
print "$$(eval $$(call PackageDir," info "," dir "," pkg "))"; \
} ' < $<; \
true; \
) > $@.tmp
mv $@.tmp $@
-include $(TMP_DIR)/info/.files-$(SCAN_TARGET).mk
根据GNU Make对包含的文件的重建机制,以上规则总是会被执行。这一规则涉及到多行变量PackageDir
,它通过awk
命令行工具生成了$(TMP_DIR)/info/.files-$(SCAN_TARGET).mk
,即动态地生成了Makefile,通过对PackageDir
的引用,导入openwrt中各个软件包的信息,其中就包括了软件包之间的依赖关系。本文提到的Collecting package info
实际上是对ubus
展开PackageDir
生成的消息。最后,自动生成的$(TMP_DIR)/info/.files-$(SCAN_TARGET).mk
部分内容如下:
DEPS_package/firmware/linux-firmware/Makefile=*.mk
DEPS_package/kernel/linux/Makefile=modules/*.mk $(TOPDIR)/target/linux/*/modules.mk $(TOPDIR)/include/netfilter.mk
$(eval $(call PackageDir,base-files,base-files,))
$(eval $(call PackageDir,boot_arm-trusted-firmware-mediatek,boot/arm-trusted-firmware-mediatek,))
$(eval $(call PackageDir,boot_arm-trusted-firmware-mvebu,boot/arm-trusted-firmware-mvebu,))
$(eval $(call PackageDir,boot_arm-trusted-firmware-rockchip,boot/arm-trusted-firmware-rockchip,))
$(eval $(call PackageDir,boot_arm-trusted-firmware-sunxi,boot/arm-trusted-firmware-sunxi,))
$(eval $(call PackageDir,boot_arm-trusted-firmware-tools,boot/arm-trusted-firmware-tools,))
remake的缺陷
上面的调试过程虽然用到了remake
,但用到的不多。固然有openwrt的构建脚本比较复杂的原因,但主要在于其存在一些缺陷,导致在调试时经常崩溃:
$ remake V=s -j1 menuconfig
remake[1]: Entering directory '/home/yejq/program/openwrt/scripts/config'
set -e; mkdir -p ./; trap "rm -f ./.mconf-cfg.tmp" EXIT; { /bin/sh mconf-cfg.sh; } > ./.mconf-cfg.tmp; if [ ! -r mconf-cfg ] || ! cmp -s mconf-cfg ./.mconf-cfg.tmp; then true ' UPD mconf-cfg'; mv -f ./.mconf-cfg.tmp mconf-cfg; fi
remake[1]: Leaving directory '/home/yejq/program/openwrt/scripts/config'
debugger() function called with parameter "trap into remake debugger"
:o (:32)
Segmentation fault (core dumped)
/home/yejq/program/openwrt/include/toplevel.mk:85: *** [prepare-tmpinfo] error 139
还有一些内存异常:
remake<<22>> continue
remake[1]: *** virtual memory exhausted. Stop.
/home/yejq/program/openwrt/include/toplevel.mk:85: *** [prepare-tmpinfo] error 2
#0 prepare-tmpinfo at /home/yejq/program/openwrt/include/toplevel.mk:76
#1 menuconfig at /home/yejq/program/openwrt/include/toplevel.mk:132
尽管如此,remake
仍然是学习GNU Make的重要工具之一:对于简单的工程,调试过程中不会出现很多的崩溃异常。