大型的C/C++程序做到最后,bug高发的时期过去之后,一般来说会出现的问题都是很妖的。最最常见的现象就是一下几种:
- 调用栈全部混乱,一堆问号出现在这里,你明明知道自己编译的时候有放入符号,现在却什么信息都看不到
- 莫名其妙的调用了一个代码里面没有调用的函数
- 重复的free或者delete某块内存,然后崩溃
- 部分静态或者全局的数据混乱了,代码里面除了初始化就没啥地方写过他们了
- 莫名的某个对象的数据全部混乱了
其实有经验的程序员看到我上面这堆牢骚就知道,归根结底,这些问题都是一个原因:内存混乱。可以直接操作内存是C/C++的效率之源也是一切的罪恶之源。没有个两三年的编程经验,很难深刻的理解内存、指针和引用这些看起来没啥深奥的问题。关于可能出现的内存混乱我大概的归纳一下:
- 使用没有初始化的内存
- 内存越界
- 内存泄漏
为了理解如何处理这些情况,我们先稍微总结一下程序的内存的使用会出现在什么地方:
- 静态,全局内存:静态和全局对象会在main函数之前由libc进行初始化和分配,他们因该会出现在运行的数据段上,一般来说是数据段的开头部分。问题2, 4和5可能由这种内存的混乱引起。
- 栈内存:基本上可以认为是局部变量初始化的内存了,在程序入栈的时候分配,出栈的时候丢弃。这种内存混乱可能会引起问题1, 2, 4, 5
- 堆内存:由malloc、new分配,由free、delete回收的内存。这种内存混乱的话,会引起2、3、4、5
基本上想要彻底在机制上解决内存问题,肯定是没有希望的,因为如果这是可行的,那么早就有nb的人做出来,并放到标准里面了。于是他们做出了java。。。。。所以我们现在追求的就是如何能够快速的追踪到问题的源头。
不像是其他的逻辑bug,内存混乱引起的问题,现象是千奇百怪的,而且发生问题的源头往往和现象不在同一个调用栈里面,或者是整个调用栈全部乱掉,让你无从可查。所以必须要使用一些其他手段来帮助我们。根据不同的内存使用种类,我说一下自己的思路,和目前已经有的做法:
- 堆内存:为什么先从堆开始说起,因为堆上的内存分配和删除是完全控制在我们自己手里面的,说实话,这块东西早就有很多的工具和代码进行跟踪保护和分配了。而且相关的原理基本相通。
- 最完整的办法解决这个问题是建立自己的内存管理体系,使用内存池,托管系统的malloc、free或者是new、delete。这样不仅仅可以在犯错的时候我们有机会去监测或者记录。还可以通过内存池来增加系统的效率。
- 如果暂时没有成体系的内存管理系统,也可以使用第三方工具,比如非侵入式的valgrin或者是编译到代码里面的efence。这些工具都可以很好的帮你监控报告这些内存的混乱。
- 对于内存溢出这个问题,不论是自己的内存管理体系还是第三方的监测工具,基本上都是一个原理。在分配内存的时候多申请一定的字节,比如20个bytes,并将他们初始化为一些特殊的字节。然后去检查这块内存有没有变化。如果有变化就是出现了溢出。最佳的方法是用cpu或者是操作系统提供的内存保护机制,这样一旦有任何的要对这块内存的操作指令,就会立刻break出来,这时再用gdb等的debug工具就可以直接查看调用栈找到元凶。较差的方法是定期或者是析构的时候监测,不过这样效果不好,只能知道到底有没有溢出,或者是哪块内存溢出。到底是谁干的,还是要大海捞针。
- 栈内存:栈内存的混乱基本上会死的很直接,不会让问题慢慢的慢慢的变大。不过栈内存的特殊性,会导致他把案发现场的很多痕迹抹去,因为栈混乱了,所以你看到的调用栈完全是混乱的。你不知道出现的原因是什么。
- 对于这种情况,我暂时使用了一种看起来有点简陋的方法。就是使用一个buffer来手动的存储目前的调用栈。入栈的时候通过宏把函数名加入buffer,退栈的时候通过宏把函数名移除。坏处是增加了入栈出栈的代价,同时要记录的函数需要在开始和结束的时候添加宏。
- 静态、全局内存混乱:这块是比较复杂的,特别是出现了这种内存的溢出,他会悄无声息的抹去你很多的全局,静态变量,但是系统依然可以运行,不过很多依赖于这些变量的行为都会变的无法预测,天知道这时候会做出什么变态的事情。同时这种内存的分配是在进入main之前发生的,我们完全无能为力。想要对付这种内存我建议。。。。。
- 在开发小组中规定。。。。不要使用这种内存。