4

valgrind 快速入门页面提到:

试着让你的程序干净到 Memcheck 不会报告任何错误。一旦你达到了这个状态,就更容易看到程序的更改何时导致 Memcheck 报告新的错误。多年使用 Memcheck 的经验表明,即使是大型程序也可以以干净的方式运行 Memcheck。例如,KDE、OpenOffice.org 和 Firefox 的大部分都是 Memcheck-clean,或者非常接近它。

这个块让我有点困惑。鉴于 C 标准的工作方式,我会假设大多数(如果不是全部)产生 memcheck 错误的做法会在程序上调用未定义的行为,因此应该像瘟疫一样避免。

然而,引用块中的最后一句话暗示实际上有“著名”程序在生产环境中运行,但存在 memcheck 错误。读完这篇文章后,我想我应该对此进行测试,并尝试使用 valgrind 运行 VLC,在启动它后立即收到一堆 memcheck 错误。

这让我想到了这个问题:是否有充分的理由不从生产程序中消除此类错误?发布包含此类错误的程序是否有任何好处?如果是,开发人员如何确保它的安全,尽管据我所知,包含此类错误的程序可能会发生不可预测的行为并且没有办法对其一般行为做出假设?如果是这样,您能否提供真实世界的示例,说明程序在出现这些错误时比没有错误时更好?

4

5 回答 5

2

有一个案例,修复 Valgrind 报告的错误实际上导致了安全漏洞,请参见例如https://research.swtch.com/openssl。使用未初始化内存的目的是通过一些随机字节来增加熵,修复导致更可预测的随机数,确实削弱了安全性。

如果是 VLC,请随时调查 ;-)

于 2021-05-23T22:33:31.330 回答
1

如果一段代码在永远不会导致未初始化的存储包含它不能泄漏的机密信息的上下文中运行,则某些算法可能会受益于保证读取未初始化的存储除了产生可能无意义的值之外不会产生任何副作用。例如,如果需要快速设置一个哈希映射,在它被拆除之前通常只有少数项目放置在其中,但有时可能有很多项目,一个有用的方法是有一个包含数据项的数组和具有按添加顺序排列的值,以及将哈希值映射到存储槽编号的哈希表。如果存储到表中的项目数为 N,则项目的哈希为 H,并且尝试访问 hashTable[H] 保证会产生一个值I这将是存储在那里的数字(如果有的话),或者是任意数字,然后会发生以下三种情况之一:

  • I可能大于或等于N。在这种情况下,该表不包含哈希值为 H 的值。

  • I可能小于N,但是items[I].hash != H。在这种情况下,该表不包含哈希值为 的值H

  • I可能小于Nitems[I].hash == H。在这种情况下,该表显然包含至少一个值(在 slot 中的那个I),其哈希值为H

请注意,如果未初始化的哈希表可能包含机密数据,则可以触发哈希请求的对手可能能够使用定时攻击来获取有关其内容的一些信息。但是,从哈希表槽读取的值可能会影响除执行时间之外的函数行为的任何方面的唯一情况是写入哈希表槽的情况。

换句话说,哈希表将包含需要正确读取的已初始化条目和无意义的未初始化条目,其内容无法明显影响程序行为,但代码可能无法确定一个条目可能会影响程序的行为,直到它读取它之后。

对于程序在预期读取初始化数据时读取未初始化数据将是一个错误,并且由于程序尝试读取数据的大多数地方都会期待初始化数据,因此大多数读取未初始化数据的尝试都是错误。如果一种语言包含一个结构来明确要求实现要么读取已编写的数据,否则产生一些没有副作用的任意值,那么将尝试在没有这种结构的情况下读取未初始化的数据视为缺点。然而,在没有这种结构的语言中,避免有关读取未初始化数据的警告的唯一方法是放弃一些有用的算法,否则这些算法可能会受益于上述保证。

于 2021-05-24T15:07:54.253 回答
1

我在 Stack Overflow 上关于 Valgrind 的帖子的经验是,通常存在一种或两种错误的过度自信感,或者对编译器和 Valgrind 正在做什么缺乏理解[这些观察都不是针对 OP]。由于上述任何一个原因而忽略错误都是灾难的根源。

Memcheck 误报非常罕见。我使用 Valgrind 已经很多年了,我一方面可以数出我遇到的误报类型。也就是说,Valgrind 开发人员和优化编译器发出的代码之间正在进行一场战斗。例如看这个链接(如果有人感兴趣,在 FOSDEM 网站上还有很多关于 Valgrind 的很好的介绍)。一般来说,问题在于优化编译器可以进行更改,只要行为没有可观察到的差异。Valgrind 已经对可执行文件的工作方式进行了假设,如果新的编译器优化步骤超出了这些假设,可能会导致误报。

