当前位置:首页 > 单片机 > 程序喵大人

项目背景

现实困境

做C、C++开发的朋友应该都知道,C、C++中的内存是手动管理的,手动内存管理是一把双刃剑,虽然提供了极致性能,但可能由于开发者的一点点疏忽,就导致内存泄露。据非官方统计,全球每年因内存泄露导致的系统崩溃事故超过120万次。

C、C++开发者面临以下痛点时经常束手无策:

  • 幽灵式内存泄露:程序运行数天后,出现内存耗尽,因为程序是一点点释放的,不太容易发现具体问题所在。
  • 多线程竞态问题:死锁导致的服务假死,并且不好复现。

现有方案的局限

传统工具,Asanvalgrindgdb功能非常强大,可以检测基本的问题,但也恰恰是因为功能太过丰富且强大,所以性能损耗非常高,无法用于线上环境,并且难以捕获随机出现的死锁场景。

项目目标

开发一个零侵入、高性能、全维度的运行时诊断系统:

  • 内存监控:可以实时追踪每个内存块的完整生命周期。
  • 死锁检测:可以检测出死锁,并能检测出哪个线程的哪几把锁出现了死锁,哪个线程由于等待的哪把锁而出现的死锁,可以精确关联源代码位置。
  • 内存泄露检测:可以检测出具体哪块内存出现了泄露,并精确关联到源代码位置。

项目介绍

整体架构如图:

内存检测

直接看代码,下面代码会发生内存泄露:

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; } 

直接就可以检测这个动态库的内存情况:

本工具可以检测出程序申请了多少内存,申请了多少块内存,以及具体哪里发生了内存泄露,可以精确到具体的源代码位置。

它不仅可以检测mallocfree申请和释放的内存,即便是C++的newdeletenew[]delete[]std::make_uniquestd::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); } 

从代码中可以看到,Thread1Thread2会发生死锁,集成工具后:

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行左右,但涉及到的技术内容非常丰富且硬核。

通过本项目,你可以收获到:

  1. 提升C、C++的编码能力、内存管理黑科技、多线程调试技巧
  2. ELF 文件结构,包括section 和 segment的概念以及具体作用等。
  3. 编译链接技术,动态链接与静态链接的区别。
  4. 动态链接与加载,了解动态链接器如何在运行时解析符号和加载动态库。
  5. PLT机制,与GOT之间的关系。
  6. GOT作用,如何存储动态链接的函数地址。
  7. 函数调用约定,不同架构下的函数调用约定。
  8. 内存保护机制,了解Linux上的内存保护机制(如DEP、ASLR),以及如何影响代码注入和钩子技术。
  9. 调试工具,使用工具(如objdump、gdb)分析二进制文件,理解如何定位和修改PLT。
  10. Hook技术,如何将自定义代码注入到目标进程中,以实现钩子功能。
  11. 钩子的安全性,钩子技术是否有风险。
  12. 编写和测试,学习如何编写钩子代码,并在不同环境中进行测试。
  13. 钩子技术的性能分析。
  14. 动态库的加载过程,详细了解共享库的加载过程,包括如何在运行时解析依赖关系。
  15. 符号解析与重定位,符号解析的机制以及重定位表的作用。
  16. 内存管理和分配机制,内存管理机制,特别是如何安全地分配和修改内存以实现钩子。
  17. 内存泄漏检测技术,理解了内存管理分配机制,可以实现检测内存泄漏的能力。
  18. 锁机制,锁的底层实现原理,如何实现加解锁相关的钩子。
  19. 死锁检测技术,理解了加解锁的底层机制,可以实现检测程序是否产生了死锁。
  20. 编译链接技术,Debug模式和Release模式的区别。
  21. 符号管理机制,调试符号信息的作用。
  22. 调用栈技术,如何获取线程的调用堆栈,如何根据地址解析出对应代码函数名和行号。


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