GNU Makefile--调试器remake

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的重要工具之一:对于简单的工程,调试过程中不会出现很多的崩溃异常。

上一篇:make和makefile


下一篇:C语言项目编译