內(nèi)存泄露、死鎖檢測(cè)工具來(lái)了
掃描二維碼
隨時(shí)隨地手機(jī)看文章
項(xiàng)目背景
現(xiàn)實(shí)困境
做C、C++開(kāi)發(fā)的朋友應(yīng)該都知道,C、C++中的內(nèi)存是手動(dòng)管理的,手動(dòng)內(nèi)存管理是一把雙刃劍,雖然提供了極致性能,但可能由于開(kāi)發(fā)者的一點(diǎn)點(diǎn)疏忽,就導(dǎo)致內(nèi)存泄露。據(jù)非官方統(tǒng)計(jì),全球每年因內(nèi)存泄露導(dǎo)致的系統(tǒng)崩潰事故超過(guò)120萬(wàn)次。
C、C++開(kāi)發(fā)者面臨以下痛點(diǎn)時(shí)經(jīng)常束手無(wú)策:
- 幽靈式內(nèi)存泄露:程序運(yùn)行數(shù)天后,出現(xiàn)內(nèi)存耗盡,因?yàn)槌绦蚴且稽c(diǎn)點(diǎn)釋放的,不太容易發(fā)現(xiàn)具體問(wèn)題所在。
- 多線程競(jìng)態(tài)問(wèn)題:死鎖導(dǎo)致的服務(wù)假死,并且不好復(fù)現(xiàn)。
現(xiàn)有方案的局限
傳統(tǒng)工具,Asan、valgrind、gdb功能非常強(qiáng)大,可以檢測(cè)基本的問(wèn)題,但也恰恰是因?yàn)楣δ芴^(guò)豐富且強(qiáng)大,所以性能損耗非常高,無(wú)法用于線上環(huán)境,并且難以捕獲隨機(jī)出現(xiàn)的死鎖場(chǎng)景。
項(xiàng)目目標(biāo)
開(kāi)發(fā)一個(gè)零侵入、高性能、全維度的運(yùn)行時(shí)診斷系統(tǒng):
- 內(nèi)存監(jiān)控:可以實(shí)時(shí)追蹤每個(gè)內(nèi)存塊的完整生命周期。
- 死鎖檢測(cè):可以檢測(cè)出死鎖,并能檢測(cè)出哪個(gè)線程的哪幾把鎖出現(xiàn)了死鎖,哪個(gè)線程由于等待的哪把鎖而出現(xiàn)的死鎖,可以精確關(guān)聯(lián)源代碼位置。
- 內(nèi)存泄露檢測(cè):可以檢測(cè)出具體哪塊內(nèi)存出現(xiàn)了泄露,并精確關(guān)聯(lián)到源代碼位置。
項(xiàng)目介紹
整體架構(gòu)如圖:
內(nèi)存檢測(cè)
直接看代碼,下面代碼會(huì)發(fā)生內(nèi)存泄露:
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(); // 會(huì)打印 lib1.so 的內(nèi)存使用情況 CloseDynamicExample(); return 0; }
直接就可以檢測(cè)這個(gè)動(dòng)態(tài)庫(kù)的內(nèi)存情況:
本工具可以檢測(cè)出程序申請(qǐng)了多少內(nèi)存,申請(qǐng)了多少塊內(nèi)存,以及具體哪里發(fā)生了內(nèi)存泄露,可以精確到具體的源代碼位置。
它不僅可以檢測(cè)malloc、free申請(qǐng)和釋放的內(nèi)存,即便是C++的new、delete、new[]、delete[]、std::make_unique、std::make_shared,也可以,不管程序是通過(guò)哪種方式申請(qǐng)和釋放的內(nèi)存,只要發(fā)生了內(nèi)存泄露,工具都可以檢測(cè)到。
整體采用Hook方案,基本流程如圖:
死鎖檢測(cè)
看這段發(fā)生死鎖的代碼:
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; } // 導(dǎo)出的函數(shù),用于創(chuàng)建死鎖場(chǎng)景 static void CreateDeadlock() { pthread_t t1, t2, t3; pthread_attr_t attr; // 初始化線程屬性 pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // 創(chuàng)建分離的線程 pthread_create(&t1, &attr, ThreadFunc1, nullptr); pthread_create(&t2, &attr, ThreadFunc2, nullptr); pthread_create(&t3, &attr, ThreadFunc3, nullptr); // 銷毀線程屬性 pthread_attr_destroy(&attr); // 等待一段時(shí)間讓死鎖發(fā)生 sleep(3); }
從代碼中可以看到,Thread1和Thread2會(huì)發(fā)生死鎖,集成工具后:
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();
結(jié)果如圖:
工具可以檢測(cè)出哪里發(fā)生了死鎖、哪個(gè)線程持有了哪把鎖、以及哪把鎖被哪個(gè)線程持有了。
且無(wú)論你是通過(guò)pthread_lock、還是mutex.lock、還是unique_lock或者lock_guard,只要發(fā)生了死鎖,工具都可以檢測(cè)到,并且可以定位到源代碼位置。
整體也采用Hook方案,流程如圖所示:
項(xiàng)目收獲
項(xiàng)目代碼量不大,核心代碼大概2000行左右,但涉及到的技術(shù)內(nèi)容非常豐富且硬核。
通過(guò)本項(xiàng)目,你可以收獲到:
- 提升C、C++的編碼能力、內(nèi)存管理黑科技、多線程調(diào)試技巧
- ELF 文件結(jié)構(gòu),包括section 和 segment的概念以及具體作用等。
- 編譯鏈接技術(shù),動(dòng)態(tài)鏈接與靜態(tài)鏈接的區(qū)別。
- 動(dòng)態(tài)鏈接與加載,了解動(dòng)態(tài)鏈接器如何在運(yùn)行時(shí)解析符號(hào)和加載動(dòng)態(tài)庫(kù)。
- PLT機(jī)制,與GOT之間的關(guān)系。
- GOT作用,如何存儲(chǔ)動(dòng)態(tài)鏈接的函數(shù)地址。
- 函數(shù)調(diào)用約定,不同架構(gòu)下的函數(shù)調(diào)用約定。
- 內(nèi)存保護(hù)機(jī)制,了解Linux上的內(nèi)存保護(hù)機(jī)制(如DEP、ASLR),以及如何影響代碼注入和鉤子技術(shù)。
- 調(diào)試工具,使用工具(如objdump、gdb)分析二進(jìn)制文件,理解如何定位和修改PLT。
- Hook技術(shù),如何將自定義代碼注入到目標(biāo)進(jìn)程中,以實(shí)現(xiàn)鉤子功能。
- 鉤子的安全性,鉤子技術(shù)是否有風(fēng)險(xiǎn)。
- 編寫(xiě)和測(cè)試,學(xué)習(xí)如何編寫(xiě)鉤子代碼,并在不同環(huán)境中進(jìn)行測(cè)試。
- 鉤子技術(shù)的性能分析。
- 動(dòng)態(tài)庫(kù)的加載過(guò)程,詳細(xì)了解共享庫(kù)的加載過(guò)程,包括如何在運(yùn)行時(shí)解析依賴關(guān)系。
- 符號(hào)解析與重定位,符號(hào)解析的機(jī)制以及重定位表的作用。
- 內(nèi)存管理和分配機(jī)制,內(nèi)存管理機(jī)制,特別是如何安全地分配和修改內(nèi)存以實(shí)現(xiàn)鉤子。
- 內(nèi)存泄漏檢測(cè)技術(shù),理解了內(nèi)存管理分配機(jī)制,可以實(shí)現(xiàn)檢測(cè)內(nèi)存泄漏的能力。
- 鎖機(jī)制,鎖的底層實(shí)現(xiàn)原理,如何實(shí)現(xiàn)加解鎖相關(guān)的鉤子。
- 死鎖檢測(cè)技術(shù),理解了加解鎖的底層機(jī)制,可以實(shí)現(xiàn)檢測(cè)程序是否產(chǎn)生了死鎖。
- 編譯鏈接技術(shù),Debug模式和Release模式的區(qū)別。
- 符號(hào)管理機(jī)制,調(diào)試符號(hào)信息的作用。
- 調(diào)用棧技術(shù),如何獲取線程的調(diào)用堆棧,如何根據(jù)地址解析出對(duì)應(yīng)代碼函數(shù)名和行號(hào)。





