Makefile调试、检查目标和依赖的时间关系

admin 2025年1月16日23:31:08评论7 views字数 7248阅读24分9秒阅读模式

Makefile调试、检查目标和依赖的时间关系

本文原创发布于微信公众号“洛奇看世界”

我在上一篇《Makefile新手?千万别错过了这个入门教程》推荐了李云的《驾驭 Makefile》作为新手入门 Makefile 的推荐教程。

在该教程中存在两个问题:

  1. 编译时出现死循环,不断反复生成 main.dep 和 foo.dep
  2. 每次编译时都会反复交替生成 foo.o 和 main.o

这两个问题都由同一个原因引起,即目标文件对目录的依赖,而依赖的目录中每次更新文件时都会更新目录的修改时间。

本文追根溯源,详细展示这两个问题的调试流程和解决办法。

全文分为四个部分:

  1. 问题和源码
  2. 无限生成依赖文件的分析
  3. 无限生成依赖文件的解决
  4. 交替生成文件的原因和解决办法

如果只关注问题的原因分析,请跳转到第2.3节; 

如果只关注问题的解决办法,请直接跳转到第3节;

1. 问题和源码

1.1 无限和反复生成文件的问题

这里的两个问题分别指:

  • 无限循环生成 foo.depmain.dep 文件
  • 反复交替生成 foo.omain.o 文件

具体来说,就是:

  1. 编译时出现死循环,不断反复生成 main.dep 和 foo.dep
Makefile调试、检查目标和依赖的时间关系
图 1. 反复生成 main.dep 和 foo.dep
  1. 每次编译时都会反复交替生成 foo.o 和 main.o
Makefile调试、检查目标和依赖的时间关系
图 2. 反复交替生成 foo.o 和 main.o

从第二次 make 开始,先生成 foo.o,然后生成 main.o,再次编译时先生成 main.o,然后生成 foo.o……,不断循环往复。

1.2 源码

如果你想亲自查看下这个问题,代码仓库地址:

https://github.com/guyongqiangx/makefile-study

关于这个问题的调试信息位于 clock-skew-issue 分支上:

$ git clone https://github.com/guyongqiangx/makefile-study -b clock-skew-issue
cd makefile-study/Makefile/3-complicated/
makefile-study/Makefile/3-complicated$ ls -lh
total 40K
-rw-rw-r-- 1 rocky rocky 5.7K Dec 24 17:00 debug-log-0-clock-skew-issue.log
-rw-rw-r-- 1 rocky rocky 2.4K Dec 24 17:00 debug-log-fix1-no-depending-deps-dirs.log
-rw-rw-r-- 1 rocky rocky 2.8K Dec 24 17:00 debug-log-fix2-update-modification-time.log
-rw-rw-r-- 1 rocky rocky   87 Dec 24 17:00 foo.c
-rw-rw-r-- 1 rocky rocky   55 Dec 24 17:00 foo.h
-rw-rw-r-- 1 rocky rocky   54 Dec 24 17:00 main.c
-rw-rw-r-- 1 rocky rocky 1017 Dec 24 17:00 Makefile
-rw-rw-r-- 1 rocky rocky 1.1K Dec 24 17:00 Makefile.Fix1
-rw-rw-r-- 1 rocky rocky 1.2K Dec 24 17:00 Makefile.Fix2
makefile-study/Makefile/3-complicated$ 

2. 无限生成依赖文件的分析

以下是无限循环生成 foo.depmain.o 文件的问题的分析过程。

在 Makefile 中,一个目标是否需要更新,由这个目标和依赖的最后修改时间(last-modification time)决定。

如果存在以下两种情况之一:

  1. 依赖的最后修改时间比目标的最后修改时间新
  2. 目标不存在

make 就会执行相应的命令来更新目标。

第 1 条比较好理解

目标文件 complicated.exe 依赖于另外两个文件 foo.omain.o,只要后两者有任何一个比目标文件新,那就会更新目标文件 complicated.exe

