malloc 与 free 的那些坑:C/C++ 内存管理的常见漏洞与踩坑实录

malloc 与 free 的那些坑:C/C++ 内存管理的常见漏洞与踩坑实录

如果你写过 C/C++,一定对 mallocfree 不陌生。它们是动态内存管理的基石,但也是无数 Bug 的温床。很多内存漏洞并不是“写错了一行代码”,而是在逻辑上看起来合理,运行起来却埋下隐患

这篇文章我们就来系统梳理 malloc / free 使用中的常见内存漏洞,配合代码示例,帮你避开那些“写着写着就踩了”的坑。


一、内存泄漏(Memory Leak)

1. 忘记 free

这是最经典、也最容易被忽视的问题。

void foo() {
    int *p = (int *)malloc(sizeof(int) * 10);
    // 使用 p ...
    // 忘记 free(p)
}

问题:

每次调用 foo() 都会泄漏一块内存。如果这个函数被频繁调用(比如在循环或网络请求中),程序的内存占用会持续增长,最终可能被 OOM Killer 干掉。

正确做法:

void foo() {
    int *p = (int *)malloc(sizeof(int) * 10);
    if (!p) return;

    // 使用 p ...

    free(p);
    p = NULL;  // 防止悬空指针
}

2. 提前 return / goto 导致漏 free

int process() {
    char *buf1 = malloc(1024);
    char *buf2 = malloc(2048);

    if (some_error) {
        return -1;  // buf1 和 buf2 都没释放!
    }

    free(buf1);
    free(buf2);
    return 0;
}

正确做法:使用统一的清理路径

int process() {
    char *buf1 = NULL;
    char *buf2 = NULL;
    int ret = -1;

    buf1 = malloc(1024);
    buf2 = malloc(2048);
    if (!buf1 || !buf2)
        goto cleanup;

    if (some_error)
        goto cleanup;

    ret = 0;
cleanup:
    free(buf1);
    free(buf2);
    return ret;
}

二、悬空指针(Dangling Pointer)

free 后继续使用

int *p = malloc(sizeof(int));
*p = 10;
free(p);
*p = 20;  // 未定义行为!

free 之后,那块内存已经归还给堆管理器,再访问就是未定义行为。可能表现为:

  • 程序直接崩溃(SIGSEGV)
  • 数据被其他变量覆盖,产生诡异 Bug
  • 暂时“看起来正常”,但在特定条件下炸裂

正确做法:free 后置空

free(p);
p = NULL;

虽然不能防止所有错误,但至少后续对 NULL 的解引用会立刻崩溃,方便定位问题。


三、重复释放(Double Free)

char *p = malloc(100);
free(p);
free(p);  // double free!

后果:

Double free 是堆破坏的经典来源,可能导致程序崩溃,也可能被攻击者利用来构造任意地址写入漏洞,是非常危险的安全问题。

正确做法:

free(p);
p = NULL;

// 后续再 free(p) 就是安全的,free(NULL) 是合法的空操作

四、越界访问(Buffer Overflow / Underflow)

写越界

int *arr = malloc(sizeof(int) * 5);
for (int i = 0; i <= 5; i++) {  // i = 5 越界
    arr[i] = i;
}
free(arr);

问题:

越界写入可能覆盖堆管理器的元数据(如 chunk header),导致后续 free 时崩溃,或者引发更难排查的堆破坏问题。

读越界

char *str = malloc(6);
strcpy(str, "hello");
char c = str[6];  // 越界读

读越界虽然不一定立刻崩溃,但可能读到脏数据,破坏程序逻辑。


五、不匹配的分配/释放

malloc / free 混用 new / delete

int *p = (int *)malloc(sizeof(int));
delete p;  // 未定义行为

或反过来:

int *p = new int[10];
free(p);  // 未定义行为

规则:

分配方式释放方式
mallocfree
newdelete
new[]delete[]

混用可能导致:

  • 内存泄漏
  • 析构函数不被调用(C++ 对象)
  • 堆损坏

六、sizeof 的经典陷阱

对指针用 sizeof

int *arr = malloc(sizeof(arr) * 10);  // 错误!

这里 sizeof(arr) 是指针的大小(通常 4 或 8 字节),而不是 int 的大小。

正确写法:

int *arr = malloc(sizeof(*arr) * 10);
// 或者
int *arr = malloc(sizeof(int) * 10);

推荐第一种写法,因为当类型改变时,不需要同步修改 malloc 中的类型。


七、内存未初始化

int *p = malloc(sizeof(int) * 10);
printf("%d\n", p[0]);  // 未初始化,值是垃圾值

对比:

函数是否初始化
malloc
calloc是(清零)

如果你需要清零的内存,用 calloc 更安全:

int *p = calloc(10, sizeof(int));  // 所有元素初始化为 0

八、realloc 的陷阱

1. 直接覆盖原指针

p = realloc(p, new_size);  // 危险!

如果 realloc 失败,返回 NULL,而原来的 p 就丢了,造成内存泄漏。

正确做法:

void *new_p = realloc(p, new_size);
if (!new_p) {
    // 处理错误,p 仍然有效
    free(p);
    return;
}
p = new_p;

2. realloc(0)

p = realloc(p, 0);  // 行为是实现定义的

有些实现会返回 NULL,有些会释放内存并返回可 free 的指针。为了可移植性,避免这样写。


九、释放栈内存

int x;
int *p = &x;
free(p);  // 未定义行为!

free 只能释放 malloc / calloc / realloc 返回的堆内存。释放栈变量、全局变量、字符串字面量都会出问题。


十、结构体中的指针成员

typedef struct {
    char *name;
} Person;

Person *p = malloc(sizeof(Person));
p->name = malloc(64);
// ...
free(p);  // name 指向的内存泄漏了!

正确做法:先释放成员,再释放结构体

free(p->name);
free(p);

十一、多线程下的内存问题

虽然 malloc / free 本身通常是线程安全的,但以下模式仍然危险:

// 线程 A
free(p);

// 线程 B
*p = 10;  // 悬空指针

这类问题需要配合锁或智能指针(C++)来管理生命周期。


十二、防御性编程建议

1. 谁分配,谁释放

明确内存的所有权归属。如果 API 内部 malloc,文档应明确由调用者 free

2. 使用工具检测

工具用途
Valgrind检测内存泄漏、越界、使用未初始化内存
AddressSanitizer编译期插桩,检测堆/栈/全局越界
Electric Fence捕获越界访问

3. C++ 优先使用智能指针

std::unique_ptr<int[]> arr(new int[10]);
// 或
auto arr = std::make_unique<int[]>(10);

RAII 可以彻底避免大多数手动 new/delete 的问题。


总结

mallocfree 看似简单,但坑无处不在。最常见的几类问题可以归纳为:

类型关键词
泄漏忘记 free、提前 return
悬空free 后使用
重复释放double free
越界写/读越界
不匹配malloc/delete 混用
未初始化垃圾值
realloc覆盖原指针

一句话总结:

动态内存管理的核心不是“怎么分配”,而是 “谁拥有、谁释放、何时释放” 。把这三个问题想清楚,大部分坑都能提前避开。

0
0
0
0
评论
未登录
暂无评论