假阴性通常意味着 Valgrind 没有正确封装某些行为。通常这将是 Valgrind 中的一个错误。

Valgrind 无法告诉你错误的严重程度。例如,您可能有printf一个指向字符数组的指针,该字符数组包含一些未初始化的字节,但始终以 nul 终止。Valgrind 会检测到一个错误,在运行时你可能会在屏幕上看到一些随机的垃圾,这可能是无害的。

我遇到的一个修复可能不值得努力的例子是使用该putenv功能。如果您需要将动态分配的字符串放入环境中,那么释放该内存是一件痛苦的事情。您要么需要将指针保存在某处,要么保存一个指示 env var 已设置的标志,然后在可执行文件终止之前调用一些清理函数。所有这些只是为了泄漏大约 10-20 个字节。

我的建议是

  1. 力求代码中的零错误。如果您允许大量错误,那么判断您是否引入新错误的唯一方法是使用过滤错误的脚本并将它们与某些参考状态进行比较。
  2. 确保您了解 Valgrind 生成的错误。如果可以的话,修复它们。
  3. 使用抑制文件。谨慎使用它们来处理您无法修复的第三方库中的错误、修复比错误更糟糕的无害错误以及任何误报。
  4. 尽可能使用 -s/-v Valgrind 选项并删除未使用的抑制(这可能需要一些脚本)。
于 2021-05-26T08:48:06.713 回答
1

memcheck 并不完美。以下是误报率较高的一些问题和可能原因:

  1. memcheck 的能力和影子位传播相关规则以减少开销 - 但它会影响误报率
  2. 标志寄存器的不精确表示
  3. 更高的优化级别

来自 memcheck论文(发表于 usenix 2005)——但从那时起事情可能肯定发生了变化。

像 Memcheck 这样的系统不能同时没有假阴性和假阳性,因为这相当于解决了停机问题。我们的设计试图几乎完全避免误报并尽量减少误报。实践经验表明,这大多是成功的。即便如此,过去两年的用户反馈揭示了一个有趣的事实:许多用户(通常未说明)期望 Memcheck 根本不应该报告任何误报,无论被检查的代码多么奇怪。

我们认为这是不现实的。更好的期望是接受误报很少但不可避免的事实。因此,有时需要在代码中添加虚拟初始化以使 Memcheck 保持安静。这可能会导致代码比它严格需要的稍微保守一些,但至少它提供了更强的保证,即它确实没有使用任何未定义的值。一个有价值的目标是实现 Memcheck-cleanness,以便立即发现新的错误。这与修复源代码以删除所有编译器警告没有什么不同,即使是那些明显无害的警告。

许多大型程序现在确实运行 Memcheck-clean,或者几乎如此。根据作者的个人经验,最近的 Mozilla 版本与 OpenOffice.org-680 开发分支的清理版本以及大部分 KDE 桌面环境都接近。所以这是一个可以实现的目标。

最后,我们会观察到 Memcheck 最有效的使用不仅来自临时调试,而且还经常用于运行其自动回归测试套件的应用程序。这样的套件往往会在实现的黑暗角落进行练习,从而增加其 Memcheck 测试的代码覆盖率。

这是关于避免误报的部分:

Memcheck 的误报率非常低。但是,一些手工编码的汇编序列和一些非常罕见的编译器生成的习语可能会导致误报。

您可以使用选项找到错误的根源--track-origins=yes,您也许可以看到发生了什么。

于 2021-05-23T22:52:21.883 回答
1

一个例子是当您故意编写不可移植的代码以利用系统特定的优化时。相对于 C 标准,您的代码可能是未定义的行为,但您碰巧知道您的目标实现确实以您想要的方式定义了行为。

一个著名的例子是优化的strlen实现,例如在向量化 strlen 摆脱读取未分配内存时讨论的那些. 如果允许它们潜在地读取字符串的终止空字节,则可以更有效地设计此类算法。这对于标准 C 来说是明显的 UB,因为这可能超出了包含字符串的数组的末尾。但是在典型的现实生活机器上(例如 x86 Linux),您知道实际会发生什么:如果读取触及未映射的页面,您将获得 SIGSEGV,否则读取将成功并为您提供碰巧的字节在那个记忆区域。因此,如果您的算法检查对齐以避免不必要地跨越页面边界,它对于 x86 Linux 可能仍然是完全安全的。(当然,您应该使用适当的 ifdef 来确保此类代码不会在您无法保证其安全性的系统上使用。)

另一个与 更相关的例子memcheck可能是,如果您碰巧知道系统的malloc实现总是将分配请求四舍五入到 32 字节的倍数。如果您已经分配了一个缓冲区,malloc(33)但现在发现您还需要 20 个字节,您可以节省自己的开销,realloc()因为您知道您实际上有 64 个字节可以使用。

于 2021-05-23T22:34:21.093 回答