news 2026/6/17 4:59:56

手写 C++ 内存泄漏检测器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
手写 C++ 内存泄漏检测器

手写 C++ 内存泄漏检测器

  • 1.引言
  • 2.宏定义检测内存泄漏
  • 3.Hook检测内存泄漏
  • 4.AddressSanitizer检测内存泄漏
  • 5.内存检测器整体设计
  • 6.代码
  • 7.死锁问题分析
  • 8.Guard Bytes
  • 9.结尾

1.引言

内存泄漏不同于程序崩溃、报错等显性bug,它不会让程序立即失效,而是以静默消耗资源的方式持续蚕食系统内存,导致程序运行速度逐渐卡顿、响应延迟升高,最终引发OOM内存溢出、服务宕机、程序闪退等严重问题。

本文将重点介绍如何检测内存泄漏,并实现一个检测内存泄漏的工具,帮助加深理解。

2.宏定义检测内存泄漏

检测内存泄漏的整体思路就是:记录每一次内存的申请和释放。哪个文件、哪一行申请的内存,记录下来。
在程序执行结束后,通过查看记录的内存分配和释放条目,就可以知道有没有发生内存泄漏了。

#include<iostream>#include<cstdlib>void*_malloc(size_tsize,constchar*file,intline){std::cout<<"[+]Allocating "<<size<<" bytes at "<<file<<":"<<line<<"\n";returnmalloc(size);}void_free(void*ptr){std::cout<<"[-]Deallocating memory at "<<ptr<<"\n";free(ptr);}#definemalloc(size)_malloc(size,__FILE__,__LINE__)#definefree(ptr)_free(ptr)intmain(){int*p1=(int*)malloc(100);int*p2=(int*)malloc(200);free(p2);// p1故意不释放return0;}

执行结果:

以上就是一个极简的检测内存泄漏的方法,不用纠结它在实际场景下有没有用,关键在于理解这种思想——万变不离其宗。

程序的执行有四个过程,分别是:预处理、编译、汇编和链接
其中,宏替换就发生在预处理阶段。main函数里的malloc,会被替换成_malloc(size, __FILE__, __LINE__),然后在_malloc函数内部,记录这次申请发生在哪个函数的哪一行。free也是同样的道理。这样,就做到了对内存分配和释放的追踪。

通过上面的运行结果,我们可以看到,申请了两次内存,但是只释放了一次,所以必然存在内存泄漏的问题。

3.Hook检测内存泄漏

Hook检测内存泄漏的原理是劫持系统调用,在真正执行系统调用之前,记录内存申请信息。

#include<stdio.h>#include<stdlib.h>#include<dlfcn.h>// 保存原来系统调用typedefvoid*(*sys_malloc_t)(size_t);typedefvoid(*sys_free_t)(void*);staticsys_malloc_tsys_malloc=NULL;staticsys_free_tsys_free=NULL;__attribute__((constructor))voidinit_hook(){sys_malloc=(sys_malloc_t)dlsym(RTLD_NEXT,"malloc");sys_free=(sys_free_t)dlsym(RTLD_NEXT,"free");}staticintin_hook=0;void*malloc(size_tsize){if(in_hook)returnsys_malloc(size);// 避免递归调用in_hook=1;// 标记进入hookvoid*call_addr=__builtin_return_address(0);printf("[+][%p]size: %ld\n",call_addr,size);in_hook=0;// 标记退出hookreturnsys_malloc(size);}voidfree(void*ptr){if(in_hook)returnsys_free(ptr);// 避免递归调用in_hook=1;// 标记进入hookvoid*call_addr=__builtin_return_address(0);printf("[-][%p]\n",call_addr);in_hook=0;// 标记退出hooksys_free(ptr);}intmain(){char*p1=(char*)malloc(10);char*p2=(char*)malloc(20);free(p1);return0;}

这里解释一下代码中的一些疑点:

  • attribute((constructor))init_hook函数被标记为构造函数,目的是让init_hook函数在main函数之前被调用。
  • RTLD_NEXT:跳过当前模块,查找下一个匹配的符号,从而避免直接调用自身导致死循环。
  • in_hook:这个字段是用来避免无限递归调用。因为printf函数内部会调用malloc,而系统的malloc已经被我们劫持,所以printf调到的其实是我们自定义的malloc。在自定义malloc里调自定义malloc,一直无限循环下去,最终就会栈溢出。
  • __builtin_return_address(0):这是 GCC 编译器提供的一个内置函数。参数 0 表示获取当前函数的返回地址,也就是“是谁调用了这个 malloc/free”的内存地址。通过打印这个地址,我们就可以结合addr2line工具,将内存地址转换为具体的代码文件名和行号,从而精准的定位内存泄漏。

