博主介绍:程序喵大人
- 35- 资深C/C++/Rust/Android/iOS客户端开发
- 10年大厂工作经验
- 嵌入式/人工智能/自动驾驶/音视频/游戏开发入门级选手
- 《C++20高级编程》《C++23高级编程》等多本书籍著译者
- 更多原创精品文章,首发gzh,见文末
- 👇👇记得订阅专栏,以防走丢👇👇
😉C++基础系列专栏
😃C语言基础系列专栏
🤣C++大佬养成攻略专栏
🤓C++训练营
👉🏻个人网站
项目背景
现实困境
做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模式的区别。
-
符号管理机制,调试符号信息的作用。
-
调用栈技术,如何获取线程的调用堆栈,如何根据地址解析出对应代码函数名和行号。
本项目为C++训练营专属,感兴趣可以看文末,备注训练营。
码字不易,欢迎大家点赞,关注,评论,谢谢!
C++训练营
专为校招、社招3年工作经验的同学打造的1V1 C++训练营,量身定制学习计划、每日代码review,简历优化,面试辅导,已帮助多名学员获得大厂offer!