众所周知,踩内存是一个非常麻烦的问题,不管是在应用层或是内核层,关于踩内存的检测也有各种各样的工具,比如应用层的优秀开源valgrind,内核内置的kmemcheck等。关于这些工具的具体信息就不在这里做进一步描述了,本文主要简单介绍一下几种踩内存工具的工作原理。
一种最为简单的踩内存工具为通过给分配的内存前后多加上一小块内存,用于存放我们预先设定的值,比如Crc和Magic:
不管是应用程序(App)或是内核模块(Module),它们分配内存都是利用我们重载过的接口(malloc或kmalloc或new或其它等),这些接口分配比实际请求要大一点的内存,以便在前和后能空留出一个或几个字节存放检测值(也就是前面提到的Crc和Magic)。Magic可以是我们预先指定的特定值,比如0xC0,而Crc可以是某些条件(比如当前请求内存分配的进程的id、名、代码行等)的crc值。应用程序或是内核模块释放内存同样也是利用我们重载过的接口(free或kfree或delete或其它等),在这些接口里就可以做踩内存检测,如果发现Magic不再是我们预先指定的特定值,比如0xC0,则表示该块内存被踩了,而Crc呢?虽然没有固定的值,但也可以通过做同样的计算后做对比检测,或者我们还可以提前在分配内存时把这些值记录下来,比如可以设置一个hash表,把App id/name, Code line,Module id/name, Code line …和对应的Crc存起来,通过扫描Crc是否存在来做判断,也可以通过这个hash表做Crc反查到对应的进程和模块。
可以看到这种方法的缺点就是发现踩内存比较慢,要等在内存释放时才能检测获知。所以,可以提供一些proc接口以便随时通过命令显示内存当前使用情况。
另一种可以实时检测到踩内存的做法很巧妙,在codeproject上有详细的英文介绍,下面也再介绍一下。应用程序(App)或是内核模块(Module)对内存的分配释放接口仍然由我们重载,这无需多说,它的主要思路就是不管应用程序(App)或是内核模块(Module)申请多么小的内存,我们都直接给它分配2个页面(这里先讨论实际申请的内存块小于1页的情况),而这块实际内存块在这2个页面里怎么放呢?有两种放法:
第一种(左)是把实际请求内存块放在第一页的末尾,而把第二页设置为只读,此时如果往后越界必定踩到第二页,而由于该页为只读属性,于是立即被OS弹出异常。
第二种(右)是把实际请求内存块放在第二页的开始,而把第一页设置为只读,此时如果往前越界必定踩到第一页,而由于该页为只读属性,于是立即被OS弹出异常。
可以看到,这两种情况都只能检测前向和后向中的任何一种越界,而对于相应的另一种就无能为力。如果实际申请的内存块大于1页的时也是如此(如下图,大于1页,小于2页的情况,其它类似),除非实际申请的内存块恰好为内存页整数倍(包括其它额外开销所占的内存),也许可以在前后都设置只读页。但是,我们有办法解决这个问题,那就是提供参数可以用于指定是进行前向越界检测还是后向越界检测,而实际程序/模块踩内存检查时,同时跑两个相同内核模块(可能需要两台机器)或两个相同应用程序,一个进行前向越界检测,一个进行后向越界检测,因为只要越界踩内存必定被OS抓出来,所以这种办法也算是可行的。
前面提到的两种方法对越界踩内存的检测比较可行,但对于随机踩就看运气了,而且运气还要比较好才行。而顺带说一下的第三种做法就是延用上面第二种方法的思想,把OS里没用到的内存都设置为只读,提高这种运气的可能性,然后就是念经拜佛,希望踩内存的那行代码,就给我踩到只读内存页,被OS抓出来吧。