第 2 条,目标不存在,可以换个思路转换成第 1 条的情况

假想目标存在,但其最后修改时间为 0,即时间戳 1970-01-01 00:00.000,因为目前电脑 UTC 时间的起始值 0 就对应这个时间点,而时间不会出现负数,因此没有时间能比这个时间更早了。

问题:
有没有电脑显示文件的时间戳早于 1970-01-01 00:00.000 呢?

因此,这两条的情况都是目标比依赖新,所以当更新完成后,目标的最后修改时间会比依赖的最后修改时间新,至少一样新。

关于这部分,在官方 GNU Make Manual 的 4.2 Rule Syntax 一节中原话是这么说的:

Makefile调试、检查目标和依赖的时间关系
图 3. Make 时间依赖规则

原文链接:https://www.gnu.org/software/make/manual/make.html#Rule-Syntax

我们可以通过调试来确认是不是这个原因。那如何调试呢?

2.1 比较目标和依赖的时间戳

既然是通过最后修改时间来判断目标是否需要更新,那我们可以在 Makefile 中手动获取 deps 目录和目录下的 *.dep 文件的最后修改时间,通过比较这两个时间来验证我们的想法。

获取文件的时间戳

如何获取一个文件的时间戳呢,度娘一下,发现可以通过stat命令来搞定。

stat 命令默认的输出信息比较多,如下:

stat main.c
  File: ‘main.c’
  Size: 54         Blocks: 8          IO Block: 4096   regular file
Device: 801h/2049d Inode: 188292      Links: 1
Access: (0664/-rw-rw-r--)  Uid: ( 1001/   rocky)   Gid: ( 1001/   rocky)
Access: 2020-12-24 17:00:56.114282103 +0800
Modify: 2020-12-24 17:00:56.114282103 +0800
Change: 2020-12-24 17:00:56.114282103 +0800
 Birth: -

但其实我们感兴趣的只是倒数第三行的修改时间(Modify)。

通过查看 stat 命令的选项,--format--printf 可以用于自定义输出格式。

在具体的格式上:

  • %Y 用于显示整数时间戳,但不容易阅读
  • %y 用于显示容易识别的时间
  • %n 用于显示文件名

(更多格式请参考命令 man stat 的输出)

我们自定义 stat 获取文件的修改时间如下:

# 使用 '--printf' 选项自定义输出格式
stat --printf="%yt%nn" main.c
2020-12-24 17:00:56.114282103 +0800 main.c

获取目标和依赖的最后修改时间

为了检验 deps 目录,以及目录中 *.dep 文件的时间戳变化情况,我们将 Makefile 的 %.dep 规则修改如下:

Makefile调试、检查目标和依赖的时间关系
图 4. 显示目录和文件的时间戳

主要新增了两个白色方框包含的部分,

  • 38 行显示改动前 deps 目录的时间戳
  • 44 行显示改动后deps目录的时间戳
  • 45 行显示在改动后 deps 目录下所有 .dep 文件的时间戳
  • 47 行的 sleep 用于编译时暂停,方便观察时间戳

运行的效果如下:

Makefile调试、检查目标和依赖的时间关系
图 5. 编译显示文件时间戳

在上面的红色方框中,deps 目录在生成了 deps/main.dep 文件以后,时间戳发生了变化:

17:47:05.582369639 --> 17:47:05.594369640

在下面的绿色方框中,deps 目录在生成了 deps/foo.dep 文件以后,时间戳也发生了变化:

17:47:05.594369640 --> 17:47:07.638369704

deps/main.dep 的时间戳保持了不变。

从而验证了我们的猜想,修改 deps 目录下的文件导致 deps 的目录时间戳被更新了。

再次在 deps 目录下生成 foo.dep 文件时,导致 deps 目录的最后修改时间比先生成的文件 main.dep 最后修改时间新。

换句话说,此时规则 deps/main.dep: deps main.c 的依赖满足不了了,需要重新生成 deps/main.dep

