内存、死锁检测工具

项目是什么,用来解决什么问题
这是一个面向 Linux C/C++ 动态库的内存与死锁检测工具项目。你要亲手实现的,不只是一个“调用现成工具查看结果”的 Demo,而是一套能够拦截目标调用、记录运行信息,并把异常关联到内存分配位置和线程锁关系的诊断工具。
设想两个常见的排障场景:服务功能正常,但运行时间越长,内存占用越高;程序没有退出,却有几个线程一直不再向前执行。只看日志或内存总量,很难回答:哪次分配没有释放?哪个线程拿着什么锁,又在等待谁?
这个项目围绕两条线展开:
- 追踪内存的去向。 在目标调用路径上记录分配地址、大小及调用信息,关联释放事件;在检查时找出尚未释放的分配,再结合业务生命周期判断是否存在泄漏。
- 解释线程为什么停住。 记录线程的持锁与等待关系,识别相互等待的环路,让“程序卡住了”变成可以追查的线程、锁和调用位置。
值得深入的工程难点,不在于打印一份报告,而在于报告背后的实现:
- 怎样接入已有动态库? 沿 ELF、PLT/GOT 和动态链接路径理解函数调用,学习 Hook 怎样将目标调用转交给自己的记录逻辑,再继续执行原有操作。
- 怎样记录事件,又不干扰被测程序? 分配与释放可能来自多个线程,记录逻辑自身也可能申请内存;需要考虑并发访问、递归进入以及开始和停止跟踪时的状态管理。
- 怎样从运行信息还原问题? 把地址、调用栈、线程与锁关联起来,区分“仍然存活的对象”和“已丢失释放机会的分配”,也区分普通锁等待与循环等待。
项目整体架构如下,后面的两组示例分别展示内存跟踪和锁跟踪的使用方式。

内存检测:从“占用在涨”追到具体分配。
下面保留项目原有示例:TestMemoryLeak() 已经调用 free(ptr),这一笔分配没有泄漏;TestMemoryLeak2() 中的 ptr 和 p 没有对应释放,而 q 是局部智能指针,离开作用域时会自动释放它管理的对象。
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 = new int[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、new、delete、new[]、delete[] 以及 std::make_unique、std::make_shared 等场景。检测是否覆盖它们,取决于实际分配最终是否经过已拦截的调用路径;自定义分配器、静态链接和不同运行库实现,都需要单独验证,不能承诺所有分配方式都能被捕获。
Hook 记录流程如下。学习重点是把“拦截—记录—继续调用—匹配释放”串起来,而不是只看到最后的统计数字。

死锁检测:把“线程不动了”还原成等待关系。
下面的原有示例让两个线程按相反顺序申请 mutexA 和 mutexB。当线程 1 持有 A、线程 2 持有 B,然后分别等待对方持有的锁时,就形成循环等待;线程 3 使用自己的局部锁,不属于这个环路。
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);
}
示例中的 sleep 只是增加出现上述交错顺序的机会,并不是确定性的线程同步手段。验证时应观察实际持锁和等待关系,不能仅凭等待了几秒就判断一定发生死锁。
原稿提供的锁跟踪接口片段如下。实际使用时,目标业务的执行与观察应放在 StartTracking() 和 StopTracking() 之间;仅启动后立即停止,不能据此确认业务里的死锁已被检测。
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();
下面是原稿中的死锁检测结果示例:

你要读懂的不是一句“存在死锁”,而是:线程 1 持有 A、等待 B,线程 2 持有 B、等待 A,这些关系怎样组成闭环。死锁与数据竞争是不同问题,这个项目的锁跟踪不能直接等同于通用的数据竞争检测。
pthread_mutex_lock、std::mutex::lock 以及 std::unique_lock、std::lock_guard 涉及的底层路径是否可被拦截,需要按运行库与锁类型验证。定位到源代码行还依赖调试符号、调用栈获取和地址解析;不能保证所有 Release 构建都保留同样清晰的位置。
锁跟踪同样采用 Hook 思路,流程如下:

