内存泄露、死锁检测工具来了
扫描二维码
随时随地手机看文章
项目背景
现实困境
做C、C++开发的朋友应该都知道,C、C++中的内存是手动管理的,手动内存管理是一把双刃剑,虽然提供了极致性能,但可能由于开发者的一点点疏忽,就导致内存泄露。据非官方统计,全球每年因内存泄露导致的系统崩溃事故超过120万次。
C、C++开发者面临以下痛点时经常束手无策:
- 幽灵式内存泄露:程序运行数天后,出现内存耗尽,因为程序是一点点释放的,不太容易发现具体问题所在。
- 多线程竞态问题:死锁导致的服务假死,并且不好复现。
现有方案的局限
传统工具,Asan、valgrind、gdb功能非常强大,可以检测基本的问题,但也恰恰是因为功能太过丰富且强大,所以性能损耗非常高,无法用于线上环境,并且难以捕获随机出现的死锁场景。
项目目标
开发一个零侵入、高性能、全维度的运行时诊断系统:
- 内存监控:可以实时追踪每个内存块的完整生命周期。
- 死锁检测:可以检测出死锁,并能检测出哪个线程的哪几把锁出现了死锁,哪个线程由于等待的哪把锁而出现的死锁,可以精确关联源代码位置。
- 内存泄露检测:可以检测出具体哪块内存出现了泄露,并精确关联到源代码位置。
项目介绍
整体架构如图:
内存检测
直接看代码,下面代码会发生内存泄露:
extern "C"int TestMemoryLeak() { int *ptr = (int *)malloc(100); printf("TestMemoryLeak: %p\n", ptr); free(ptr); return 0; } extern"C"int TestMemoryLeak2() { int *ptr = (int *)malloc(110); printf("TestMemoryLeak2: %p\n", ptr); int *p = newint[10]; auto q = std::make_unique<int>(10); return 0; }
集成了工具后:
int main() { OpenDynamicExample(); MemoryDetector detect("/mnt/d/project/camping/detector/libdynamic_example.so"); detect.StartTracking(); UseDynamicExample(); detect.StopTracking(); // 会打印 lib1.so 的内存使用情况 CloseDynamicExample(); return 0; }
直接就可以检测这个动态库的内存情况:
本工具可以检测出程序申请了多少内存,申请了多少块内存,以及具体哪里发生了内存泄露,可以精确到具体的源代码位置。
它不仅可以检测malloc、free申请和释放的内存,即便是C++的new、delete、new[]、delete[]、std::make_unique、std::make_shared,也可以,不管程序是通过哪种方式申请和释放的内存,只要发生了内存泄露,工具都可以检测到。
整体采用Hook方案,基本流程如图:
死锁检测
看这段发生死锁的代码:
static void *ThreadFunc1(void *) { pthread_mutex_lock(&mutexA); std::cout << "Thread 1: Locked A\n"; sleep(1); std::cout << "Thread 1: Trying to lock B\n"; pthread_mutex_lock(&mutexB); std::cout << "Thread 1: Locked B\n"; pthread_mutex_unlock(&mutexB); pthread_mutex_unlock(&mutexA); return nullptr; } static void *ThreadFunc2(void *) { pthread_mutex_lock(&mutexB); std::cout << "Thread 2: Locked B\n"; sleep(1); std::cout << "Thread 2: Trying to lock A\n"; pthread_mutex_lock(&mutexA); std::cout << "Thread 2: Locked A\n"; pthread_mutex_unlock(&mutexA); pthread_mutex_unlock(&mutexB); return nullptr; } static void *ThreadFunc3(void *) { std::mutex mtx; std::cout << "Thread 3: Trying to lock mutex\n"; mtx.lock(); std::cout << "Thread 3: Locked mutex\n"; sleep(1); mtx.unlock(); return nullptr; } // 导出的函数,用于创建死锁场景 static void CreateDeadlock() { pthread_t t1, t2, t3; pthread_attr_t attr; // 初始化线程属性 pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 创建分离的线程 pthread_create(&t1, &attr, ThreadFunc1, nullptr); pthread_create(&t2, &attr, ThreadFunc2, nullptr); pthread_create(&t3, &attr, ThreadFunc3, nullptr); // 销毁线程属性 pthread_attr_destroy(&attr); // 等待一段时间让死锁发生 sleep(3); }
从代码中可以看到,Thread1和Thread2会发生死锁,集成工具后:
LockHook lock_hook("./libdynamic_example.so"); if (!lock_hook.StartTracking()) { std::cerr << "Failed to start lock tracking\n"; dlclose(handle); return 1; } lock_hook.StopTracking();
结果如图:
工具可以检测出哪里发生了死锁、哪个线程持有了哪把锁、以及哪把锁被哪个线程持有了。
且无论你是通过pthread_lock、还是mutex.lock、还是unique_lock或者lock_guard,只要发生了死锁,工具都可以检测到,并且可以定位到源代码位置。
整体也采用Hook方案,流程如图所示:
项目收获
项目代码量不大,核心代码大概2000行左右,但涉及到的技术内容非常丰富且硬核。
通过本项目,你可以收获到:
- 提升C、C++的编码能力、内存管理黑科技、多线程调试技巧
- ELF 文件结构,包括section 和 segment的概念以及具体作用等。
- 编译链接技术,动态链接与静态链接的区别。
- 动态链接与加载,了解动态链接器如何在运行时解析符号和加载动态库。
- PLT机制,与GOT之间的关系。
- GOT作用,如何存储动态链接的函数地址。
- 函数调用约定,不同架构下的函数调用约定。
- 内存保护机制,了解Linux上的内存保护机制(如DEP、ASLR),以及如何影响代码注入和钩子技术。
- 调试工具,使用工具(如objdump、gdb)分析二进制文件,理解如何定位和修改PLT。
- Hook技术,如何将自定义代码注入到目标进程中,以实现钩子功能。
- 钩子的安全性,钩子技术是否有风险。
- 编写和测试,学习如何编写钩子代码,并在不同环境中进行测试。
- 钩子技术的性能分析。
- 动态库的加载过程,详细了解共享库的加载过程,包括如何在运行时解析依赖关系。
- 符号解析与重定位,符号解析的机制以及重定位表的作用。
- 内存管理和分配机制,内存管理机制,特别是如何安全地分配和修改内存以实现钩子。
- 内存泄漏检测技术,理解了内存管理分配机制,可以实现检测内存泄漏的能力。
- 锁机制,锁的底层实现原理,如何实现加解锁相关的钩子。
- 死锁检测技术,理解了加解锁的底层机制,可以实现检测程序是否产生了死锁。
- 编译链接技术,Debug模式和Release模式的区别。
- 符号管理机制,调试符号信息的作用。
- 调用栈技术,如何获取线程的调用堆栈,如何根据地址解析出对应代码函数名和行号。