到底是不是这样了,我们通过其它方式验证下。

2.2 使用 --debug 选项调试 Makefile

为了调试 Makefile,自然要检查下 make 命令都支持哪些选项。

在 GNU Make Manual 的 9.7 Summary of Options 中列举了所有支持的选项,其中跟调试相关的选项有 -d--debug[=options]。其中 --debug 的 options 又有多个选择。

make完整的命令行选项参考man make输出或下面的官方文档:

  • https://www.gnu.org/software/make/manual/make.html#Options-Summary

你或许会问,这么多option我到底该用哪个?说实话,一开始我也不知道,好在我们的 Makefile 比较简单,可以一个一个的挨个试,看看哪个的输出信息能满足我们的需要。

最后发现 --debug=m 的输出信息刚好是我们需要的,为了方便查看,我将 Makefile 中添加的打印信息以及make输出的版权信息去掉后如下:

完整 log 请参考代码目录下的文件:debug-log-0-clock-skew-issue.log

makefile-study/Makefile/3-complicated$ make --debug=m
...
Reading makefiles...
Updating makefiles....
  File 'deps/main.dep' does not exist.
    File 'deps' does not exist.
   Must remake target 'deps'.
...
   Successfully remade target file 'deps'.
 Must remake target 'deps/main.dep'.
...
 Successfully remade target file 'deps/main.dep'.
  File 'deps/foo.dep' does not exist.
 Must remake target 'deps/foo.dep'.
...
 Successfully remade target file 'deps/foo.dep'.
Re-executing[1]: make --debug=m
...
Reading makefiles...
Updating makefiles....
  Prerequisite 'deps' is newer than target 'deps/main.dep'.
 Must remake target 'deps/main.dep'.
...
 Successfully remade target file 'deps/main.dep'.
Re-executing[2]: make --debug=m
...
Reading makefiles...
Updating makefiles....
  Prerequisite 'deps' is newer than target 'deps/foo.dep'.
 Must remake target 'deps/foo.dep'.
...
 Successfully remade target file 'deps/foo.dep'.
Re-executing[3]: make --debug=m

这段 log 一共执行了3遍 make 命令,翻译过来如下:

  • 第一遍执行 make --debug=m:
    1. 读取所有 makefile,并更新;
    2. 文件 deps/main.dep 不存在,文件 deps/main.dep 依赖的 deps 目录也不存在;
    3. 创建 deps 目录,生成 deps/main.dep 文件;
    4. 文件 deps/foo.dep 不存在;
    5. 生成 deps/foo.dep 文件;
    6. 重新执行 make --debug=m 命令;
  • 第二遍执行 make --debug=m:
    1. 文件 deps/main.dep 的依赖 deps 目录修改时间比 deps/main.dep 目录新;
    2. 生成 deps/main.dep 文件;
    3. 重新执行 make --debug=m 命令;
  • 第三遍执行 make --debug=m
    1. 文件 deps/foo.dep 的依赖 deps 目录修改时间比 deps/foo.dep 目录新;
    2. 生成 deps/foo.dep 文件;
    3. 重新执行 make --debug=m 命令;

这里:

  • 执行第二遍时发现之前生成的 deps/main.dep 依赖满足不了,重新生成 deps/main.dep
  • 执行第三遍时发现另一个文件 deps/foo.dep 的依赖满足不了,重新生成 deps/foo.dep
  • 循环往复,真是冤冤相报何时了……