执行结果:

4.AddressSanitizer检测内存泄漏

AddressSanitizer(简称 ASan)是由 Google 开发的一款高效的内存错误检测工具,现已集成在主流的 GCC 和 Clang 编译器中。它不仅能检测缓冲区溢出、使用已释放内存等运行时错误,还具备强大的堆内存泄漏自动检测能力。

要在 C/C++ 项目中启用 ASan,需要在编译和链接时添加特定的标志。

  • -fsanitize=address:开启 AddressSanitizer3。
  • -g:包含调试符号,以便输出具体的文件名和行号。
  • -fno-omit-frame-pointer:保留帧指针,帮助 ASan 生成清晰的回溯调用栈。
  • -O1 或 -O0:降低优化级别,避免内联等优化导致报错位置偏移。

我们以下面的简单代码为例,通过Asan来检测内存泄漏。

#include<stdio.h>#include<stdlib.h>intmain(){int*p1=(int*)malloc(100);int*p2=(int*)malloc(200);free(p2);// p1故意不释放return0;}

gcc -fsanitize=address,leak -g memleak3.cpp -o test3

运行结果:
AddressSanitizer默认只检测越界访问、Use After Free、栈溢出等,内存泄漏检测需要我们手动开启,编译选项加上-fsanitize=leak即可。

5.内存检测器整体设计

memleak_project/
├── leak_detector.hpp
├── leak_detector.cpp
├── main.cpp

这是内存检测器小项目的代码框架,功能包括:

  • malloc/free hook
  • new/delete hook
  • DBG_NEW(文件+行号)
  • Leak检测
  • Double Free检测
  • Invalid Free检测
  • Buffer Overflow检测(Guard Bytes)
  • backtrace调用栈
  • 线程安全

6.代码

#pragmaonce#include<unordered_map>#include<mutex>#include<string>#include<cstdint>#include<cstdlib>constexpruint64_tHEAD_MAGIC=0xDEADBEEFCAFEBABE;constexpruint64_tTAIL_MAGIC=0xABCDEF1234567890;constexprintMAX_BACKTRACE=16;structMemMeta{size_tsize;constchar*file;intline;bool freed;//标记该内存是否已经被释放void*stack[MAX_BACKTRACE];intstack_size;};class LeakDetector{public:staticinlinethread_local bool s_in_detector=false;structReentryGuard{ReentryGuard(){LeakDetector::s_in_detector=true;}~ReentryGuard(){LeakDetector::s_in_detector=false;}};staticvoid*allocate(size_tsize,constchar*file,intline);staticvoiddeallocate(void*ptr);staticvoidreport();private:staticstd::unordered_map<void*,MemMeta>_records;staticstd::mutex _mutex;};class AutoReporter{public:~AutoReporter();};externAutoReporter g_reporter;// hookvoid*dbg_malloc(size_tsize,constchar*file,intline);voiddbg_free(void*ptr);void*operatornew(size_tsize);voidoperatordelete(void*ptr)noexcept;void*operator new[](size_tsize);voidoperator delete[](void*ptr)noexcept;void*operatornew(size_tsize,constchar*file,intline);void*operator new[](size_tsize,constchar*file,intline);#defineDBG_NEWnew(__FILE__,__LINE__)#defineDBG_MALLOC(size)dbg_malloc(size,__FILE__,__LINE__)#defineDBG_FREE(ptr)dbg_free(ptr)

这个头文件主要完成接口声明和结构体的定义的。

