在2018年初,我们的博客补充了有关Chromium项目源代码第六次检查的一系列文章。 该系列包括8条有关错误的文章以及如何预防错误的建议。 有两篇文章引起了热烈的讨论,我仍然偶尔会通过邮件收到有关其中所涉及主题的评论。 也许,我应该再作一些解释,正如他们所说的那样,将记录弄得很清楚。
自编写定期检查Chromium项目源代码的一系列文章以来已经过去了一年:
- 铬:第六次项目检查和250个错误
- 漂亮的铬和笨拙的Memset
- 突破和失败
- 铬:内存泄漏
- 铬:错别字
- 铬:使用不受信任的数据
- 为什么检查malloc函数返回什么很重要
- 铬:其他错误
专门讨论
memset和
malloc的文章引起了并继续引起争论,这使我感到奇怪。 显然,由于我在表达自己的想法时不够准确,所以有些困惑。 我决定返回这些文章并进行一些说明。
记忆集
让我们从有关
memset的文章开始,因为这里的一切都很简单。 出现了一些有关初始化结构的最佳方法的争论。 很多程序员写道,最好不要给出建议:
HDHITTESTINFO hhti = {};
但可以通过以下方式编写:
HDHITTESTINFO hhti = { 0 };
原因:
- 构造代码{0}在读取代码时比{}更容易注意到。
- 构造{0}比{}更直观易懂。 这意味着0表示该结构填充有零。
因此,读者建议我在文章中更改此初始化示例。 我不同意这些论点,也不打算在本文中进行任何编辑。 现在,我将解释我的观点并提供一些原因。
至于可见度,我认为这是个人品味和习惯的问题。 我认为括号内的0不会从根本上改变这种情况。
至于第二个论点,我完全不同意。 类型为{0}的记录提供了错误识别代码的原因。 例如,您可以假设如果将0替换为1,则所有字段都将用1初始化。 因此,这种写作风格很可能是有害的而不是有益的。
PVS-Studio分析仪甚至还具有相关的诊断
V1009 ,其描述在下面引用。
V1009。 检查阵列初始化。 仅第一个元素被显式初始化。分析器检测到可能与以下事实有关的错误:声明数组时,仅为一个元素指定该值。 因此,其余元素将由零或默认构造函数隐式初始化。
让我们考虑可疑代码的示例:
int arr[3] = {1};
也许程序员比
arr期望的要完全由
arr组成,但事实并非如此。 该数组将包含值1、0、0。
正确的代码:
int arr[3] = {1, 1, 1};
由于与构造
arr = {0}的相似性而可能发生这种混淆,该构造将整个数组初始化为零。
如果项目中积极使用了此类构造,则可以禁用此诊断。
我们还建议不要忽略代码的清晰度。
例如,用于编码颜色值的代码记录如下:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00 }; int Green[3] = { 0x00, 0xff };
由于隐式初始化,所有颜色均已正确指定,但最好更清楚地重写代码:
int White[3] = { 0xff, 0xff, 0xff }; int Black[3] = { 0x00, 0x00, 0x00 }; int Green[3] = { 0x00, 0xff, 0x00 };
分配
在进一步阅读之前,请回顾一下文章“
为什么检查malloc函数返回什么很重要 ”的内容。 这篇文章引起了很多辩论和批评。 以下是一些讨论:
reddit.com/r/cpp,reddit.com/r/C_Programming,habr.com (zh)。 有时读者仍会通过电子邮件将有关本文的信息发送给我。
这篇文章因以下几点而受到读者的批评:
1.如果 malloc 返回 NULL ,那么最好立即终止程序,而不是编写一堆 if -s并尝试以某种方式处理内存,因此,无论如何经常无法执行程序。直到错误越来越高,我才一直坚持到内存泄漏的后果为止。 如果允许您的应用程序在不发出警告的情况下终止其工作,那就这样吧。 为此,甚至在
malloc之后或使用
xmalloc进行一次检查就足够了(请参阅下一点)。
我反对并警告说缺少检查,因此程序将继续运行,好像什么都没发生。 这是完全不同的情况。 这很危险,因为它会导致未定义的行为,数据损坏等。
2.没有解决方案的描述在于编写包装器函数来分配内存,然后对其进行检查或使用已经存在的函数,例如 xmalloc 。同意,我错过了这一点。 在写这篇文章时,我只是没有考虑纠正这种情况的方法。 对我来说,向读者传达没有支票的危险更为重要。 如何解决错误是口味和实现细节的问题。
xmalloc函数不是标准C库的一部分(请参阅“
xmalloc和malloc之间有什么区别? ”)。 但是,此函数可以在其他库中声明,例如,在GNU utils库(
GNU libiberty )中。
该功能的要点是程序无法分配内存时会崩溃。 此功能的实现可能如下所示:
void* xmalloc(size_t s) { void* p = malloc(s); if (!p) { fprintf (stderr, "fatal: out of memory (xmalloc(%zu)).\n", s); exit(EXIT_FAILURE); } return p; }
因此,通过每次调用
xmalloc函数而不是
malloc ,可以确保由于使用空指针而导致程序中不会发生未定义的行为。
不幸的是,
xmalloc也不是万能药。 应该记住,在编写库代码时,使用
xmalloc是不可接受的。 稍后再说。
3.大多数评论如下:“实际上, malloc 从不返回 NULL 。”幸运的是,我不是唯一一个知道这是错误方法的人。 我真的很喜欢我的支持中的以下
评论 :
根据我讨论此主题的经验,我感觉到Internet中有两个教派。 第一个的拥护者坚信,在Linux下,malloc永远不会返回NULL。 第二个支持者全心全意地声称,如果无法在程序中分配内存,则无法执行任何操作,只能崩溃。 没有办法说服他们。 尤其是当这两个宗派相交时。 您只能将其作为给定的。 甚至在哪个专业资源上进行讨论也不重要。我想了一会儿,决定听从建议,所以我不会说服任何人:)。 希望这些开发人员小组仅编写非致命程序。 例如,如果游戏中的某些数据损坏了,那么其中就没有关键。
唯一重要的是库,数据库的开发人员不得这样做。
呼吁高度依赖的代码和库的开发人员
如果要开发库或其他高度相关的代码,请始终检查
malloc / realloc函数返回的指针的值,如果无法分配内存,则向外返回错误代码。
在库中,如果内存分配失败,则无法调用
exit函数。 出于同样的原因,您不能使用
xmalloc 。 对于许多应用程序,简单地中止它们是不可接受的。 因此,例如,数据库可能会损坏。 一个人可能会丢失经过数小时评估的数据。 因此,当多线程应用程序而不是正确处理不断增长的工作负载而只是终止时,可以实现该程序以“拒绝服务”漏洞。
无法假定将以何种方式和项目使用库。 因此,应该假定该应用程序可以解决非常关键的任务。 这就是为什么仅通过调用
exit杀死它是没有用的。 编写此类程序很可能会考虑到内存不足的可能性,并且在这种情况下可以执行某些操作。 例如,由于内存的碎片严重,CAD系统无法分配足够的内存缓冲区以进行常规操作。 在这种情况下,这不是它在紧急模式下因数据丢失而崩溃的原因。 该程序可以提供保存项目并正常重启的机会。
在任何情况下,都不可能依靠
malloc始终能够分配内存。 还不知道在哪个平台上以及如何使用该库。 如果一个平台上的内存不足情况很奇怪,那么在另一个平台上可能很常见。
我们不能期望如果
malloc返回
NULL ,那么程序将崩溃。 什么都可能发生。 正如我在
文章中所描述的,程序可能不会通过空地址写入数据。 结果,某些数据可能会被破坏,从而导致不可预测的后果。 即使是
记忆集也是危险的。 如果填充数据的顺序相反,则首先会破坏某些数据,然后程序将崩溃。 但是崩溃可能为时已晚。 如果在
memset函数运行时在并行线程中使用受污染的数据,则后果可能是致命的。 您可以在数据库中获取损坏的事务,也可以发送命令来删除“不必要的”文件。 一切都有可能发生。 我建议读者自己做梦,由于内存中使用垃圾会发生什么。
因此,该库只有一种使用
malloc函数的正确方法。 您需要立即检查该函数是否返回,如果为NULL,则返回错误状态。
其他连结
- OOM处理
- 使用NULL指针的乐趣: 第1 部分 , 第2部分
- 每个C程序员应该了解的未定义行为: 第1 部分 , 第2 部分 , 第3部分