Fork me on GitHub
0%

内存溢出与内存泄漏

区别

  • 内存溢出

    内存溢出是指程序在运行过程中尝试使用超过其分配内存的范围的内存空间。这可能导致程序崩溃或产生不可预测的行为。内存溢出通常发生在程序尝试写入超过分配内存范围的数据时,这可能是由于数组越界、指针错误等原因引起的。比如:数组越界访问;指针错误,尝试访问未分配或已释放的内存。

  • 内存泄漏

    内存泄漏是指程序在运行过程中未能正确释放已经不再需要的内存,导致系统中的可用内存逐渐减少,最终可能耗尽系统的可用内存资源。内存泄漏通常发生在程序中的某个部分未能释放动态分配的内存,导致这部分内存无法再被程序访问,但仍然占据系统的内存空间。比如:未使用释放内存的函数(如freedelete);循环引用;未关闭的文件句柄等资源。

内存溢出解决方案

  • 数组越界检查并合理使用指针。
  • 内存越界检查工具: 使用内存越界检查工具或编译器选项,以检测潜在的内存溢出问题。
  • 使用安全的库函数: 使用strcpy替换strncpy,使用sprtintf替换snprintf。
  • 防御性编程:在代码中使用防御性编程的技术,例如在删除指针后将其置为nullptr,以避免悬空指针引用。

内存泄漏解决方案

这部分主要针对堆空间malloc之后没有free造成的内存泄漏提供的解决方案。其核心思想都是使用自定义的malloc函数来替换掉库函数,使我们能够检测到哪块内存是没有被释放的。

github代码地址

宏定义

宏定义方式适用于单个文件检测,使用静态连接的方式进行检测。通过自定义malloc、free函数,使用创建文件的机制来确认哪里出现的内存泄漏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#define STORE_PATH "memleakStore"
void* my_malloc(size_t size, const char* filename, const char* func, int line)
{
char buffer[128] = {0};
const char* storepath = STORE_PATH;
if(access(storepath, F_OK) != 0) {
mkdir(storepath,0755);
}

void* ptr = malloc(size);
if(ptr != NULL) {
snprintf(buffer, sizeof(buffer), "%s/%p.mem", storepath, ptr);
FILE* fp = fopen(buffer, "w");
memset(buffer, 0, sizeof(buffer));
snprintf(buffer, sizeof(buffer), "[+] (%s %s %d) (%p + %ld)\n", filename, func, line, ptr, size);
fwrite(buffer, sizeof(char), sizeof(buffer), fp);
fclose(fp);
}
return ptr;
}

void my_free(void* ptr, const char* filename, const char* func, int line)
{
char buffer[128] = {0};
const char* storepath = STORE_PATH;
if(access(storepath, F_OK) != 0) {
printf("%s not exist. Please Check \n", storepath);
return;
}

snprintf(buffer, sizeof(buffer), "%s/%p.mem", storepath, ptr);
if(unlink(buffer) != 0) {
printf("Double Free Error\n");
FILE* fp = fopen(buffer, "w");
memset(buffer, 0, sizeof(buffer));
snprintf(buffer, sizeof(buffer), "[-] (%s %s %d) (%p)\n", filename, func, line, ptr);
fwrite(buffer, sizeof(char), sizeof(buffer), fp);
fclose(fp);
return ;
}
free(ptr);
}

#define malloc(size) my_malloc(size, __FILE__,__func__,__LINE__)
#define free(ptr) my_free(ptr, __FILE__,__func__,__LINE__)

Hook

Hook是使用LD_PRELOAD机制,使用dlsym采用动态连接的方式,重定义malloc、free来定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#define _GNU_SOURCE        
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>

typedef void (*glibc_free)(void* ptr);
typedef void* (*glibc_malloc)(size_t size);

static glibc_malloc gmalloc = NULL;
static glibc_free gfree = NULL;

static void hook_init(void)
{
if(gmalloc == NULL) {
gmalloc = dlsym(RTLD_NEXT, "malloc");
if (!gmalloc) {
fprintf(stderr, "unable to get malloc symbol!\n");
exit(1);
}
}

if(gfree == NULL) {
gfree = dlsym(RTLD_NEXT, "free");
if (!free) {
fprintf(stderr, "unable to get free symbol!\n");
exit(1);
}
}
fprintf(stderr, "malloc and free successfully wrapped\n");
}
void* malloc(size_t size)
{
void* caller = __builtin_return_address(1);
if(gmalloc == NULL) {
hook_init();
}

void* ptr = gmalloc(size);
fprintf(stderr, "[+] caller %p malloc from %p size %ld\n", caller, ptr, size);
// 如果打印,一定要用fprintf(stderr),否则会产生无限循环,因为fprintf(stdout)也会使用malloc!

return ptr;
}

void free(void* ptr)
{
void* caller = __builtin_return_address(1);
if(gfree == NULL) {
hook_init();
}

gfree(ptr);
fprintf(stderr, "[-] caller %p free from %p\n", caller, ptr);
}

bpftrace

bfptrace官方建议linux内核版本为4.9或以上。具体使用方式需要进一步查看相关资料

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 挂载点
uprobe:/lib/x86_64-linux-gnu/libc.so.6:malloc
# 判断条件是否发生(进程是否==memleak)
/comm == "memleak"/
# action动作
{
printf("malloc\n");
}

uprobe:/lib/x86_64-linux-gnu/libc.so.6:free
/comm == "memleak"/
{
printf("free\n");
}

LD_PRELOAD

LD_PRELOAD 是一个在 Linux 系统中用于预加载共享库的环境变量。通过设置 LD_PRELOAD,可以在运行时加载指定的共享库,覆盖程序默认加载的库,从而影响程序的行为。这在某些情况下可以用于修改或增强程序的行为,进行调试,或者实现一些特殊的需求。

在dlsym的使用过程中_GNU_SOURCE 是一定要添加的,因为RTLD_NEXT 宏依赖于这个宏。RTLD_NEXT 就是告诉 ld-linux.so 不要在当前文件中找 free 这个符号,而是要按照动态库的搜索顺序找到下一个动态库,并在它里面寻找 free 函数,实际上,这里找到的就是 glibc 里的 free 函数了。

note:

需要注意的是,在实现malloc函数的时候,里面的打印函数不要使用printf,因为printf里面也调用了malloc,这就导致无限递归最终stackoverflow。所以打印方式使用fprintf(sterr,xxx)即可

参考资料