序
踩内存问题,大家都知道,是一个比较难分析的问题。
踩内存问题被发现,通常是程序崩溃的时候,能够生成coredump分析,知道是哪个内存被踩了,但通常是很难分析出是哪段代码出现了踩内存的问题。
本文会介绍几种分析踩内存问题的工具,有些工具是最近发现的,我还没有大量使用过,所以只是个简单的介绍,各位看官自行判断是否实用。
文章写的比较匆忙,内容可能会不太完整,请各位见谅。
gdb watch
发现踩内存问题后,我们可以通过gdb来起出现问题的应用程序,然后对出现问题的变量进行watch,每次这个变量被读取/修改时,程序都会暂停,我们就可以看一下是不是正常的修改。
这个网上资料比较多,就不详细介绍了。
这种方法的缺点: 每次修改都会停下来,如果这个变量经常被修改,那么基本是无法调试的。不知道能否通过gdb脚本来解决这个问题。
优点:很灵活,可以设置对很多变量的watch,当然设多了会占用比较多的cpu;暂停下来以后,可以看的信息很多
mprotect
mprotect可以修改内存的权限,可以将需要保护的内存设置为只读,然后每次访问这块内存,进程就会接收到一个异常信号(应该是SIGSEGV),然后可以对这个信号注册回调函数,进行你需要的处理,例如打印调用栈。
当然正常的访问是不应该进行处理的,所以在正常的访问前,先执行mprotect将内存改为可读写,访问结束后,再改为只读。
mprotect还有一个问题,它只能对一整个page进行设置,并不能指定到一个字节,所以回调里面还得判断访问的是否你需要监视的内存,如果不是,还得进行比较复杂的处理:1. 将这个page设置为可读写,否则这个修改就无法完成了,会一直触发SIGSEGV信号。2. 修改完成之后,还得重新设置为只读,这时候的设置比较麻烦了,应该已经回归到正常运行状态了。有一种处理方法,可以先在触发异常的代码的下一行将代码修改为INT3指令(类似gdb断点的原理),那么就会触发中断,在中断中再进行设置,设置完再将INT3修改回原来的代码。
还是比较麻烦的,不是很推荐使用。
asan
asan本身自带踩内存的检测,只要编译时带上asan,就能够进行一些踩内存的检测了。
使用asan进行检测会有一些踩内存问题无法检测到,主要是踩的内存不在asan加的保护内存中,而是踩的正常内存,此时就检测不出来。
如果出现这种问题,还可以采用asan提供的ASAN_POISON_MEMORY_REGION宏,对指定的内存进行保护
参考:https://github.com/google/sanitizers/wiki/AddressSanitizerManualPoisoning
同样的,使用poison时,在正常的访问前,需要先解除保护,也就是unpoison,访问完成后,再进行poison。
这样就要求对所有会访问监视内存的代码前后进行修改,否则正常访问也会报错了。
perf(hardware breakpoint)
gdb的watch,在数量比较少的时候,应该也是使用的hardware breakpoint。perf提供了可以手动设置hardware breakpoint的方法,针对指定的内存设置hardware breakpoint之后,对这个内存的读写就会触发中断,然后由指定的处理函数进行处理,如果不指定,则是默认输出到trace。
这个正好试用了一下,记录一下使用示例:
使用perf,在内存0x5594191c6018上设置了hardware breakpoint,采集每次对该内存的访问情况:
root/$ sudo perf record -e mem:0x5594191c6018 -p $(pgrep bcc_test)
^C[ perf record: Woken up 55 times to write data ]
[ perf record: Captured and wrote 14.168 MB perf.data (450225 samples) ]
perf script查看结果:
root/$ sudo perf script
bcc_test 12823 5633590.352853: 1 mem:0x5594191c6018: 5594191c31c1 _plus0+0x18 (/home/xuhaitao/test/bcc/build/bcc_test)
bcc_test 12823 5633590.352856: 1 mem:0x5594191c6018: 5594191c31cb _plus0+0x22 (/home/xuhaitao/test/bcc/build/bcc_test)
bcc_test 12823 5633590.352858: 1 mem:0x5594191c6018: 5594191c31c1 _plus0+0x18 (/home/xuhaitao/test/bcc/build/bcc_test)
bcc_test 12823 5633590.352859: 1 mem:0x5594191c6018: 5594191c31cb _plus0+0x22 (/home/xuhaitao/test/bcc/build/bcc_test)
bcc_test 12823 5633590.352860: 1 mem:0x5594191c6018: 5594191c31c1 _plus0+0x18 (/home/xuhaitao/test/bcc/build/bcc_test)
bcc_test 12823 5633590.352861: 1 mem:0x5594191c6018: 5594191c31cb _plus0+0x22 (/home/xuhaitao/test/bcc/build/bcc_test)
......
这样就能看到对内存0x5594191c6018进行访问和修改的位置了(_plus0+0x18和_plus0+0x22,也就是_plus0函数中的某个位置)。
这个使用是很灵活的,可以打印调用栈,还可以搭配ebpf进行使用,进行一些统计分析、数据过滤。
bcc也有人做过针对hardware breakpoint的开发,但没有入库,见:https://github.com/iovisor/bcc/pull/2456 希望以后能有成熟的bcc工具吧。
这种方法也有一些缺点:1. 你需要知道内存地址,在进程中是容易获取到的,但通过终端命令的方法设置的话,获取内存地址可能会有一些困难。 2. 可以设置的hardware point数量有限,一般x86是支持4个
除了终端命令的方式,也可以在代码中设置hardware point,参考系统提供的api:register_wide_hw_breakpoint