C++ 内存分析工具
背景
内存泄漏在c++中是一个常见的问题,有一个好用的内存分析工具就是很有必要的,下面来介绍两个简单好用的内存分析工具,Valgrind和Sanitizer;
Valgrind
valgrind是一种非侵入式的内存检测工具,在进行检查程序前不需要重新编译和连接;Memcheck会在错误发生时立即报告这些错误,给出发生错误的源行号,以及为到达该行而调用的函数的堆栈跟踪。Memcheck在字节级跟踪可寻址性,在位级跟踪值的初始化。因此,它可以检测单个未初始化位的使用,并且不会在位字段操作中报告虚假错误。Memcheck运行程序的速度比正常慢10-30倍。
工具的安装可以参考:Valgrind: Current Releases
主要包含一下集中标准的工具:
1. memcheck:内存错误检测器,检测内存问题,如内存泄漏,数组访问越界,非法指针等
2. callgrind:调用图缓存生成器,检测程序运行的时间和调用过程,用于分析性能,和Linux的perf工具类似
3. cachegrind:缓存和分支预测分析器;分析CPU缓存的命中率
4. heigrind:用于检测多线程中资源竞争问题;
5. massif:堆栈分析器,检测堆栈的hi使用使用情况;
主要介绍一下memcheck工具的的使用
原理:
通过维护两张表(valid-value和valid-address)来实现对运行程序的内存检查;
- valid-value:记录整个进程地址空间的每个字节(每个字节对应8个bits位);CPU的每个寄存器同样有与之对应的bit向量;这些bit负责记录对应的字节或者寄存器中的值是否有效,是否已初始化;
- valid-address:对整个进程地址空间的每个字节有一个与之对应的1bit,负责记该地址空间是否能够被读写;
- 当要内存读写某个字节时,首先会检查valid-address表中的字节对应的那1bit值,判断字节所在地址时候有效,若发现时无效地址,memcheck会输出读写错误的报告;当内核要加载某个字节时,该字节对应在valid-value中的值也会加载到CPU内核中,当内核寄存器中的值被用来生产内存地址或者能影响程序的输出时,memcheck会检查该字节对应valid-value表中的值是否有效或者是否初始化,并给出对应的检测结果;
使用:
首先编译源代码时要加上 -g参数,保留调试信息,然后运行下面命令
valgrind --tool=memcheck --leak-check=full --show-reachable=yes ./demo
- –tool=memcheck:表示使用memcheck工具
- –leak-check=full:表示显示每个泄漏的细节
- –log-file=val_report:可以增加该参数,将分析结果生成日志,放到当前目录下的val_report文件中便于后续分析
例1 内存泄漏
#include <iostream>
using namespace std;
int main()
{
int *p = new int(10);
cout << "*p:" << *p << endl;
//delete p;
return 0;
}
==9543== Memcheck, a memory error detector
==9543== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==9543== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info
==9543== Command: ./demo
==9543== Parent PID: 8780
==9543==
==9543==
==9543== HEAP SUMMARY:
==9543== in use at exit: 4 bytes in 1 blocks
==9543== total heap usage: 3 allocs, 2 frees, 73,732 bytes allocated
==9543==
==9543== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==9543== at 0x4C2F87B: operator new(unsigned long) (vg_replace_malloc.c:434)
==9543== by 0x4008E8: main (demo.cpp:5)
==9543==
==9543== LEAK SUMMARY:
==9543== definitely lost: 4 bytes in 1 blocks
==9543== indirectly lost: 0 bytes in 0 blocks
==9543== possibly lost: 0 bytes in 0 blocks
==9543== still reachable: 0 bytes in 0 blocks
==9543== suppressed: 0 bytes in 0 blocks
==9543==
==9543== For lists of detected and suppressed errors, rerun with: -s
==9543== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
在输出的检测分析结果中,第1行已经指出存在一个内存检测错误;第8行给出具体的内存检测结果,共有4个字节的内存出现了泄漏;在demo.cpp文件的第5行出现该内存泄漏;第16行是将本次内存泄漏的所有情况进行汇总;主要专注前4类,具体含义如下:
- definitely lost:确认存在内存泄漏;当程序结束时,存在动态申请的内存没有释放,且通过其他指针都无法正常访问该内存;
- indirectly lost : 间接存在内存泄漏;当使用含有指针成员变量的类时可能会报该错误,一般都会与definitely lost同时存在,解决definitely lost问题,该问题随之消失;
- possibly lost :可能存在内存泄漏;当程序结束时,存在动态申请的内存内有释放,且通过指针变量无法访问该内存的起始地址,但可以访问该内存中的后一部分数据;
- still reachable:存在可以访问,未丢失但也未释放;若程序正常结束,虽然不会造成程序崩溃,但长时间的运行会消耗是系统内存资源,建议修复;若非正常结束,可忽略;
例2 使用已经释放的内存
#include <iostream>
using namespace std;
int main()
{
int *p = new int(10);
cout << "*p:" << *p << endl;
delete p;
int a = *p;
return 0;
}
==10129== Memcheck, a memory error detector
==10129== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==10129== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info
==10129== Command: ./demo
==10129==
*p:10
==10129== Invalid read of size 4
==10129== at 0x40097F: main (demo.cpp:8)
==10129== Address 0x5b1fc80 is 0 bytes inside a block of size 4 free'd
==10129== at 0x4C31C86: operator delete(void*) (vg_replace_malloc.c:935)
==10129== by 0x40097A: main (demo.cpp:7)
==10129== Block was alloc'd at
==10129== at 0x4C2F87B: operator new(unsigned long) (vg_replace_malloc.c:434)
==10129== by 0x400938: main (demo.cpp:5)
==10129==
==10129==
==10129== HEAP SUMMARY:
==10129== in use at exit: 0 bytes in 0 blocks
==10129== total heap usage: 3 allocs, 3 frees, 73,732 bytes allocated
==10129==
==10129== All heap blocks were freed -- no leaks are possible
==10129==
==10129== For lists of detected and suppressed errors, rerun with: -s
==10129== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
结果分析的第7行指出,存在无效的读操作,发生在demo.cpp的第8行;该内存空间是在demo.cpp的第5行进行分配空间,在demo.cpp的第7行进行空间释放,在8行又去访问该内存;
例3 数组访问越界
#include <iostream>
#include <stdlib.h>
using namespace std;
int main()
{
int *p1 = (int*)malloc(sizeof(int)*4);
p1[4] = 10;
free(p1);
int *p2 = new int[4]{1,2,3,4};
int c = p2[50];
delete []p2;
return 0;
}
==12822== Memcheck, a memory error detector
==12822== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==12822== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info
==12822== Command: ./demo
==12822==
==12822== Invalid write of size 4
==12822== at 0x4007F7: main (demo.cpp:7)
==12822== Address 0x5b1fc90 is 0 bytes after a block of size 16 alloc'd
==12822== at 0x4C2F15A: malloc (vg_replace_malloc.c:393)
==12822== by 0x4007EA: main (demo.cpp:6)
==12822==
==12822== Invalid read of size 4
==12822== at 0x400845: main (demo.cpp:10)
==12822== Address 0x5b1fd98 is 120 bytes inside an unallocated block of size 4,121,280 in arena "client"
==12822==
==12822==
==12822== HEAP SUMMARY:
==12822== in use at exit: 0 bytes in 0 blocks
==12822== total heap usage: 3 allocs, 3 frees, 72,736 bytes allocated
==12822==
==12822== All heap blocks were freed -- no leaks are possible
==12822==
==12822== For lists of detected and suppressed errors, rerun with: -s
==12822== ERROR SUMMARY: 2 errors from 2 contexts (suppressed: 0 from 0)
检测结果第6行和第12行指出,出现无效的写和无效的读;在demo.cpp的第6行存在数组越界访问,无效的写操作;在demo.cpp的第10行出现数组访问越界,无效的读操作;
只能检测动态数组访问越界,静态数组无法检测;
例4 多次内存释放
#include <iostream>
using namespace std;
int main()
{
int *p2 = new int[4]{1,2,3,4};
delete []p2;
delete []p2;
return 0;
}
==13064== Memcheck, a memory error detector
==13064== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==13064== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info
==13064== Command: ./demo
==13064==
==13064== Invalid free() / delete / delete[] / realloc()
==13064== at 0x4C32BC2: operator delete[](void*) (vg_replace_malloc.c:1115)
==13064== by 0x4007AB: main (demo.cpp:7)
==13064== Address 0x5b1fc80 is 0 bytes inside a block of size 16 free'd
==13064== at 0x4C32BC2: operator delete[](void*) (vg_replace_malloc.c:1115)
==13064== by 0x400798: main (demo.cpp:6)
==13064== Block was alloc'd at
==13064== at 0x4C30989: operator new[](unsigned long) (vg_replace_malloc.c:652)
==13064== by 0x400757: main (demo.cpp:5
==13064==
==13064==
==13064== HEAP SUMMARY:
==13064== in use at exit: 0 bytes in 0 blocks
==13064== total heap usage: 2 allocs, 3 frees, 72,720 bytes allocated
==13064==
==13064== All heap blocks were freed -- no leaks are possible
==13064==
==13064== For lists of detected and suppressed errors, rerun with: -s
==13064== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
检测结果第6行指出,出现无效的内存释放操作;该内存在demo.cpp文件的第5行申请,demo.cpp的第6行释放,demo.cpp在第7行进行二次释放,错误操作;在结果的第19行也指出,2个内存块申请,3分内存块释放操作,存在错误的内存释放操作;
例5 申请和释放操作不匹配
#include <iostream>
#include <stdlib.h>
int main()
{
int *p1 = (int*)malloc(sizeof(int)*4);
delete p1;
return 0;
}
==13194== Memcheck, a memory error detector
==13194== Copyright (C) 2002-2022, and GNU GPL'd, by Julian Seward et al.
==13194== Using Valgrind-3.20.0 and LibVEX; rerun with -h for copyright info
==13194== Command: ./demo
==13194==
==13194== Mismatched free() / delete / delete []
==13194== at 0x4C31C86: operator delete(void*) (vg_replace_malloc.c:935)
==13194== by 0x40076A: main (demo.cpp:6)
==13194== Address 0x5b1fc80 is 0 bytes inside a block of size 16 alloc'd
==13194== at 0x4C2F15A: malloc (vg_replace_malloc.c:393)
==13194== by 0x40075A: main (demo.cpp:5)
==13194==
==13194==
==13194== HEAP SUMMARY:
==13194== in use at exit: 0 bytes in 0 blocks
==13194== total heap usage: 2 allocs, 2 frees, 72,720 bytes allocated
==13194==
==13194== All heap blocks were freed -- no leaks are possible
==13194==
==13194== For lists of detected and suppressed errors, rerun with: -s
==13194== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
检测结果第6行指出,存在内存申请和释放的操作不匹配;在demo.cpp文件的第6行使用了malloc申请内存,在demo.cpp文件的第6行使用了delete操作,内存申请和释放的操作不匹配;
具体使用手册参考:Valgrind
Sanitizer
简介
Address Aanitizer是一个快速检测内存错误的工具,它只将程序运行的速度拖慢了两倍,相比valgrand的10~20倍来说性能已经非常好了;他主要包含一个编译器instrumentation模块和一个替换malloc()/free()函数的运行时动态库;
GCC从4.8开始,AddressSanitizer就成为其一部分,一般建使用gcc4.9以上的版本;
原理
ASAN(AddressSanitizer)运行时,将malloc()/free() 进行了替换;malloc()分配缓存前后的空间以及被free()释放的空间都标记位poisoned,替换情况如下:
//替换前
*address = ...;
//替换后
if (IsPoisoned(addrss))
{
ReportError(address, kAccessSize, kIsWrite);
}
//访问之前检查地址是否可以访问
*address = ...;
使用步骤
- 编译时需要添加 -fsanitize=address 编译选项
- 添加 -fno-omit-frame-pointer 编译选项,可得到更详细的stack trace
- 选择 -O1 或者更高级别的编译优化选项
gcc -fsanitize=address -fno-omit-frame-pointer -O1 -g demo.cpp -o demo
错误类型
-
堆(heap)use after free 释放后使用
int main () { int* p = new int(10); delete p; return *p; }
-
heap buffer overflow 堆缓存访问溢出
int main() { int* arr[10] = new int[10]; int a = arr[10]; return 0; }
-
stack buffer overflow 栈缓存访问益处
int main() { int arr[10] = {10}; int a = arr[10]; return 0; }
-
global buffer overflow 全局缓存访问益处
int arr[10] = {10}; int main() { int a = arr[10]; return 0; }
-
memory leaks 内存泄漏
int main() { int* a = new int(10); return 0; }