第三章 内存损坏

内存损坏指覆写一块不属于写者的内存;或者无效地修改内存即使内存属于写者。比如,在竞争条件下,数据被改成了参与的线程没有期待的东西。这些损坏的数据可以是内存管理器的内部堆数据结构或者是用户空间,即应用程序数据对象。这些错误最终显示的样子广泛来说很不同。

毫无疑问,内存损坏属于你需要调试的最困难的问题。主要原因是从广义上看,内存损坏和内存访问问题如内存上溢/下溢,重复释放,访问释放的内存,使用未初始化的变量等等,通常在问题发生的时刻/地方不会有什么症状。被损坏的数据要么深深潜伏在其他数据结构或者沿着控制流传播到很远。

取决于许多因素,内存损坏导致的可观察的症状变化多端。许多标准和文档简单地警告内存错误的结果是未定义。受影响的程序可能crash、行为奇怪、或者生成异常的计算结果。内存损坏导致的程序crash是内核确定一个程序在访问无效的内存时采取的措施。这就是著名的段错误或者访问错误异常,表示当前指令方外的内存地址不属于程序分配的地址空间(细节见第六章)

如果crash发生在错误代码运行时,开发人员发现问题是简单的,跟下面不会crash的情况比,这让它是一件“好事”。如果一个代码bug损坏了从内核角度看正常的内存,也就是说,如果访问的地址是内核分配给进程的空间,它不会立马crash程序。相反,它会静静地把数据修改错误,就像一个时间炸弹💣。它意外的爆炸是迟早的事情。不幸的是,大部分内存损坏属于后者,难以调试。

因为最后的失败很多时候在不相关的地方弹出来,很多资历浅的工程师感到吃惊和他们大多时候得到的结论是,怪发现者——内存损坏的受害者。当面对这样的问题时,工程师需要搞明白程序是怎么样达到特定的状态和确定错误的源头。用另外的话说,他需要明白内存损坏的“未定义”行为和解释它是怎么样开始隐藏了bug但是最后出其不意的方式显露出来。这需要更多关于内存管理器数据结构、编译器特性、架构协议和程序逻辑的密切知识。任何一个经历过的人会告诉你这是非常有挑战性的。在我们深入调试内存损坏的技巧前,让我们看一些常见的内存错误和看看他们是怎么损坏堆元数据的。