内存泄漏是在没有自动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只能看虚拟区域的地址。
dladdr1()
函数将地址转换为符号信息。函数原型:
#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()和muntrace()函数,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()
无效。
setenv()和unsetenv()函数,改变或添加环境变量。函数原型:
#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;
}
编译的时候会出现警告,系统不推荐使用这样的方法。
代码比较长,为了避免篇幅较长,不利于阅读,这里没有贴上。如果需要,可以联系博主。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。