使用AddressSanitizer搭配addr2line查找C/C++内存泄漏问题

本文主要是介绍使用AddressSanitizer搭配addr2line查找C/C++内存泄漏问题,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

  • 前言
  • AddressSanitizer
  • 检测步骤
    • 泄漏发生在可执行程序本身
    • 泄漏发生在编译所需动态库中
    • 泄漏发生在自定义加载的动态库中
      • unknown module
      • maps
      • 具体操作
  • 总结

前言

指针是C/C++程序中的利器,同时也引入了风险,现代C++中增加了智能指针来降低使用“裸”指针带来的风险,但是智能指针不是一颗银弹,它不能解决所有的指针问题,内存泄漏在C/C++程序开发中依旧是值得注意的,学会合理、合适的方法来查找内存泄漏问题也是一项有用的技能。

通常内存泄漏问题会在开发到一定程度时集中检查,一些检测方法长时间不去使用难免会忘记,所以本文记录一种自己常用的检测方法,方便日后查阅。

AddressSanitizer

AddressSanitizer 是什么东西呢?从名字上直接翻译叫“地址消毒剂”,其实就是用来检查地址问题的。

它是一款地址问题检测工具,简称 ASAN,开源项目主地址为 google/sanitizers,是众多检测工具AddressSanitizer, MemorySanitizer, ThreadSanitizer, LeakSanitizer 中的一款,功能非常强大,可以检测出栈上缓冲区溢出、堆上缓冲区溢出、引用已释放内存、内存泄漏等多种地址问题。

今天想记录的是使用 AddressSanitizer 检测内存泄漏的步骤,其实检测内存泄漏的功能目前已经被基本独立成了 LeakSanitizer,不过仍可以通过在 AddressSanitizer 工具中通过参数来开启和关闭使用。

检测步骤

其实使用 ASAN 检测内存泄漏还是比较简单的,g++4.8 以上的版本自带了 ASAN 工具,只要编译时指定好参数,编译完成后正常启动运行程序就可以了,只不过有些情况下只从检测报告中无法准确定位问题,需要借助一些工具进一步缩小检测范围。

泄漏发生在可执行程序本身

这种情况检测起来比较容易,编写如下测试代码:

//test.cpp#include <iostream>void func()
{int* p = new int(); // 内存泄漏的位置p = nullptr;
}int main()
{func();std::cout << "test leak" << std::endl;return 0;
}

使用g++进行编译,编译时添加参数 -fsanitize=leak 就可以了,启动后可以清晰的展示出内存泄漏的位置 test.cpp:5,也就是 test.cpp 文件的第5行。

albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -g -o test --std=c++11 -fsanitize=leak
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test
test leak=================================================================
==344==ERROR: LeakSanitizer: detected memory leaksDirect leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7fc7d796d815 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xd815)#1 0x400967 in func() /mnt/d/data/cpp/testleak/test.cpp:5#2 0x400985 in main /mnt/d/data/cpp/testleak/test.cpp:11#3 0x7fc7d722083f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)SUMMARY: LeakSanitizer: 4 byte(s) leaked in 1 allocation(s).
albert@home-pc:/mnt/d/data/cpp/testleak$

这里有个小意外,将 int* p = new int(); 这句代码改成 int* p = new int[10]; 可以检测出内存泄漏如下:

albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -fsanitize=leak -g -o test --std=c++11
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test
test leak=================================================================
==416==ERROR: LeakSanitizer: detected memory leaksDirect leak of 400 byte(s) in 1 object(s) allocated from:#0 0x7fdc0c16d975 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xd975)#1 0x400967 in func() /mnt/d/data/cpp/testleak/test.cpp:5#2 0x40097f in main /mnt/d/data/cpp/testleak/test.cpp:11#3 0x7fdc0ba2083f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)SUMMARY: LeakSanitizer: 400 byte(s) leaked in 1 allocation(s).
albert@home-pc:/mnt/d/data/cpp/testleak$

但是将 int* p = new int(); 这句代码改成 int* p = new int[1024]; 就无法检测是内存泄漏了,只能修改编译选项为 -fsanitize=address 才能检测出泄漏,目前还不知道真正的原因是什么。

albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -fsanitize=address -g -o test --std=c++11
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test
test leak=================================================================
==432==ERROR: LeakSanitizer: detected memory leaksDirect leak of 4096 byte(s) in 1 object(s) allocated from:#0 0x7f1d42b296b2 in operator new[](unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.2+0x996b2)#1 0x400b27 in func() /mnt/d/data/cpp/testleak/test.cpp:5#2 0x400b3f in main /mnt/d/data/cpp/testleak/test.cpp:11#3 0x7f1d4235083f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)SUMMARY: AddressSanitizer: 4096 byte(s) leaked in 1 allocation(s).
albert@home-pc:/mnt/d/data/cpp/testleak$

