C++ 内存问题排查原理与工具

在日常编码过程中,或许大家的项目跑起来没事,但如果你没对项目做过内存检测的话,事实上项目可能早已遍布各种内存问题。为此在项目中进行内存检测就十分有必要。

一般我们会借助一些内存工具来进行内存问题排查,在Windows 下,比较常用的工具有GFlags、Application Verifier、AddressSanitizer、VLD等,而Linux下主要是Valgrind、AddressSanitizer 等。

上面这些都是比较知名和常见的动态内存排查工具,也就是针对运行时的内存问题进行排查,另外还有静态排查工具 。所谓静态排查,其实就是利用代码扫描器对代码进行扫描,如Cppcheck 和 TscanCode 。这些代码扫描工具除了可以在一定程度上帮我们排查内存问题,还能指正部分代码质量问题,对代码质量提升有一定的帮助。

本章会对上面的工具进行一定程度的介绍,但在开始介绍之前,我们应该清楚常规要关注的内存问题有哪些,我们自己编写检测代码进行排查的话,又应该如何去做。因为工具虽多,但其实核心原理都是一样的,当我们理解检测原理的本质后,就能够知晓我们需要利用这些工具的那一项功能。以及分析排查结果是否准确。

检测原理

所以在介绍工具之前,我们需要先学习内存的检测原理。一般情况下,常规关注的内存问题有:内存泄露、内存越界、双重释放、野指针访问

下面进行内存检测概述,更详细的内存原理和内存检测介绍在这里:(迟点补上)

首先内存检测,一般是配合Hook API技术完成的,所谓Hook API,本质就是交换函数地址,假设有如下调用 (伪代码):

bool HOOKAPI(
	void** pRealFunction, // 返回原函数的真实地址
	void* pHookFunction, // 用于hook的目标函数
	void* pSrcFunction // 原函数地址
	);

void* real_malloc = NULL; // 全局变量,真实的的malloc地址

void* my_malloc(size_t size){
	// 强调一下,real_malooc 不一定会被调用,因为有些工具会自己实现堆分配管理
	// 强调一下,real_malooc 不一定会被调用,因为有些工具会自己实现堆分配管理
	// 强调一下,real_malooc 不一定会被调用,因为有些工具会自己实现堆分配管理
	void* p = real_malloc(size);// 调用真正的malloc函数
	//进行监听处理,做一些额外的工作
	return p; // 返回分配得到的内存
}

int main(){
	HOOKAPI(&real_malloc, my_malloc, malloc);
	void* p = malloc(100); //其实这里被调用的是 my_malloc
}

当然 HOOKAPI 这一步很多时候会被隐藏起来调用,所以大家不知道你的API已经被调包了,这一点大家知道就好,至于怎么Hook API 怎么实现,不在本章的介绍范围内,有兴趣的可以自行了解。
而内存检测的本质,核心就在于对 malloc 和 free 函数的监听处理,做了一些额外的工作完成的,至于为什么没有对new 和 delete 指令进行监听,原因很简单,因为new和delete的底层也是通过调用 malloc/free 函数完成的。
我们接下来介绍一下,对不同的内存问题,内存检测工具都做了哪些动作。

内存泄露:
内存泄露的原因很简单,就是说你 malloc/new 一个指针,当指针使用完成之后,没有进行 free/delete 调用。
我们知道,一般情况下,malloc和free是需要成对出现的。如下:

int main(){
	void* p malloc(100);
	// 对p 进行一些操作
	free(p);
	return 0;
}

如果没有实现 free 动作,或者 free 动作没有被执行,那么就会造成内存泄露。那么检测器如何进行检测的呢?

大家思考一下,如果我们有一个链表(或者二叉树),在监听到 malloc 动作时,以 real_malooc 返回的指针 p 为节点,插入链表中,当 free p 时,从链表中找出 p 的位置,并删除掉。那么当 main 函数结束时,如果链表中的节点不为空,就证明产生了内存泄露。我们还可以将这些节点用 printf 函数逐个打印出来,方便开发者进行内存检测。
伪代码如下:

int __startup(){ // 这个才是进程真正开始的第一个函数
	// 进行进程环境初始化
	memory_listen();// 进行内存监听
	main(); // 这个才是我们平时实现的 main函数
	memory_shutdown(); // 关闭内存监听
	memory_print(); // 打印泄露的内存节点。
	// 进行进程环境回收
}