#include"leak_detector.hpp"#include<iostream>#include<execinfo.h>#include<cstring>// 定义std::unordered_map<void*,MemMeta>LeakDetector::_records;std::mutex LeakDetector::_mutex;AutoReporter g_reporter;void*LeakDetector::allocate(size_tsize,constchar*file,intline){if(s_in_detector){returnstd::malloc(size);}ReentryGuard guard;size_ttotal_size=sizeof(u_int64_t)+size+sizeof(u_int64_t);char*raw=(char*)malloc(total_size);if(!raw)throw std::bad_alloc();*(u_int64_t*)raw=HEAD_MAGIC;*(u_int64_t*)(raw+sizeof(u_int64_t)+size)=TAIL_MAGIC;void*user_ptr=raw+sizeof(u_int64_t);MemMeta meta;meta.size=size;meta.file=file;meta.line=line;meta.freed=false;meta.stack_size=backtrace(meta.stack,MAX_BACKTRACE);{std::lock_guard<std::mutex>lock(_mutex);_records[user_ptr]=meta;}returnuser_ptr;}voidLeakDetector::deallocate(void*ptr){if(!ptr)return;if(s_in_detector){std::free(ptr);return;}ReentryGuard guard;std::lock_guard<std::mutex>lock(_mutex);autoit=_records.find(ptr);if(it==_records.end()){std::cerr<<"\n[INVALID FREE]\n"<<"ptr = "<<ptr<<'\n';return;}MemMeta&meta=it->second;if(meta.freed){std::cerr<<"\n[DOUBLE FREE]\n"<<"ptr = "<<ptr<<'\n';return;}char*raw=(char*)ptr-sizeof(u_int64_t);uint64_thead=*(uint64_t*)raw;uint64_ttail=*(uint64_t*)(raw+sizeof(u_int64_t)+meta.size);if(head!=HEAD_MAGIC){std::cerr<<"\n[BUFFER UNDERFLOW]\n";}if(tail!=TAIL_MAGIC){std::cerr<<"\n[BUFFER OVERFLOW]\n";}meta.freed=true;free(raw);_records.erase(it);}voidLeakDetector::report(){std::lock_guard<std::mutex>lock(_mutex);if(_records.empty()){std::cout<<"\n[NO LEAK]\n";return;}std::cout<<"\n====================================\n";std::cout<<"MEMORY LEAK REPORT\n";std::cout<<"====================================\n";for(auto&[ptr,meta]:_records){std::cout<<"\nLeak Address: "<<ptr<<'\n';std::cout<<"Size: "<<meta.size<<" bytes\n";std::cout<<"Location: "<<meta.file<<":"<<meta.line<<'\n';char**symbols=backtrace_symbols(meta.stack,meta.size);std::cout<<"Backtrace:\n";for(inti=0;i<meta.stack_size;i++){std::cout<<" "<<symbols[i]<<'\n';}std::free(symbols);std::cout<<"--------------------------------\n";}}AutoReporter::~AutoReporter(){LeakDetector::report();}void*dbg_malloc(size_tsize,constchar*file,intline){returnLeakDetector::allocate(size,file,line);}voiddbg_free(void*ptr){LeakDetector::deallocate(ptr);}void*operatornew(size_tsize){returnLeakDetector::allocate(size,"Unkown",0);}voidoperatordelete(void*ptr)noexcept{LeakDetector::deallocate(ptr);}void*operator new[](size_tsize){returnLeakDetector::allocate(size,"Unkown",0);}voidoperator delete[](void*ptr)noexcept{LeakDetector::deallocate(ptr);}void*operatornew(size_tsize,constchar*file,intline){returnLeakDetector::allocate(size,file,line);}void*operator new[](size_tsize,constchar*file,intline){returnLeakDetector::allocate(size,file,line);}

这是对头文件中接口的实现。

#include"leak_detector.hpp"#include<iostream>#include<cstring>voidleakTest(){std::cout<<"\n===== Leak Test =====\n";int*p=DBG_NEWint[10];}voiddoubleFreeTest(){std::cout<<"\n===== double Free Test =====\n";int*p=DBG_NEWint;delete p;delete p;}voidInvalidFreeTest(){std::cout<<"\n===== Invalid Free Test =====\n";intx=10;dbg_free(&x);}voidOverflowTest(){std::cout<<"\n===== Overflow Test =====\n";char*buf=(char*)DBG_MALLOC(16);std::strcpy(buf,"abcdefghijklmnopqrstuvwxyz");DBG_FREE(buf);}voidMallocTest(){std::cout<<"\n===== malloc/free Test =====\n";void*p1=DBG_MALLOC(128);DBG_FREE(p1);void*p2=DBG_MALLOC(256);(void)p2;}voidNewDeleteTest(){std::cout<<"\n===== new/delete Test =====\n";int*p=newint(10);delete p;}intmain(){leakTest();doubleFreeTest();InvalidFreeTest();OverflowTest();MallocTest();NewDeleteTest();return0;}

