前言
指针是C/C++程序中的利器,同时也引入了风险,现代C++中增加了智能指针来降低使用“裸”指针带来的风险,但是智能指针不是一颗银弹,它不能解决所有的指针问题,内存泄漏在C/C++程序开发中依旧是值得注意的,学会合理、合适的方法来查找内存泄漏问题也是一项有用的技能。
通常内存泄漏问题会在开发到一定程度时集中检查,一些检测方法长时间不去使用难免会忘记,所以本文记录一种自己常用的检测方法,方便日后查阅。
AddressSanitizer
AddressSanitizer 是什么东西呢?从名字上直接翻译叫“地址消毒剂”,其实就是用来检查地址问题的。
它是一款地址问题检测工具,简称 ASAN,开源项目主地址为 google/sanitizers,是众多检测工具AddressSanitizer, MemorySanitizer, ThreadSanitizer, LeakSanitizer 中的一款,功能非常强大,可以检测出栈上缓冲区溢出、堆上缓冲区溢出、引用已释放内存、内存泄漏等多种地址问题。
今天想记录的是使用 AddressSanitizer 检测内存泄漏的步骤,其实检测内存泄漏的功能目前已经被基本独立成了 LeakSanitizer,不过仍可以通过在 AddressSanitizer 工具中通过参数来开启和关闭使用。
检测步骤
其实使用 ASAN 检测内存泄漏还是比较简单的,g++4.8 以上的版本自带了 ASAN 工具,只要编译时指定好参数,编译完成后正常启动运行程序就可以了,只不过有些情况下只从检测报告中无法准确定位问题,需要借助一些工具进一步缩小检测范围。
泄漏发生在可执行程序本身
这种情况检测起来比较容易,编写如下测试代码:
1  | //test.cpp  | 
使用g++进行编译,编译时添加参数 -fsanitize=leak 就可以了,启动后可以清晰的展示出内存泄漏的位置 test.cpp:5,也就是 test.cpp 文件的第5行。
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -g -o test --std=c++11 -fsanitize=leak  | 
这里有个小意外,将 int* p = new int(); 这句代码改成 int* p = new int[10]; 可以检测出内存泄漏如下:
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -fsanitize=leak -g -o test --std=c++11  | 
但是将 int* p = new int(); 这句代码改成 int* p = new int[1024]; 就无法检测是内存泄漏了,只能修改编译选项为 -fsanitize=address 才能检测出泄漏,目前还不知道真正的原因是什么。
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -fsanitize=address -g -o test --std=c++11  | 
泄漏发生在编译所需动态库中
如果内存泄漏发生在编译时使用的动态库中,那么这和上一种情况基本一致,可以直接编译后运行就能发现,测试代码如下
1  | // myadd.h  | 
1  | // myadd.cpp  | 
1  | // test.cpp  | 
添加编译选项 -fsanitize=leak 编译后运行,也可以直接显示出内存泄漏的位置,内存泄漏在 libmyadd.so 动态库中的 add 函数中。
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ g++ -shared -fPIC -o libmyadd.so myadd.cpp  | 
泄漏发生在自定义加载的动态库中
这种情况要想精确定位问题就麻烦一些了,下面是用来测试的代码
1  | // myadd.h  | 
1  | // myadd.cpp  | 
1  | // test.cpp  | 
添加编译选项 -fsanitize=leak 编译后运行,输入数字618,程序运行结束,显示内存泄漏出现在 0x7fc88f0f06b7  (<unknown module>)
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ g++ -shared -fPIC -o libmyadd.so myadd.cpp -g  | 
unknown module
当使用 dlopen 的方式加载的动态库时,产生的内存泄漏常显示为 (<unknown module>),那是因为内存检测工具在程序退出时分析泄漏情况,而这时自定义加载的动态库往往已经手动调用 dlclose 关闭了,这时就会显示成 0x7fc88f0f06b7  (<unknown module>) 的显示。
maps
针对于出现 (<unknown module>) 的这种情况,可以通过查询 /proc/pid/maps 来辅助查询,maps 文件显示进程映射后的内存区域和访问权限,是程序正在运行时的信息,数据格式如下:
1  | 7f8c7adb6000-7f8c7adba000 rw-p 00000000 00:00 0  | 
- 第一列:7f8c7b360000-7f8c7b39f000,表示本段内存映射的虚拟地址空间范围。
 - 第二列:r-xp,表示此段虚拟地址空间的属性。
r表示可读,w表示可写,x表示可执行,p和s共用一个字段,互斥关系,p表示私有段,s表示共享段,-表示没有权限。 - 第三列:00000000,表示映射偏移。对有名映射,表示此段虚拟内存起始地址在文件中以页为单位的偏移。对匿名映射,它等于0或者vm_start/PAGE_SIZE。
 - 第四列:00:00,表示映射文件所属设备号。对有名映射来说,是映射的文件所在设备的设备号,对匿名映射来说,因为没有文件在磁盘上,所以没有设备号,始终为00:00。
 - 第五列:247365,表示映射文件所属节点号。对有名映射来说,是映射的文件的节点号。对匿名映射来说,因为没有文件在磁盘上,所以没有节点号,始终为0。
第六列:/usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0,表示映射文件名或堆、栈。对匿名映射来说,是此段虚拟内存在进程中的角色。[stack]表示在进程中作为栈使用,[heap]表示堆。对有名来说,是映射的文件名。其余情况则无显示。 
7f8c7b360000-7f8c7b39f000 r-xp 00000000 00:00 247365 /usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0
这一行就展示了 liblsan.so 这个动态库映射的内存中位置和权限情况,liblsan.so 也就是 ASAN 工具用来检测内存泄漏的工具所依赖的动态库。
具体操作
- 启动一个终端,然后运行 test 程序,因为程序中要求从控制台读取一个变量,所以运行后程序会一直停留在控制台等待输入
 
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ ./test  | 
- 重新打开一个终端,查询 test 程序的进程id,然后拷贝对应的 maps 文件
 
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ ps -ef | grep test  | 
- 在第一个终端中输入数字,程序运行结束,显示出内存泄漏信息
 
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ ./test  | 
- 从检测报告中看到 
0x7f8c79cf06b7 (<unknown module>),在备份的 testmaps 文件中查找范围,发现处于下面一段之中 
1  | 640000000000-640000003000 rw-p 00000000 00:00 0  | 
- 至此发现问题出现在 
libmyadd.so这个动态库中,再用0x7f8c79cf06b7减去动态链接库基地址7f8c79cf0000,得到偏移量为0x6b7,此时使用 addr2line 工具进行转化。 
1  | albert@home-pc:/mnt/d/data/cpp/testleak$ addr2line -C -f -e /mnt/d/data/cpp/testleak/libmyadd.so 0x6b7  | 
- 至此就找到了内存泄漏的确切位置,在/mnt/d/data/cpp/testleak/myadd.cpp文件第4行的 
add函数之中。 
总结
- 在 C++11 之后尽可能使用智能指针来管理在堆上申请的内存,
shared_ptr、weak_ptr、unique_ptr能帮我们减少许多麻烦 - 想要检测程序内存用用问题, AddressSanitizer 是一个不错的选择,其中有关内存泄漏的检测已经被整合到 LeakSanitizer 工具中
 - 当程序中的内存泄漏发生在 
dlopen加载的动态库中时,常常出现(<unknown module>)的情况,这时需要借助proc/pid/maps文件和 addr2line 工具来完成精确定位。 
祝融落地。一百年了,还没有什么事情是做不到的,我们需要的是时间,我等着看你们在真正的力量面前瑟瑟发抖~
2021-5-16 00:08:24