臭名昭著的 printf 修复

The notorious printf fix

本文关键字:修复 printf 臭名昭著      更新时间:2023-10-16

根据我的经验,我在使用 printf(或任何其他 std out 日志记录)进行调试时遇到了一些奇怪的行为。

行为 1 :

一种常见的情况是在多线程应用程序中使用 printf 以查找发生某些错误的原因,并使用 printf 突然"修复"错误(ofc printfs 在攻击性调用的地方,导致巨大的输出)。

在这种情况下,我认为 printf 增加了一些延迟,因此可能有一些低优先级线程没有 CPU,所以我开始朝这个方向看。

在奇迹 printf 修复之后,我关注的另一个方向是同步,因为我推测对 printf 的调用虽然是多线程的,但由系统在后面同步,因此具有 printf 的不同线程通过等待彼此完成对 I/O 缓冲区的写入来在它们之间同步。

Q1 : 我关于第一种情况的两个假设是否正确?

Q2 : 当这种情况发生时,我应该考虑其他方向吗?

行为 2 :

这种情况很少发生,但是当遇到这种情况时,即使是高级开发人员也会质疑自己,我真的很感激对此的解释。

它是这样的:

  • 代码不起作用...(清理、编译、运行)
  • 代码仍然不起作用,所以你添加一个printf看看为什么(清理,编译,运行)
  • 代码开始正常工作....删除以前添加的 printf(清理、编译、运行)
  • 代码现在工作正常!!!!!!!!!(挠头,难以置信地盯着)。

在实践中,我更多地使用这种方法,然后一旦这种方法修复 CPU 可能会比发生一次更多的与 pe 挂钩的错误:Android"cpu 可能被钉住"错误。

实际上运行良好,以至于它成为一个已知的"修复"(如果它从第一次尝试开始不起作用,您只需重复该过程直到它消失)。

请注意,代码已正确清理,它从来都不是与较旧的编译对象链接的问题。

最流行的猜测之一是编译的代码是不同的,原因未知(编译器是否根据某个文件的行(包括空格)有一些随机性?

Q3 : 这种行为的原因可能是什么(我也愿意猜测)?尽管代码相同,编译器可以生成不同的程序集吗?

请注意,我所说的项目非常大,具有多个静态库,因此这些行为无法在小代码片段上复制(尽管我也听说过单文件程序上发生的场景 2)。

Q1:你可以通过查找来自两个不同printf的交错字符来判断printf是否在幕后同步。如果没有交错,则printf同步。我希望这比占用 CPU 更有可能解决问题。

问题 2:我会寻找未正确保护互斥锁的共享资源。

问题 3:编译器在优化过程中可能会使用随机数。例如,如果编译器有 32 个变量和 8 个寄存器要放入它们,它可能会"掷骰子"以确定将哪些变量放入寄存器中。 您可以通过禁用优化来测试此理论。 如果不进行优化,输出应该是一致的。你可以通过比较二进制来检验这个理论。

> printf() 的线程安全性在其他问题中讨论,例如在这里对于 Linux,同样值得注意的是,任何外联函数调用都可能导致写回内存 - 引用 David Butenhoff 的话

"在实践中,大多数编译器不会尝试保留 跨调用外部函数的全局数据,因为它太 很难知道例程是否可以以某种方式访问 数据的地址。

任何一个方面都可能意味着由于未能正确使用同步指令而导致的未定义行为可以通过调用printf()来"屏蔽"(根据您的体系结构、问题的确切性质等,其可靠性程度不同)。

正如您所说,呼叫printf所花费的时间也会影响竞争条件产生不利影响的频率。

关于重新编译修复错误的程序:首先,如果错误首先是间歇性的,那么在某些更改之前而不是之后观察它并不一定证明任何因果关系。 可能还有其他因素,例如由于防病毒扫描,备份计划,其他用户等其他因素而导致的系统负载减少。 其次,可执行文件可能不同:编译器可能会注入诸如递增版本号或构建时间戳或其他一些系统数据之类的东西 - 例如数据长度的变化可能会对其他数据的对齐产生连锁反应,并产生许多微妙的后果。 编译器也可能使用地址随机化或其他一些技术,这再次可能会影响数据对齐 - 以可能掩盖错误或改变性能的方式。