内存检测函数,为什么会出现在main 之前和之后被调用,一般是利用C++的全局变量 RAII 特性,当然还有其他办法,这里不作详细介绍,需要大家自行学习C++机制和系统机制。而有些工具,是需要大家手动调用代码的,比如VS自带的 内存检测,大家可以在 main 函数中手动调用这些函数:

int main(){
	 todo(); // 进行你的业务代码处理
	_CrtDumpMemoryLeaks(); // 打印泄露的内存节点。
}

一般情况下,内存检测工具,会在malloc函数的监听中,除了real_malloc得到的地址外,会额外记录下你分配函数的函数调用堆栈地址,然后在打印内存节点的时候,先连接你的符号文件,找出堆栈地址的相关信息,然后再将链表中的节点信息逐一打印出来,方便开发者查找问题。
windows下符号文件的后缀是 .pdb, linux 下符号文件可能叫 .debug 或者无后缀。一般情况下调试器都是能够自动链接上符号文件的,如果打印时缺乏详细的堆栈信息,那么说明找不到符号文件。

到目前为止,内存泄露的检测方式,已经大致跟大家介绍完了。大家只要根据调试器或排查工具输出的信息,进行逐一排查即可。但这里还有一个问题,就是当我们打印出来的链表节点为空,那么是不是证明我们的程序中已经没有内存泄露了呢?

即使打印出来的列表为空,其实也不能完全证明我们的没有内存泄漏,原因主要有两方面。一是可能有些场景你还没测试到,所以需要完善你的测试用例,或者在线上环境上报内存日志,再次观察是否产生了内存泄漏。
第二个原因是一个更值得关注的点,那就是监听的时机。假设我们是在进入main函数后开始的监听,main函数准备退出时,打印出日志,那么就意味着一个问题,就是当一个malloc动作是在main函数之前执行的,那么这个动作将无法被监控到,这也表示链表不会为这次内存分配新增一个节点。所以即使最后没有进行内存释放,这次的泄漏信息也不会被打印出来。

为此我们需要了解一下 main 的前后执行了那些动作,以此了解那些地方有可能没有被我们所监听到。
请看下面的伪代码:

int __startup(){ // 这个才是进程真正开始的第一个函数
	// 进行进程环境初始化
	init_c_evn(); // 初始化C风格的全局变量类型
	init_cplus_evn(); // 初始化C++风格的全局变量类型,就是 class 
	for(;;){ // 加载ntdll以外的动态库
		load_lib(){
			init_lib_c_evn(); // 这是在 load_lib 内部进行的
			init_lib_cplus_evn(); // 这是在 load_lib 内部进行的
		}
	}
	main(); // 这个才是我们平时实现的 main函数
	// 进行进程环境回收
	for(;;){// 卸载ntdll以外的动态库
		free_lib(){
			uninit_lib_cplus_evn(); // 这是在 free_lib内部进行的
		}
	}
	uninit_cplus_evn(); // 反初始化 C++全局变量,就是析构全局变量的class
	// C风格的全局变量,是不需要反初始化的
}

很多的工具,都是利用 C++全局变量或者动态库进行加载的,如果利用的是C++全局变量,那么一些C风格的全局变量进行了内存分配,就有可能没被监听到,如果利用的是动态库,则C++的全局变量就有可能没有被监听到。另外还有一种注入式的内存监听,它跟ntdll一样,会在__startup函数调用前注入,所以能够监听ntdll以外的所有内存泄漏。

但不管何种办法,很明显的一点就是,在main函数中的内存分配,基本都会被监听到。所以main函数内部的内存泄漏,基本都是会被打印出来。 再一个问题是,如果我们的打印函数是在main函数结束时调用的话,由于一些C++全局变量和动态库的反初始化还没有被执行,所以即使你在反初始化动作中已经加入了free动作,由于还来不及调用,所以检测工具照样会将他们当成内存泄漏打印出来。因为打印函数的调用在反初始化前面。

所以大家要注意的一点是:内存泄漏问题,由于打印时机等各种原因,工具是有可能存在误报的,所以不管最后你的检测工具,有没有打印出内存泄漏信息,开发者都要根据具体情况进行具体分析。

内存越界:
所谓内存越界,就是对内存的读写访问,超出了它的分配范围,代码如下:

int main(){
	char* p= (char*)malloc(10);
	p[-1] = 'a'; // 向前越界
	p[10] = 'b'; // 向后越界
	free(p);
	return 0;
}

