Ringbahn内存问题和内存管理陷阱
2023-11-20 20:02:43
在实现ringbahn[1]时,我至少发现了两个内存安全错误,这直接引发了一系列严重的问题,比如段错误、分配器中止以及非常难以理解的未定义行为。尽管我已修复了发现的错误,目前还无法证明代码库不存在更多的内存安全问题,但是我相信做好记录以供日后参考还是非常有必要的。
案例一
实现了一个名为LinearArena的内存分配器,它是 Ringbahn 的备用内存分配器。LinearArena 主要负责仅对连续内存块进行一次性分配。环形缓冲区非常类似,但它额外提供了覆盖已用空间的能力,以便进行重用,其内存使用量受到缓冲区大小的限制,该大小在分配期间确定。
在最早的版本中,LinearArena 曾经非常擅长重复使用先前分配的内存块(只要它们不重叠)。这看起来非常好,因为它最大限度地减少了执行内存分配所需的系统调用数量,内存分配通常非常耗时。
问题是 LinearArena 没有正确地处理重叠的情况,当发生内存块重叠时,LinearArena 会返回一个指针,该指针指向已释放的内存块内。这可能导致段错误、分配器中止以及代码中其他困难且无法理解的 bug。
值得庆幸的是,发现这个问题只需要几分钟,因为错误导致了一个段错误,很容易就可以追溯到错误根源。修复也很简单,我只需要添加一个检查即可验证块是否重叠。
案例二
这个bug非常难以发现,它源于一个地方有两个相同实例的 std::string 对象。当我发现这个问题时,我首先怀疑我犯了一个愚蠢的错误,在代码中错误地复制了内容。
当我修复这个错误并运行调试器时,调试器报告了一个段错误。我尝试了许多方法,但都没有找到任何问题。我重写了代码,直到意识到一个特定的 std::string 对象有两个副本,而它们应该只有一个。
错误发生在 std::move() 的调用方式上。我没有传递一个 rvalue 引用,而是传递了一个 lvalue 引用。这导致std::move() 尝试将 string 对象移动到它自身,这显然是不可能的。
发生这种情况是因为我试图通过引用方式而不是通过值的方式传递 string 对象。修复错误的唯一方法是传递一个值。
内存管理:一个永无止境的斗争
内存管理通常被认为是 C++ 中最困难的部分之一,而出现内存错误也往往是非常痛苦的。在项目的早期阶段,修复内存错误是比较容易的。但是,随着代码库的不断扩展,特别是在团队协作的背景下,解决内存错误将变成一项更加艰巨且耗时的任务。
即使是经验丰富的开发人员有时也会犯内存错误,内存错误常常非常难以发现。这些错误通常会导致很难理解的症状,并且追踪它们的根源可能是一项漫长而艰巨的任务。
为了最大限度地减少内存错误,我们可以采取许多预防措施。其中包括:
- 使用合适的工具 :即使我们非常小心,内存错误也总是可能出现的。因此,我们应该使用合适的工具来帮助我们检测和修复内存错误。这些工具包括内存调试器、静态分析工具以及单元测试。
- 进行彻底的测试 :我们应该对代码库进行彻底的测试,以便在错误进入生产环境之前发现它们。测试应该涵盖各种可能的输入和场景,并且应该定期运行。
- 遵循良好的编码规范 :遵循良好的编码规范可以帮助我们避免常见的错误,例如使用未初始化的变量或越界访问数组。
- 代码审查 :代码审查是发现代码错误的另一种有效方法。在代码提交到代码库之前,我们应该让其他开发人员审查我们的代码。
尽管采取了所有这些预防措施,内存错误仍然有可能发生。因此,我们应该始终做好准备,以便在错误发生时能够快速有效地修复它们。