这是测试代码。
编译命令:g++ leak_detector.cpp main.cpp -o test -ldl

7.死锁问题分析

void*LeakDetector::allocate(size_tsize,constchar*file,intline){......{std::lock_guard<std::mutex>lock(_mutex);_records[user_ptr]=meta;}......}

我们看看调用链:调用allocate分配内存——>lock_guard加锁——>哈希表 _records[user_ptr] = meta内部,会调用operator new分配内存——>因为我们自定义了全局operator new,所以调用的哈希表内部调用的实际上就是我们自定义的operator new——>自定义的全局operator new调用allocate——>申请锁。

线程自己持有锁,进入了临界区,但是在临界区内,又申请同一把锁,所以导致死锁。

通过引入Reentrancy Guard,避免allocate被重入。

8.Guard Bytes

constexpr uint64_t HEAD_MAGIC = 0xDEADBEEFCAFEBABE;
constexpr uint64_t TAIL_MAGIC = 0xABCDEF1234567890;

这里引入了两个魔法数字,在申请内存时,实际上是这样分配的:


多分配了16个字节,HEAD_MAGIC和TAIL_MAGIC各占8字节。

这么做的原因在于malloc不检测内存的上溢和下溢,越界可能只是写到了隔壁的内存,程序可能不崩,晚点崩,随机崩。
在用户内存前后埋“哨兵”,如果魔法数字被修改,就说明内存溢出。

9.结尾

欢迎批评指正!

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/17 4:58:45

情感分析工具链选型指南:从预处理到部署的四层架构

1. 项目概述&#xff1a;为什么“选工具”比“写代码”更决定 sentiment analyzer 的成败我带过七支不同行业的NLP小队&#xff0c;从电商客服语义路由系统&#xff0c;到医疗问诊记录情绪波动监测平台&#xff0c;再到地方政府12345热线工单情感倾向归类项目。每次复盘&#x…

作者头像 李华
网站建设 2026/6/17 4:57:45

ThinkPad风扇控制终极指南:3分钟让笔记本告别噪音与高温

ThinkPad风扇控制终极指南&#xff1a;3分钟让笔记本告别噪音与高温 【免费下载链接】TPFanCtrl2 ThinkPad Fan Control 2 (Dual Fan) for Windows 10 and 11 项目地址: https://gitcode.com/gh_mirrors/tp/TPFanCtrl2 你是否曾经在安静的环境中被ThinkPad风扇的"直…

作者头像 李华
网站建设 2026/6/6 12:53:44

DC综合网表预处理:set verilogout_no_tri与set_fix_multiple_port_nets指令详解

1. 项目概述&#xff1a;为什么DC综合网表需要“预处理”&#xff1f;在数字芯片设计的后端流程里&#xff0c;从逻辑综合到物理实现的交接点&#xff0c;网表文件的质量直接决定了后续布局布线&#xff08;APR&#xff09;的成败。很多工程师&#xff0c;尤其是刚接触全流程的…

作者头像 李华
网站建设 2026/6/6 12:49:15

Ollama:本地大模型部署利器

可以查看官网&#xff1a;https://ollama.com 一、什么是 Ollama Ollama 是一个开源的大语言模型&#xff08;LLM&#xff09;本地部署与运行框架。它让开发者可以在自己的电脑上 — 无论是 Windows、macOS 还是 Linux — 一键下载和运行 Llama、DeepSeek、Qwen、Gemma、Mistr…

作者头像 李华
网站建设 2026/6/6 12:45:53

FPGA时序分析核心:TimeQuest模型、路径计算与调试实战

1. 时序分析&#xff1a;数字设计的“心跳”与“脉搏”搞数字电路设计&#xff0c;尤其是FPGA开发&#xff0c;时序分析&#xff08;Timing Analysis&#xff09;是绕不过去的一道坎。你可以把它想象成给一个复杂的机械钟表校时&#xff0c;不仅要确保每个齿轮&#xff08;逻辑…

作者头像 李华