内存越界的可怕之处,在于它执行错误的内存操作后,往往进程需要再跑一段时间之后,才会发生崩溃。也就是说很多时候内存越界引发的崩溃,所看到的堆栈,往往不是第一案发现场。所以越界检查要做的,一般是在越界时第一时间触发异常,这样就能够为开发者提供内存越界的第一案发现场。

目前来说,内存越界的检测方法有三种:
一是设置内存区域权限,比如用户需要分配10字节的内存,在malloc监听中,实际给用户分配 18个字节,然后1-4 和14-18 这一头一尾的内存位置,设置上不可访问权限。而给用户返回的指针,当然是 p + 4 的位置。所以一旦用户像上面那样调用-1 和 10的下标访问,进程马上就会报出异常。
上述方案目前是最好的,毕竟能够拿到第一案发现场,不过这种方法,对兼容性和内存开销都有一定要求,所以很多工具并不能很好地支持这种方案。

只要你理解了第一种方案,第二和第三方案也很好理解,第二种方案是,在内存块前面设置不可访问属性,即往前越界检查,但后面不设置,然后你访问 p[-1] 的时候,马上就会触发异常。第三种方案是给内存尾部设置一个值,比如 0xFF 0xFF 0xFF 0xFF,占据4个位置。比如执行上面的 p[10] = ‘b’。 然后当用户调用 free 函数时,检查内存尾部的值是否还等于 0xFF 。如果不等于,那就是被修改了,然后在 free 函数中提示用户内存越界了,即往后越界检查。

第三种方案明显有一定的缺陷,比如用户对 p[10] 只读不写,又或者写入的值跟内存尾部的值一样,那么最后free 的时候,肯定触发不了异常。即使符合了触发条件,也只是提示这个指针有越界行为,并没有为开发者提供第一案发现场,排查起来有一定难度。

第三个方案要注意的是,因为越界检查是发生 free 函数的,如果此时越界的指针,还附带了内存泄露的情况,即 free 没有被调用,那么这个指针的越界情况将不被提示。所以使用第三种方案的情况,首先要做好内存泄露检查。

双重释放

双重释放又被称为多次释放,就是对同一个指针,多次调用 free 函数。如果你使用了内存检测工具,一般第二次释放就马上触发异常。

检测双重释放的方法有三种,首先前面两种的检测思想也是基于内存泄漏的节点信息。第一种方法是,当一个 free 函数调用时,在监听函数内部,找出对应的节点信息,如果找不到,则证明内存已经被释放,于是进程抛出异常并提示用户触发了双重释放。但这种方法有一个问题。前面已经提及,由于进入监控的时机不同,有些 malloc 操作的监听可能被跳过,导致链表中缺乏对应的节点信息,那么这个时候触发的提示其实就属于误报。这个时候用户选择继续执行,跳过该信息即可。

因为第一种方案可能会带来一些缺陷,所以又有第二种检测方案,就是当第一次调用 free 的时候,链表的节点不移除,而是给节点信息加上“释放”的标签。当第二次调用 free 函数时,检测到节点早已带有“释放”标签,这个时候就可以抛出异常了。而至于发生上面第一种方案所说的,找不到对应的节点信息,则说明我们之前可能没有捕捉该节点的malloc信息,所以free的时候跳过这个内存块检测,直接将参数交给真正的释放函数处理。

当然,在内存泄漏打印时,不再是直接打印节点信息,而是先判断节点是否带有“释放”标签。 而当同一个内存位置再次被 malloc 的时候,则需要移除“释放”标签。

通过比较上面两种方案可以发现,工具的监听时机其实也是很重要的,不然会产生误报,或者少报的情况。而第二种方案,如果工具内部处理不当的话,由于节点一直不释放,检测工具可能会占用大量的内存。

另外上面的内存越界我们也说过,由于兼容性和内存占用的原因,所以有些工具,是在free的时候才检测是否产生内存越界的,所以 free 函数的异常抛出和打印信息,大家要区分好是内存越界问题还是双重释放问题。

至于第三种方案,其实就是野指针检测方案,因为第一次调用 free 后, 指针 p 就已经成为了野指针,所以可以通过前面两种方案的其中一个,再配合上野指针检测,来提供更完善的双重释放检测机制。

野指针检测
关于野指针检测,首先需要了解内存分配原理。一般我们分配内存,都是调用 malloc/new 的,new 指令前面已经说过,底层也是调用 malloc 函数,而 malloc 函数,准确来说,它是从已分配好内存堆中,划分一个内存块返回给用户。注意了,是从已经分配的好的内存堆,也就是说即使你没有调用 malloc,这段内存堆早已经是有效的,而默认的情况下它的大小为1M。将来还可能随着用户的不断申请,这个内存堆会不断地主动扩大。