泄漏发生在编译所需动态库中

如果内存泄漏发生在编译时使用的动态库中,那么这和上一种情况基本一致,可以直接编译后运行就能发现,测试代码如下

// myadd.hint add(int a, int b);
// myadd.cpp#include "myadd.h"int add(int a, int b)
{int* p = new int(); // 内存泄漏的位置return a + b;
}
// test.cpp#include "myadd.h"
#include <iostream>int main()
{std::cout << "519 + 1 = " << add(519, 1) << std::endl;return 0;
}

添加编译选项 -fsanitize=leak 编译后运行,也可以直接显示出内存泄漏的位置,内存泄漏在 libmyadd.so 动态库中的 add 函数中。

albert@home-pc:/mnt/d/data/cpp/testleak$ g++ -shared -fPIC -o libmyadd.so myadd.cpp
albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test3.cpp -L. -lmyadd -o test -Wl,-rpath=. -fsanitize=leak
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test
519 + 1 = 520=================================================================
==493==ERROR: LeakSanitizer: detected memory leaksDirect leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7ff1aff6d815 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xd815)#1 0x7ff1afd506b7 in add(int, int) (libmyadd.so+0x6b7)#2 0x4009cd in main (/mnt/d/data/cpp/testleak/test+0x4009cd)#3 0x7ff1af61083f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)SUMMARY: LeakSanitizer: 4 byte(s) leaked in 1 allocation(s).
albert@home-pc:/mnt/d/data/cpp/testleak$

泄漏发生在自定义加载的动态库中

这种情况要想精确定位问题就麻烦一些了,下面是用来测试的代码

// myadd.hextern "C" int add(int a, int b);
// myadd.cpp#include "myadd.h"extern "C" int add(int a, int b)
{int* p = new int(); // 内存泄漏的位置return a + b;
}
// test.cpp#include "myadd.h"
#include <dlfcn.h>
#include <iostream>typedef int (*FUNC)(int a,int b);int main() {void* handle = dlopen("./libmyadd.so", RTLD_LAZY);FUNC myadd = (FUNC)dlsym(handle,"add");int nVal = 0;std::cin >> nVal;std::cout << "519 + 1 = " << myadd(519, 1) << ", input:" << nVal << std::endl;dlclose(handle);return 0;
}

添加编译选项 -fsanitize=leak 编译后运行,输入数字618,程序运行结束,显示内存泄漏出现在 0x7fc88f0f06b7 (<unknown module>)

albert@home-pc:/mnt/d/data/cpp/testleak$ g++ -shared -fPIC -o libmyadd.so myadd.cpp -g
albert@home-pc:/mnt/d/data/cpp/testleak$ g++ test.cpp -ldl -o test -Wl,-rpath=. -g -fsanitize=leak
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test
618
519 + 1 = 520, input:618=================================================================
==817==ERROR: LeakSanitizer: detected memory leaksDirect leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7fc89076d815 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xd815)#1 0x7fc88f0f06b7  (<unknown module>)#2 0x400bc2 in main (/mnt/d/data/cpp/testleak/test+0x400bc2)#3 0x7fc88fe1083f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)SUMMARY: LeakSanitizer: 4 byte(s) leaked in 1 allocation(s).
albert@home-pc:/mnt/d/data/cpp/testleak$

unknown module

当使用 dlopen 的方式加载的动态库时,产生的内存泄漏常显示为 (<unknown module>),那是因为内存检测工具在程序退出时分析泄漏情况,而这时自定义加载的动态库往往已经手动调用 dlclose 关闭了,这时就会显示成 0x7fc88f0f06b7 (<unknown module>) 的显示。

maps

针对于出现 (<unknown module>) 的这种情况,可以通过查询 /proc/pid/maps 来辅助查询,maps 文件显示进程映射后的内存区域和访问权限,是程序正在运行时的信息,数据格式如下:

7f8c7adb6000-7f8c7adba000 rw-p 00000000 00:00 0
7f8c7adc0000-7f8c7af32000 r-xp 00000000 00:00 189413             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7f8c7af32000-7f8c7af3f000 ---p 00172000 00:00 189413             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7f8c7af3f000-7f8c7b132000 ---p 0017f000 00:00 189413             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7f8c7b132000-7f8c7b13c000 r--p 00172000 00:00 189413             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7f8c7b13c000-7f8c7b13e000 rw-p 0017c000 00:00 189413             /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.21
7f8c7b13e000-7f8c7b142000 rw-p 00000000 00:00 0
7f8c7b150000-7f8c7b153000 r-xp 00000000 00:00 243909             /lib/x86_64-linux-gnu/libdl-2.23.so
7f8c7b153000-7f8c7b154000 ---p 00003000 00:00 243909             /lib/x86_64-linux-gnu/libdl-2.23.so
7f8c7b154000-7f8c7b352000 ---p 00004000 00:00 243909             /lib/x86_64-linux-gnu/libdl-2.23.so
7f8c7b352000-7f8c7b353000 r--p 00002000 00:00 243909             /lib/x86_64-linux-gnu/libdl-2.23.so
7f8c7b353000-7f8c7b354000 rw-p 00003000 00:00 243909             /lib/x86_64-linux-gnu/libdl-2.23.so
7f8c7b360000-7f8c7b39f000 r-xp 00000000 00:00 247365             /usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0
7f8c7b39f000-7f8c7b3a2000 ---p 0003f000 00:00 247365             /usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0
7f8c7b3a2000-7f8c7b59e000 ---p 00042000 00:00 247365             /usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0
7f8c7b59e000-7f8c7b5a0000 r--p 0003e000 00:00 247365             /usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0
7f8c7b5a0000-7f8c7b5a1000 rw-p 00040000 00:00 247365             /usr/lib/x86_64-linux-gnu/liblsan.so.0.0.0
7f8c7b5a1000-7f8c7c1f4000 rw-p 00000000 00:00 0
7f8c7c200000-7f8c7c225000 r-xp 00000000 00:00 243945             /lib/x86_64-linux-gnu/ld-2.23.so
7f8c7c225000-7f8c7c226000 r-xp 00025000 00:00 243945             /lib/x86_64-linux-gnu/ld-2.23.so
  • 第一列:7f8c7b360000-7f8c7b39f000,表示本段内存映射的虚拟地址空间范围。
  • 第二列:r-xp,表示此段虚拟地址空间的属性。r表示可读,w表示可写,x表示可执行,ps共用一个字段,互斥关系,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 工具用来检测内存泄漏的工具所依赖的动态库。

具体操作

  1. 启动一个终端,然后运行 test 程序,因为程序中要求从控制台读取一个变量,所以运行后程序会一直停留在控制台等待输入
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test
  1. 重新打开一个终端,查询 test 程序的进程id,然后拷贝对应的 maps 文件
albert@home-pc:/mnt/d/data/cpp/testleak$ ps -ef | grep test
albert     986   889  0 21:42 pts/0    00:00:00 ./test
albert     988   953  0 21:42 pts/1    00:00:00 grep --color=auto test
albert@home-pc:/mnt/d/data/cpp/testleak$ cp /proc/986/maps testmaps
  1. 在第一个终端中输入数字,程序运行结束,显示出内存泄漏信息
albert@home-pc:/mnt/d/data/cpp/testleak$ ./test# 以下为新的信息,输入了515515
519 + 1 = 520, input:515=================================================================
==986==ERROR: LeakSanitizer: detected memory leaksDirect leak of 4 byte(s) in 1 object(s) allocated from:#0 0x7f8c7b36d815 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/liblsan.so.0+0xd815)#1 0x7f8c79cf06b7  (<unknown module>)#2 0x400bc2 in main (/mnt/d/data/cpp/testleak/test+0x400bc2)#3 0x7f8c7aa1083f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2083f)SUMMARY: LeakSanitizer: 4 byte(s) leaked in 1 allocation(s).
albert@home-pc:/mnt/d/data/cpp/testleak$
  1. 从检测报告中看到 0x7f8c79cf06b7 (<unknown module>),在备份的 testmaps 文件中查找范围,发现处于下面一段之中
640000000000-640000003000 rw-p 00000000 00:00 0
7f8c79cf0000-7f8c79cf1000 r-xp 00000000 00:00 282                /mnt/d/data/cpp/testleak/libmyadd.so
7f8c79cf1000-7f8c79cf2000 ---p 00001000 00:00 282                /mnt/d/data/cpp/testleak/libmyadd.so
7f8c79cf2000-7f8c79ef0000 ---p 00002000 00:00 282                /mnt/d/data/cpp/testleak/libmyadd.so
7f8c79ef0000-7f8c79ef1000 r--p 00000000 00:00 282                /mnt/d/data/cpp/testleak/libmyadd.so
7f8c79ef1000-7f8c79ef2000 rw-p 00001000 00:00 282                /mnt/d/data/cpp/testleak/libmyadd.so
7f8c79f00000-7f8c7a000000 rw-p 00000000 00:00 0
  1. 至此发现问题出现在 libmyadd.so 这个动态库中,再用 0x7f8c79cf06b7 减去动态链接库基地址 7f8c79cf0000,得到偏移量为 0x6b7,此时使用 addr2line 工具进行转化。