项目技术栈
项目以 C/C++ 和 Linux 运行时机制为主,不靠堆叠框架名称体现难度。每项技术都对应诊断链路上的具体职责。
| 技术与机制 | 在项目中承担的职责 |
|---|---|
| C/C++、资源管理与多线程编程 | 实现检测接口、事件记录和状态维护,处理分配生命周期与多线程下的共享数据。 |
| Linux、ELF、共享动态库 | 理解 section、segment、动态库加载和依赖解析,确定目标模块与运行时调用路径。 |
| PLT/GOT、符号解析与重定位 | 理解动态链接函数地址怎样被解析,找到 Hook 的接入位置与适用条件。 |
| 内存分配与释放 Hook | 在可拦截路径上记录分配信息、关联释放事件,生成用于判断泄漏的诊断记录。 |
| pthread 互斥锁、C++ 锁封装与锁 Hook | 观察加锁、解锁、持有和等待关系,分析线程之间是否形成循环等待。 |
| 调用栈、调试符号与 ABI/调用约定 | 将运行地址关联到函数和源码位置,同时理解不同架构、优化配置下的差异。 |
| 内存保护、DEP/ASLR | 理解内存权限与地址布局对 Hook 的影响,避免把修改调用路径当作没有边界的操作。 |
| objdump、gdb、测试与性能分析 | 查看二进制与调试运行状态,验证 Hook 是否生效,并评估记录逻辑引入的开销。 |
建议先用一个可控动态库打通分配与释放记录,再增加调用位置解析;锁跟踪则从两线程、两把锁的最小场景开始。每扩展一种接口或运行环境,都增加验证案例,而不是默认它与已有场景完全相同。
这是一项帮助理解诊断工具内部机制的工程实践,不是宣称替代 ASan、Valgrind 或 gdb,也没有经过测量就保证“零开销”或适用于所有线上环境。
完成项目后能收获什么

完成实现并亲手验证后,你应当能够把收获落实到作品、排障和设计解释上:
- 做出一套可以演示的诊断工具。 用正常释放、遗漏释放和循环等待等案例展示检测过程,说明输入是什么、跟踪了哪些事件、输出如何指向问题,而不是只展示一张结果截图。
- 把编译链接知识连到运行中的程序。 讲清静态链接与动态链接的区别,理解 ELF 的 section/segment、共享库加载、符号解析、重定位及 PLT/GOT 如何参与函数调用。
- 建立内存生命周期的排查思路。 从分配、使用到释放关联记录,解释为什么智能指针的正常析构不应被当作泄漏,以及为什么“检查时未释放”仍需要结合业务判断。
- 用线程与锁的关系解释死锁。 还原持有与等待链路,区分正常等待、循环等待和数据竞争;针对锁顺序等原因提出修改办法,再用相同场景验证。
- 理解 Hook 的工程边界。 讨论递归进入、多线程记录、ABI/调用约定、内存保护及 DEP/ASLR 的影响,知道接入失败或报告不完整时该检查什么。
- 提升二进制分析与调试能力。 用 objdump、gdb 检查调用路径,结合调用栈与调试符号定位问题;解释 Debug/Release 配置及编译优化为什么可能影响源码定位。
- 形成有证据的项目经历。 对比开启和关闭跟踪的运行开销,记录覆盖范围与未覆盖场景。面试时能回答:Hook 放在哪里?如何关联分配和释放?怎样避免记录逻辑递归?如何证明两个线程形成死锁?报告为什么有时没有行号?
这个项目适合已经掌握指针、内存管理和基础多线程编程,希望进一步理解 Linux 动态链接与底层排障的同学。建议先复现一个问题,再实现一条跟踪链路,最后扩展测试与报告;不必一开始就试图覆盖所有分配器和锁实现。
最终收获,是把“会用检测工具”进一步变成“知道工具为什么能检测、在哪里可能失效,并能用证据讲清问题”。 让简历中的 Linux、内存管理和并发调试有一套具体作品作为支撑。