大家可以这样理解,malloc 其实是向进程借用一段内存的使用权,而 free 函数,其实是将内存的使用权归还给进程而已。所以即使没有调用malloc,进程都存在一份已经申请好的内存,只是我们不知道他的地址而已,假设有如下代码:

int main() {
    char* p = (char*)malloc(14); // 得到了内存堆中其中一个块的地址
    free(p);
    if(p){
    	p[0] = 'a';
    }
    return 0;
}

因为内存相对于进程是有效的,所以我们free之后,再调用 p[0] = ‘a’; 依旧能够正常执行,只是这样不符合开发规范而已,而且在更复杂的逻辑中,这样操作可能会修改了其它指针的数据,很容易导致崩溃。所以日常开发调用 free 之后,大家要记得立刻置空指针。

既然内存对于进程来说是有效的,free 之后依旧可以使用野指针进行读写操作,那么解决方法也是非常简单,就是用我们的老办法,一开始就将进程的内存堆,权限设置为不可访问,当 malloc 和 free 函数被调用时,再重新设置某个区域内存的访问权限。如此就只要再次访问 free 后的指针,就会触发异常。

那么这种办法是不是能够100%排查野指针问题呢?答案是不一定,假设我们free p 后,另外一个地方调用 malloc 返回的内存区域,刚好跟 p 重叠,那么当使用指针 p 时,就会访问到一段有权限的内存,最后可能还是会导致莫名其妙的崩溃。所以处理野指针的最好办法,就是尽快将释放的指针置空。例如利用智能指针,日常代码也应该做好空指针判断,避免崩溃。

小结
上面给大家介绍了内存泄漏、内存越界、双重释放、野指针检测的原理和方案,相信大家已经认识到,内存工具确实可以帮我们找出大部分的内存问题,但它并不是100%能杜绝内存问题,而且根据工具的方案不同,我们排查时要注意的事项也略有不同。

工具介绍

既然已经介绍完检测原理,我们已经知道内存检测的一些坑点,那么下面来介绍一下相关的使用工具。
下面介绍的,都是一些比较常用的操作方法,更详细的操作方法,大家需要自行查找。其实这些工具操作起来都是相当简单的,短时间即可上手。

Cppcheck
1、自动变量检查
2、数组的边界检查
3、class类检查
4、过期的函数,废弃函数调用检查
5、内存泄漏检查,主要是通过内存引用指针
6、异常内存使用,释放检查
7、操作系统资源释放检查,中断,文件描述符等
8、异常STL 函数使用检查
9、代码格式错误,以及性能因素检查

TscanCode
1、自动变量检查: 返回自动变量(局部变量)指针;
2、越界检查:数组越界返回自动变量(局部变量)指针;
3、类检查:构造函数初始化;
4、内存泄露检查;
5、空指针检查;
6、废弃函数检查;
7、有错误的代码参考

上面是两款常用的代码扫描器介绍,按照代码的严格性不同,可以选用不同的工具,操作也是傻瓜式的,选择项目目录后,点击扫描即可。大部分项目推荐使用TscanCode,原因是因为它检查的项没那么多,实际的开发中,大家都知道,代码质量是达不到高标准要求的。

AddressSanitizer
AddressSanitizer 又称之为 ASAN,目前大部分的编译器已经集成该功能,而且是跨平台的,所以这个是首先推荐大家使用的。支持的检测操作有:内存越界、双重释放、野指针检测。根据介绍,内存泄漏检测也是支持的。不过我在VS219下测试,发现它并不支持内存泄漏检测,不过这也没关系,我们可以接入额外的工具,比如VLD来进行内存泄露检查。

值得一提的是,它的内存越界检测,是使用头尾内存权限设置的方式,所以能够第一时间提供内存越界的案发现场,十分好用。而且它的监听时机跟注入式监听很相似,也就是在全局变量初始化之前就开始监听了,所以能够捕获全局的内存问题。

VS 中启用 ASAN也很简单,【属性 >> 配置属性 >> C/C++ >> 常规 >> 启用地址擦除系统 - 是 (/fsanitize=address)】

clang 和 gcc 编译器中,你可以如下设置CMakeLists.txt

# 启用 AddressSanitizer
set(ENABLE_ASAN TRUE)
if(ENABLE_ASAN)
    message(STATUS "AddressSanitizer enabled")
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address")
    set(CMAKE_LINKER_FLAGS "${CMAKE_LINKER_FLAGS} -fsanitize=address")