2.3 无限循环生成deps/*.dep文件的原因

总结一下,通过上面第二步的调试,我们发现导致无限循环生成 deps/*.dep 的根本原因是:

  1. 编译时先生成了deps/main.dep文件,后生成deps/foo.dep文件,每次生成deps/*.dep文件的同时都会更新deps目录的修改时间。后生成deps/foo.dep文件的结果就是deps目录的修改时间比deps/main.dep文件新;
  2. 而包含指令-include $(DEPS)的存在,会要求更新了deps/main.depdeps/foo.dep文件以后重新读取makefile,检查依赖关系并再次执行;
  3. 再次执行时发现deps目录的修改时间比deps/main.dep文件新,此时就会重新生成deps/main.dep文件,结果就是deps目录的修改时间比deps/foo.dep文件新;
  4. 而包含指令-include $(DEPS)的存在,更新dep/main.deps文件以后会重新读取makefile,检查依赖关系并再次执行;
  5. 再次执行又发现deps的时间比deps/foo.dep文件新,又重新生成deps/foo.dep,结果就是deps目录的修改时间又一次比deps/main.dep文件新;

事故就这样发生了……

3. 无限生成依赖文件的解决

无限循环生成.dep文件的解决办法

既然问题的根源是目录下的文件依赖于目录自身,而依赖关系要求目标文件比依赖的目录新(如果不是更新,那至少也要一样新)。

解决思路有两条:

  1. 去掉文件对对目录的依赖;
  2. 始终保证目标文件比依赖的目录新(至少一样新);

从上面的两个解决思路出发就产生了两个解决方案。

方案一,去掉文件内对目录的依赖

去掉 %.dep 文件对 $(DIR_DEPS) 目录的依赖,在编译时检查 $(DIR_DEPS) 目录是否存在,如果不存在,则手动创建该目录。

#$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
$(DIR_DEPS)/%.dep: %.c
 @echo "Making $@..."
 @set -e; 
 if [ ! -e $(DIR_DEPS) ]; then 
  $(MKDIR) $(DIR_DEPS); 
 fi
 $(RM) $(RMFLAGS) $@.tmp; 
 $(CC) -E -MM $(filter %.c, $^) > $@.tmp; 
 sed 's,(.*).o[ :]*,objs/1.o: ,g' < $@.tmp > $@
 $(RM) $(RMFLAGS) $@.tmp;

方案二,保证文件比目录新

在每次生成 %.dep 文件后,用 touch 命令更新目录下所有文件的修改时间。

$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
 @echo "Making $@..."
 @set -e; 
 $(RM) $(RMFLAGS) $@.tmp; 
 $(CC) -E -MM $(filter %.c, $^) > $@.tmp; 
 sed 's,(.*).o[ :]*,objs/1.o: ,g' < $@.tmp > $@
 $(RM) $(RMFLAGS) $@.tmp; 
 touch $(DIR_DEPS)/*;

4. 交替生成文件的问题

交替生成 foo.omain.o 的原因和解决办法

和上面2.3节一样,重点还是检查目标和依赖的时间戳,这里不再详细分析调试过程。

原因如下:

objs 目录下的文件 *.o 依赖于目录自身,而每次更新 foo.omain.o 都会更新 objs 目录的修改时间。每更新一次 objs 目录下的文件都会导致 objs 目录自身比下面的一些 *.o 文件新。当再次执行时,就会更新那些比 objs 目录旧的文件。

那为什么没有发生无限循环的问题呢?

因为没有 include 指令去包含生成的 objs/*.o 文件,所以即使每次更新 objs/*.o 文件以后出现依赖满足不了的问题,并不会马上执行 makefile。

因此不会有无限的循环编译,但当你下次在命令行再次执行 make 命令时,就会发现会更新 objs/*.o 文件。

解决办法也参考前面的第 3 节的解决办法。

5. 总结

为了找出问题的根本原因,这里使用了两种手段:

  1.  使用 stat 命令检查目标和依赖的最后修改时间

  2. 使用 make --debug=m 跟踪 Make 执行流程

整个分析过程有些啰嗦,重点提出假设,通过逐步调试验证假设,找到问题的根本原因,最终才能提出解决的办法。

你有什么调试 Makefile 的好方法吗?欢迎留言或微信交流。

原文始发于微信公众号(哆啦安全):Makefile调试、检查目标和依赖的时间关系

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月16日23:31:08
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Makefile调试、检查目标和依赖的时间关系https://cn-sec.com/archives/1086173.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息