本文分享自華為雲社群《從源頭解決記憶體漏失問題:全面解析記憶體漏失檢測與修復技術》,作者 Lion Long 。
記憶體漏失是在沒有自動gc的程式語言裡面經常發生的問題;因為沒有gc,所以分配的記憶體需要程式自己呼叫釋放。其核心是呼叫分配與釋放沒有符合開閉原則,沒有配對,形成了有分配,沒有釋放的指標,從而產生了記憶體漏失。
例如:
void func(size_t s1) { void p1=malloc(s1); void p2=malloc(s1); // ... free(p1); }
以上程式碼段,分配了兩個s1大小的記憶體塊,由 p1 與 p2 指向。而程式碼塊執行完以後,釋放了 p1,而 p2 沒有釋放。形成了有分配沒有釋放的指標,產生了記憶體漏失。
隨著工程程式碼量越來越多,記憶體漏失的排查就變得極為頭疼,一個程式,其虛擬記憶體一直在增長,無法準確判斷是程式需要還是記憶體漏失。有分配沒釋放,自然使程序堆的記憶體越來越少,直到耗盡。會造成後面的執行程式碼不能成功分配記憶體。甚至使程式奔潰。
記憶體漏失是沒有自動 gc 的程式語言所產生的,解決方案一,引入 gc。這是根治記憶體漏失的最好的方案。但是這樣的方案有失去了 c/c++語言的優勢。方案二,當發生記憶體漏失的時候,能夠精準的定位程式碼哪一行所引起的。這也是實現記憶體漏失檢測的核心實現需求。
(1)能夠檢測出來記憶體漏失。
(2)能夠判斷是由程式碼哪一行引起的記憶體漏失。
一個程式,其虛擬記憶體一直在增長,無法準確判斷是程式需要還是記憶體漏失;如果是記憶體漏失也不知道是發生在哪一行程式碼。
將地址轉換為檔名和行號。
addr2line [-a|--addresses] [-b bfdname|--target=bfdname] [-C|--demangle[=style]] [-e filename|--exe=filename] [-f|--functions] [-s|--basename] [-i|--inlines] [-p|--pretty-print] [-j|--section=name] [-H|--help] [-V|--version] [addr addr ...]
描述:
addr2line將地址轉換為檔名和行號。給定可執行檔案中的地址或可重定位物件部分中的偏移量,它使用偵錯資訊確定與之關聯的檔名和行號。
要使用的可執行或可重定位物件是用-e選項指定的。預設為檔案a.out。要使用的可重定位物件中的節是用-j選項指定的。
addr2line有兩種操作模式。
注意:
addr2line是將地址轉換為檔案號,而檔案是儲存在磁碟中的,程式執行的地址是在虛擬記憶體裡面的(程式碼段),在高版本Linux 中可能無法解析出地址在檔案哪個位置。addr2line只能看虛擬區域的地址。
將地址轉換為符號資訊。函數原型:
#define _GNU_SOURCE #include <dlfcn.h> int dladdr(void *addr, Dl_info *info); int dladdr1(void *addr, Dl_info *info, void **extra_info, int flags); // Link with -ldl.
描述:
函數dladdr()
確定addr中指定的地址是否位於呼叫應用程式載入的一個共用物件中。如果是,則dladdr()
返回與addr重疊的共用物件和符號的資訊。此資訊以Dl_info
結構返回:
typedef struct { const char *dli_fname; /* Pathname of shared object that contains address */ void *dli_fbase; /* Base address at which shared object is loaded */ const char *dli_sname; /* Name of symbol whose definition overlaps addr */ void *dli_saddr; /* Exact address of symbol named in dli_sname */ } Dl_info;
函數dladdr1()
類似於dladdr()
,但通過引數extra_info
返回附加資訊。返回的資訊取決於標誌中指定的值,標誌可以具有以下值之一:
(1)RTLD_DL_LINKMAP
。獲取指向匹配檔案的連結對映的指標。extra_info引數指向<link.h>
中定義的link_map結構(即struct link_map**
)的指標。
struct link_map { ElfW(Addr) l_addr; /* Difference between the address in the ELF file and the address in memory */ char *l_name; /* Absolute pathname where object was found */ ElfW(Dyn) *l_ld; /* Dynamic section of the shared object */ struct link_map *l_next, *l_prev; /* Chain of loaded objects */ /* Plus additional fields private to the implementation */ };
(2)RTLD_DL_SYMENT
。獲取指向匹配符號的ELF符號表條目的指標。extra_info
引數是指向符號指標的指標:const ElfW(Sym)**
。ElfW()
宏定義將其引數轉換為適合硬體體系結構的ELF資料型別的名稱。例如,在64位元平臺上,ElfW(Sym)
生成資料型別名稱Elf64_Sym
,該名稱在<elf.h>
中定義:
typedef struct { Elf64_Word st_name; /* Symbol name */ unsigned char st_info; /* Symbol type and binding */ unsigned char st_other; /* Symbol visibility */ Elf64_Section st_shndx; /* Section index */ Elf64_Addr st_value; /* Symbol value */ Elf64_Xword st_size; /* Symbol size */ } Elf64_Sym;
封裝:
void * ConvertToElf(void *addr) { Dl_info info; struct link_map *link; dladdr1(addr, &info, (void **)&link, RTLD_DL_LINKMAP); // 偏差糾正 return (void *)((size_t)addr - link->l_addr); }
內 存 洩 漏 是由於 內 存 分 配 與 內 存 釋放, 不 匹 配 所引起 的 。 對 內 存 分 配 函 數malloc/calloc/realloc,以及記憶體釋放 free 進行「劫持」hook。能夠統計出記憶體分配的位置,記憶體釋放的位置,從而判斷是否匹配。
mtrace跟蹤紀錄檔。函數原型:
#include <mcheck.h> void mtrace(void); void muntrace(void);
描述:
mtrace()函數為記憶體分配函數安裝hook(勾點)函數【malloc()、realloc()、memalign(),free()】。這些hook函數記錄有關記憶體分配和釋放的跟蹤資訊。跟蹤資訊可用於發現記憶體漏失,並嘗試釋放程式中未分配的記憶體。
muntrace()
函數禁用mtrace()
安裝的hook函數,這樣就不再為記憶體分配函數記錄跟蹤資訊。如果mtrace()
沒有成功安裝任何勾點函數,則muntrace()
不執行任何操作。
呼叫mtrace()
時,它會檢查環境變數MALLOC_TRACE
的值,該變數應包含要記錄跟蹤資訊的檔案的路徑名。如果路徑名成功開啟,則其長度將被截斷為零。
如果未設定MALLOC_TRACE
,或者它指定的路徑名無效或不可寫,則不會安裝hook函數,並且mtrace()
無效。在set-user-ID
和 set-group-ID
程式中,MALLOC_TRACE
被忽略,mtrace()
無效。
改變或新增環境變數。函數原型:
#include <stdlib.h> int setenv(const char *name, const char *value, int overwrite); int unsetenv(const char *name); /* Feature Test Macro Requirements for glibc (see feature_test_macros(7)): setenv(), unsetenv(): _BSD_SOURCE || _POSIX_C_SOURCE >= 200112L || _XOPEN_SOURCE >= 600 */
描述:
如果名稱不存在,setenv()函數會將變數name新增到值為value的環境中。如果環境中確實存在name,且overwrite非零,則其值將更改為value;如果overwrite為零,則name的值不變(setenv()返回成功狀態)。此函數複製name和value指向的字串(與putenv(3)相反)。
unsetenv()函數用於從環境中刪除變數名。如果環境中不存在名稱,則函數成功,環境不變。
返回值:
setenv()函數在成功時返回零,在錯誤時返回-1,並設定errno以指示錯誤的原因。
unsetenv()函數在成功時返回零,在錯誤時返回-1,並設定errno以指示錯誤的原因。
錯誤:
錯誤碼 | 含義 |
---|---|
EINVAL | name為NULL,指向長度為0的字串,或包含「=」字元。 |
ENOMEM | 記憶體不足,無法向環境中新增新變數。 |
(1)在呼叫記憶體分配函數之前呼叫mtrace();
(2)在程式結束或者不需要追蹤記憶體漏失的地方呼叫muntrace();
(3)設定環境變數MALLOC_TRACE的值(setenv函數或者export命令);
(4)編譯時要帶上-g引數。
(5)當出現記憶體漏失時,使用addr2line工具定位記憶體漏失的位置。
$ addr2line -f -e memleak -a 0x4006b8
範例中memleak是程式名,0x4006b8是記憶體漏失的地址。
例如:
$ cc -g t_mtrace.c -o t_mtrace $ export MALLOC_TRACE=/tmp/t $ ./t_mtrace $ mtrace ./t_mtrace $MALLOC_TRACE
#include <stdio.h> #include <stdlib.h> #include <mcheck.h> int main(int argc,char **argv) { setenv("MALLOC_TRACE", "./mem.txt", 1); mtrace(); void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); muntrace(); unsetenv("MALLOC_TRACE"); return 0; }
記憶體漏失檢測檔案內容:
$ cat mem.txt = Start @ ./memleak:[0x4006b8] + 0x1886580 0xa @ ./memleak:[0x4006c6] + 0x18865a0 0x14 @ ./memleak:[0x4006d4] + 0x18865c0 0x1e @ ./memleak:[0x4006e4] - 0x18865c0 @ ./memleak:[0x4006f0] - 0x18865a0 = End
定位記憶體漏失位置:
$ addr2line -f -e memleak -a 0x4006b8 0x00000000004006b8 main memleak.c:13
Linux中有兩個宏__FILE__
、__func__
和__LINE__
,分別指示當前的檔名、函數名和行號,利用宏定義封裝記憶體分配函數和釋放函數。
#define malloc(size) _malloc(size,__FILE__,__LINE__) #define free(p) _free(p,__FILE__,__LINE__)
自己在_malloc函數和_free函數中呼叫malloc函數和free函數,並且做一些操作。
前提:宏一定要放在記憶體分配之前定義,這樣預編譯階段才會替換malloc為我們自己實現的_malloc
和_free
。
範例程式碼:
#include <stdio.h> #include <stdlib.h> void *_malloc(size_t size,const char*filename,int line) { void *p = malloc(size); printf("[+] %s : %d, %p\n", filename, line,p); return p; } void _free(void *p, const char*filename, int line) { printf("[-] %s : %d, %p\n", filename, line,p); return free(p); } #define malloc(size) _malloc(size,__FILE__,__LINE__) #define free(p) _free(p,__FILE__,__LINE__) int main(int argc,char **argv) { void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); return 0; }
採用宏定義方法的優缺點:
(1)優點,實現簡單。
(2)缺點,只適合單檔案,宏定義要放在檔案的最前面。
使用檔案替換列印:
程式執行時總是列印不必要的資訊即影響效率也不美觀,可以在一個資料夾裡建立、刪除檔案來統計記憶體漏失。
將指標值作為檔名,分配記憶體建立檔案,釋放記憶體刪除檔案,在檔案裡面記錄分配記憶體的檔名和行號。
如果資料夾裡有檔案則存在記憶體漏失,沒有檔案就代表沒有記憶體漏失。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #define LEAK_FILE_PATH "./mem/%p.mem" void *_malloc(size_t size,const char*filename,int line) { void *p = malloc(size); //printf("[+] %s : %d, %p\n", filename, line,p); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, p); FILE *fp = fopen(buff, "w"); fprintf(fp, "[+] %s : %d, addr: %p, size: %ld\n", filename, line, p, size); fflush(fp);//重新整理資料到檔案中 fclose(fp); return p; } void _free(void *p, const char*filename, int line) { //printf("[-] %s : %d, %p\n", filename, line,p); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, p); if (unlink(buff) < 0) { printf("double free %p\n", p); return; } return free(p); } #define malloc(size) _malloc(size,__FILE__,__LINE__) #define free(p) _free(p,__FILE__,__LINE__) int main(int argc,char **argv) { void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); return 0; }
注意,工具只能加快分析,不能100%確定記憶體漏失,因為複雜的系統情況比較複雜。
檢測記憶體漏失不是一開始就加上,一般通過」熱更新「的方式在需要的時候開啟,即在組態檔中有一個開啟記憶體漏失檢測的標誌位。只有需要的時候才開啟,這樣不影響程式效率。
hook使用步驟:
(1)定義函數指標。
typedef void *(*malloc_t)(size_t size); typedef void(*free_t)(void *p); malloc_t malloc_f = NULL; free_t free_f = NULL;
(2)函數實現,函數名與目標函數名一致。
void *malloc(size_t size) { // ... } void free(void *ptr) { // ... }
(3)初始化hook,呼叫dlsym()。
static init_hook() { if (malloc_f == NULL) malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc"); if (free_f == NULL) free_f = (malloc_t)dlsym(RTLD_NEXT, "free"); }
注意:
hook的時候,要考慮其他函數也用到所hook住的函數,比如在printf()函數裡面也呼叫了malloc,那麼就需要防止內部遞迴進入死迴圈。
例如:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <dlfcn.h> typedef void *(*malloc_t)(size_t size); typedef void(*free_t)(void *p); malloc_t malloc_f = NULL; free_t free_f = NULL; void *malloc(size_t size) { printf("malloc size: %ld", size); return NULL; } void free(void *ptr) { printf("free: %p\n",ptr); } static int init_hook() { if (malloc_f == NULL) malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc"); if (free_f == NULL) free_f = (free_t)dlsym(RTLD_NEXT, "free"); return 0; } int main(int argc,char **argv) { init_hook(); void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); return 0; }
以上程式碼會出現段錯誤,使用gdb偵錯會發現在malloc函數的printf()呼叫進入了無限遞迴;棧溢位。
解決方案就是新增標誌。
gcc內建功能:void * __builtin_return_address(無符號整數級別)
此函數返回當前函數或其呼叫方之一的返回地址。引數是向上掃描呼叫堆疊的幀數。值 產生當前函數的返回地址,值 生成當前函數呼叫方的返回地址,依此類推。內聯預期行為時,函數返回返回的函數的地址。若要變通解決此問題,請使用函數屬性。level:
該引數必須是常數整數。
在某些計算機上,可能無法確定除當前功能之外的任何函數的返回地址;在這種情況下,或者當到達堆疊的頂部時,此函數返回未指定的值。此外,可用於確定是否已到達堆疊的頂部。
範例程式碼:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> #define LEAK_FILE_PATH "./mem/%p.mem" #include <dlfcn.h> static int enable_malloc_hook = 1; static int enable_free_hook = 1; typedef void *(*malloc_t)(size_t size); typedef void(*free_t)(void *p); malloc_t malloc_f = NULL; free_t free_f = NULL; void *malloc(size_t size) { void *p; if (enable_malloc_hook) { enable_malloc_hook = 0; p = malloc_f(size); printf("malloc size: %ld,p=%p\n", size,p); // 獲取上一層呼叫malloc地方的地址,這個地址用於addr2line工具將其轉換為行號 void *caller = __builtin_return_address(0); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, p); FILE *fp = fopen(buff, "w"); fprintf(fp, "[+] %p , addr: %p, size: %ld\n", caller, p, size); fflush(fp);//重新整理資料到檔案中 fclose(fp); enable_malloc_hook = 1; } else p = malloc_f(size); return p; } void free(void *p) { if (enable_free_hook) { enable_free_hook = 0; //printf("free: %p\n",p); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, p); if (unlink(buff) < 0) { printf("double free %p\n", p); //enable_free_hook = 1; free_f(p); return; } free_f(p); enable_free_hook = 1; } else free_f(p); } static int init_hook() { if (malloc_f == NULL) malloc_f = (malloc_t)dlsym(RTLD_NEXT, "malloc"); if (free_f == NULL) free_f = (free_t)dlsym(RTLD_NEXT, "free"); return 0; } int main(int argc,char **argv) { init_hook(); void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); return 0; }
通過__builtin_return_address(0)
得到的地址需要addr2line工具將其轉換為檔案行號即可定位記憶體漏失的位置。
思路和hook的一樣,因為malloc和free底層呼叫的也是__libc_malloc
和__libc_free
。
範例程式碼:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> // 要記得手動建立一個mem資料夾 #define LEAK_FILE_PATH "./mem/%p.mem" extern void *__libc_malloc(size_t size); extern void __libc_free(void *p); static int enable_malloc_hook = 1; void *malloc(size_t size) { void *p; if (enable_malloc_hook) { enable_malloc_hook = 0; p = __libc_malloc(size); printf("malloc size: %ld,p=%p\n", size, p); // 獲取上一層呼叫malloc地方的地址,這個地址用於addr2line工具將其轉換為行號 void *caller = __builtin_return_address(0); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, p); FILE *fp = fopen(buff, "w"); fprintf(fp, "[+] %p , addr: %p, size: %ld\n", caller, p, size); fflush(fp);//重新整理資料到檔案中 fclose(fp); enable_malloc_hook = 1; } else p = __libc_malloc(size); return p; } void free(void *p) { char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, p); if (unlink(buff) < 0) { printf("double free %p\n", p); } __libc_free(p); } int main(int argc,char **argv) { void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); return 0; }
這種方式適用於比較老的Linux 版本,屬於舊版本的API,__malloc_hook
是指標的方式,是一個固定的值。本質上也是一種hook技術。
函數原型:
#include <malloc.h> void *(*__malloc_hook)(size_t size, const void *caller); void *(*__realloc_hook)(void *ptr, size_t size, const void *caller); void *(*__memalign_hook)(size_t alignment, size_t size, const void *caller); void (*__free_hook)(void *ptr, const void *caller); void (*__malloc_initialize_hook)(void); void (*__after_morecore_hook)(void);
描述:
GNUC庫允許您通過指定適當的勾點函數來修改malloc、realloc和free的行為。例如,您可以使用這些掛鉤來幫助偵錯使用動態記憶體分配的程式。
變數__malloc_initialize_hook指向在初始化malloc實現時呼叫一次的函數。這是一個弱變數,因此可以在應用程式中使用如下定義覆蓋它:
void(*__malloc_initialize_hook)(void)=my_init_hook();
現在函數my_init_hook()可以初始化所有勾點。
__malloc_hook
、__realloc_hooks
、__memalign_hooke
、__free_hooky
指向的四個函數的原型分別與函數malloc、realloc和memalign。
方案:
交換法,自定義函數指標,實現具體函數,將自己實現的函數與系統提供的__malloc_hook交換。
範例程式碼:
#define _GNU_SOURCE #include <stdio.h> #include <stdlib.h> #include <unistd.h> // 要記得手動建立一個mem資料夾 #define LEAK_FILE_PATH "./mem/%p.mem" #include <malloc.h> /* typedef void *(*malloc_t)(size_t size); typedef void(*free_t)(void *p); malloc_t malloc_f = NULL; free_t free_f = NULL; */ static int enable_malloc_hook = 1; static void my_init_hook(void); static void *my_malloc_hook(size_t, const void *); static void my_free_hook(void *, const void *); /* Variables to save original hooks. */ static void *(*old_malloc_hook)(size_t, const void *); static void(*old_free_hook)(void *, const void *); /* Override initializing hook from the C library. */ void(*__malloc_initialize_hook) (void) = my_init_hook; static void my_init_hook(void) { old_malloc_hook = __malloc_hook; __malloc_hook = my_malloc_hook; old_free_hook = __free_hook; __free_hook = my_free_hook; } static void * my_malloc_hook(size_t size, const void *caller) { void *result; /* Restore all old hooks */ __malloc_hook = old_malloc_hook; /* Call recursively */ //result = malloc(size); if (enable_malloc_hook) { enable_malloc_hook = 0; result = malloc(size); /* printf() might call malloc(), so protect it too. */ printf("malloc(%u) called from %p returns %p\n", (unsigned int)size, caller, result); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, result); FILE *fp = fopen(buff, "w"); fprintf(fp, "[+] %p , addr: %p, size: %ld\n", caller, result, size); fflush(fp);//重新整理資料到檔案中 fclose(fp); enable_malloc_hook = 1; } else result = malloc(size); /* Save underlying hooks */ old_malloc_hook = __malloc_hook; /* Restore our own hooks */ __malloc_hook = my_malloc_hook; return result; } static void my_free_hook(void *ptr, const void *caller) { __free_hook = old_free_hook; free(ptr); old_free_hook = __free_hook; /* printf() might call malloc(), so protect it too. */ printf("free(%p) called from %p\n", ptr, caller); char buff[128] = { 0 }; sprintf(buff, LEAK_FILE_PATH, ptr); if (unlink(buff) < 0) { printf("double free %p\n", ptr); } /* Restore our own hooks */ __free_hook = my_free_hook; } int main(int argc,char **argv) { my_init_hook(); void *p1 = malloc(10); void *p2 = malloc(20); void *p3 = malloc(30); free(p3); free(p2); return 0; }
編譯的時候會出現警告,系統不推薦使用這樣的方法。
程式碼比較長,為了避免篇幅較長,不利於閱讀,這裡沒有貼上。如果需要,可以聯絡博主,或者關注微信公眾號 《Lion 萊恩呀》 獲取。