albert@home-pc:/mnt/d/data/cpp/testleak$ addr2line  -C -f -e /mnt/d/data/cpp/testleak/libmyadd.so 0x6b7
add
/mnt/d/data/cpp/testleak/myadd.cpp:4
  1. 至此就找到了内存泄漏的确切位置,在/mnt/d/data/cpp/testleak/myadd.cpp文件第4行的 add 函数之中。

总结

  • 在 C++11 之后尽可能使用智能指针来管理在堆上申请的内存,shared_ptrweak_ptrunique_ptr 能帮我们减少许多麻烦
  • 想要检测程序内存用用问题, AddressSanitizer 是一个不错的选择,其中有关内存泄漏的检测已经被整合到 LeakSanitizer 工具中
  • 当程序中的内存泄漏发生在 dlopen 加载的动态库中时,常常出现 (<unknown module>) 的情况,这时需要借助 proc/pid/maps 文件和 addr2line 工具来完成精确定位。

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

祝融落地。一百年了,还没有什么事情是做不到的,我们需要的是时间,我等着看你们在真正的力量面前瑟瑟发抖~

这篇关于使用AddressSanitizer搭配addr2line查找C/C++内存泄漏问题的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/451441

相关文章

中文分词jieba库的使用与实景应用(一)

知识星球:https://articles.zsxq.com/id_fxvgc803qmr2.html 目录 一.定义: 精确模式(默认模式): 全模式: 搜索引擎模式: paddle 模式(基于深度学习的分词模式): 二 自定义词典 三.文本解析   调整词出现的频率 四. 关键词提取 A. 基于TF-IDF算法的关键词提取 B. 基于TextRank算法的关键词提取

使用SecondaryNameNode恢复NameNode的数据

1)需求: NameNode进程挂了并且存储的数据也丢失了,如何恢复NameNode 此种方式恢复的数据可能存在小部分数据的丢失。 2)故障模拟 (1)kill -9 NameNode进程 [lytfly@hadoop102 current]$ kill -9 19886 (2)删除NameNode存储的数据(/opt/module/hadoop-3.1.4/data/tmp/dfs/na

NameNode内存生产配置

Hadoop2.x 系列,配置 NameNode 内存 NameNode 内存默认 2000m ,如果服务器内存 4G , NameNode 内存可以配置 3g 。在 hadoop-env.sh 文件中配置如下。 HADOOP_NAMENODE_OPTS=-Xmx3072m Hadoop3.x 系列,配置 Nam

Hadoop数据压缩使用介绍

一、压缩原则 (1)运算密集型的Job,少用压缩 (2)IO密集型的Job,多用压缩 二、压缩算法比较 三、压缩位置选择 四、压缩参数配置 1)为了支持多种压缩/解压缩算法,Hadoop引入了编码/解码器 2)要在Hadoop中启用压缩,可以配置如下参数

Makefile简明使用教程

文章目录 规则makefile文件的基本语法:加在命令前的特殊符号:.PHONY伪目标: Makefilev1 直观写法v2 加上中间过程v3 伪目标v4 变量 make 选项-f-n-C Make 是一种流行的构建工具,常用于将源代码转换成可执行文件或者其他形式的输出文件(如库文件、文档等)。Make 可以自动化地执行编译、链接等一系列操作。 规则 makefile文件

好题——hdu2522(小数问题:求1/n的第一个循环节)

好喜欢这题,第一次做小数问题,一开始真心没思路,然后参考了网上的一些资料。 知识点***********************************无限不循环小数即无理数,不能写作两整数之比*****************************(一开始没想到,小学没学好) 此题1/n肯定是一个有限循环小数,了解这些后就能做此题了。 按照除法的机制,用一个函数表示出来就可以了,代码如下

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

使用opencv优化图片(画面变清晰)

文章目录 需求影响照片清晰度的因素 实现降噪测试代码 锐化空间锐化Unsharp Masking频率域锐化对比测试 对比度增强常用算法对比测试 需求 对图像进行优化,使其看起来更清晰,同时保持尺寸不变,通常涉及到图像处理技术如锐化、降噪、对比度增强等 影响照片清晰度的因素 影响照片清晰度的因素有很多,主要可以从以下几个方面来分析 1. 拍摄设备 相机传感器:相机传

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数