endif()

其中 fsanitize=address 是启用内存越界、双重释放、野指针检测,其它更多的操作,大家可以到网上自行查找。

Valgrind
Valgrind 目前只在LInux系统上面支持,排查方法是,通过Valgrind 启动你的进程,然后Valgrind会在出问题的地方,打印出对应的日志,大家根据日志信息进行排查即可。

在Linux上,大家可以通过包管理器安装 Valgrind。例如:

sudo apt-get install valgrind  # Ubuntu 或 Debian 系统
sudo yum install valgrind      # CentOS 或 Fedora 系统

然后通过 Valgrind 启动你的进程,一般使用如下指令排查内存问题:

valgrind --tool=memcheck --track-origins=yes --leak-check=full ./test.exe

在LInux 上,进程的后缀当然不叫exe,我这样写,只是为了方便大家理解哪个参数是进程而已
指令解释:
–tool=memcheck: 使用 Memcheck 工具,默认情况下 Valgrind 就使用这个工具,所以这个参数是可选的。它能够排查双重释放、野指针、内存越界问题,且为内存越界提供第一案发现场。也就是说它是通过设置内存块头尾区域的内存权限实现检测的。
–track-origins=yes: 在检测未初始化内存使用时,提供更详细的堆栈信息,帮助找出问题的根源。
–leak-check=full: 在程序退出时,打印内存泄漏报告。

可以附加的调试选项:
–read-var-info=yes: 可以在 Valgrind 输出中显示变量名,增加可读性,要记得在编译时启用调试符号)。
–error-exitcode=<code>: 可以设置 Valgrind 在检测到错误时返回一个非零退出码,以便在自动化测试中捕捉到内存错误。

Valgrind 会报告程序中未释放的内存块,这些内存块即为内存泄漏。泄漏报告分为4类:
definitely lost: 明确泄漏,程序失去了对该内存块的引用。
indirectly lost: 间接泄漏,某些泄漏的内存块引用了这些内存块。
possibly lost: 可能泄漏,程序可能丢失了对内存块的引用,或者这块内存还在使用但未释放。
still reachable: 程序退出时仍然可达,但未释放的内存块。

Valgrind 也是属于注入类型的排查工具,所以能够从全局变量初始化之前开始监听。

GFlags
GFlags 是 VS 中自带的内存检查工具,你可以通过VS进行安装,也可以在MSDN网站上下载,进行独立安装。GFlags.exe 是拥有界面的,但一般通过命令行使用:

gflags /p /enable test.exe /full /unaligned

test.exe 就是你的目标进程名称,只需要名称即可,不需要路径。但是上面的命令行,在Win10 上面执行后,运行 test.exe 会报错,根据网上反馈,/unaligned 命令在Win7是可用的,Win10 上目标进程都会报错,原因是Verifier.dll 某些动作与 ntdll.dll 不兼容。
所以在Win10上面使用 GFlags ,一般都是使用下面的命令:

gflags /p /enable test.exe /full /backwards

要注意gflags 是区分x86和x64的,大家需要将命令行指向对应的gflags进程执行,如果你不知道gflags进程在本机中的位置,可以通过 everything 工具搜索。 everything官网地址

如果要列出 gflags 正在监听的进程,则使用如下命令:

gflags /p 

gflags 是十分消耗机器性能的,所以不用的使用要记得关闭:

gflags /p /disable test.exe 

相信大家主要是对 full、backwards、unaligned 三个命令不熟悉,下面介绍一下三个指令的用法

full:普通页堆检查,支持双重释放,野指针、以及free函数中的内存越界检查。
backwards:依赖于 full 命令,用于向前越界检查,支持在内存块头部设置不可访问属性。
unaligned:依赖于 full 命令,用于内存越界检查,支持在内存块头尾两处设置内存不可访问属性。这个属性在前面已经说了,Win10下使用时有问题的,但是Win7上可以使用.

此外GFlags 还支持内存泄露检查,而且它是通过生成dump文件的形式提供用户调试检查的。前面已经说,一般内存泄露检查,是通过日志打印的方式提示的,这样更方便,但是一些幽灵内存问题,这样排查起来可能信息量不够,所以当日志不足以提供足够的信息反馈时,大家可以试试使用dump文件调试来检测内存泄露产生于何处。

不过dump生成设置有点复杂,还有另外一种是直接附加调试器来进行调试。有源码的情况下,你可以直接在VS对源码进行调试。又或者通过命令行来设置调试器,然后启动test.exe进程:

gflags /p /enable test.exe /full /backwards /debugger "调试器路径.exe"

GFlags 是通过注入Verifier.dll 的方式来进行内存检测的,所以它的监听也是在全局变量初始化之前进行。需要提醒大家的是,运行GFlags命令行是需要管理员权限的。

点击这里,链接到微软查看GFlags详细介绍:

Application Verifier

Application Verifier 跟 GFlags 一样,都是通过注入Verifier.dll 的方式来进行内存检测的,除了内存检测外,还支持下面的检测操作:
1、句柄泄漏检测:
2、多线程同步问题:
3、模拟低资源条件:
4、COM 组件的错误检测:
5、高层次的错误检测:

功能虽然比较丰富,但就内存检测而言,它虽然跟GFlags同样使用注入 Verifier.dll 的方式进行监听,但是对内存越界的检测,并没有提供内存块头尾部设置访问权限的方式,所以只关注内存检测这一块的话,不如直接使用GFlags 。

而对于Application Verifier的操作介绍,其实也是很简单的,它属于界面操作:

首先在菜单【File >> Add Application 】中选择你的exe路径

然后再界面右边的树控件中打上勾,表明你要监听的选项,一般是 Heaps、Leaks、Memroy这三个选项,你也可以加上其他选项,不过有些选项可能导致你的进程无法运行。

最后运行你的exe,进行各种操作后退出,再到Application Verifier的菜单中【View >> Logs】查看日志,通过日志来分析产生的内存泄露,如果你使用调试器调试进程的话,输出窗口中也是会打印对应的报错信息。

Application Verifier 跟GFlags一样,要注意x86和x64的区分,经我个人测试,Application Verifier对内存泄露的排查同样不好使,有时候并不提示内存泄露问题。需要提醒大家的是,运行Application Verifier 进程是需要管理员权限的。

VLD
VLD 全名 Visual Leak Detector,是一个动态库项目,主要是用来排查内存泄露问题。前面已经提过,由于一些奇奇怪怪的问题,有些工具对内存泄露的排查支持并不友好,所以其他排查工具对内存泄漏检查不理想的情况下,再给项目接入VLD,也是一个很好的选择。

点击跳转到VLD的github项目下载

下载VLD后编译x86 或 x64 版本的 dll 和 lib, 然后将 dll、lib、vld.h 三种文件拷贝你的项目中,只要在项目中引用 vld.h 文件即可,因为vld.h 文件里面,已经帮你引用了 lib。只需要将 dll 和 lib 放置到正确的位置,被项目引用即可。

需要注意的是,VLD 的注入时刻,是在C++ class 初始化阶段,也就是下面这个全局变量:

extern VisualLeakDetector g_vld;

前面已经说过,全局变量初始化,是先初始化C类型变量,再初始化 class 类型的变量,而且同类型的全局变量初始化顺序是无法控制的,所以VLD有可能漏掉一些内存分配监听,这个需要开发者自行对项目进行调试分析,查看项目中哪些全局变量的内存分配,是处于 VisualLeakDetector 构造之前的。

当main函数退出时,VisualLeakDetector 类被析构的时候,就会打印出所有未 free 的内存节点。

默认情况下,VLD是只支持Debug调试模式的,如果需要支持Release模式,则需要再 vld.h 头文件前面定义 VLD_FORCE_ENABLE 宏。如下:

#define VLD_FORCE_ENABLE
#include "vld.h"

总结

其实内存问题排查,所有的工具都会在调试器中输出异常信息,所以大家启用内存排查功能后,在调试器中运行项目即可。

内存问题排查,更多是针对堆损坏,也就是内存越界、野指针、双重释放这些问题,解决这些问题的核心,就是找到第一案发现场,而第一案发现场的获取关键,就是是否支持内存块头尾两处是否支持内存权限设置,这也是选取排查工具的关键。

在进行内存问题排查调试时,要记得关闭编译器的优化选项,否则会导致部分的输出信息准确性不够。
VS关闭优化:【属性 >> 配置属性 >> C/C++ >> 优化 >> 优化 - 已禁用 (/Od)】

CMakeList.txt 关闭优化:

# 设置编译器标志以禁用优化
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O0")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -O0")

而对于幽灵内存问题,排查起来难度很大,需要具体情况具体分析,这个则需要借助 dump 和内存调试技巧,比如Windbg 和 gdb 的 heap 分析指令,这个并不再本章的介绍范围内,大家可以自行了解。

  • 14
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值