C 语言:释放动态分配的内存,为何还能访问?

2017-06-23 17:06:21 +08:00
 NullMan
#include <stdio.h>
#include <stdlib.h>

#define N 50

int main(void) {
    int *pi;
    pi = (int *)malloc(N * sizeof(int));
    if (pi == NULL) {
        printf("Out of memory!\n");
        exit(1);
    }
    for (int i = 0; i < N; i++) {
        *(pi + i) = 100;
    }
    free(pi);
    pi[10] = 200;
    printf("%d\n", pi[10]); // 输出 200
    return 0;
}

执行 free(pi) 了,没道理 pi 还能访问。我看了下 c 标准,发现有这么一句话:

The behavior is undefined if after free() returns, an access is made through the pointer ptr (unless another allocation function happened to result in a pointer value equal to ptr)

这到底释放了内存没有?我是这么猜想的,这块内存其实回归了内存池,如果有其他的内存分配,将有可能复用这free 过的内存块。先前代码输出的200, 其实等同于垃圾值,就像声明了一个int i但未初始化而直接访问i将会得到上次使用过i内存的垃圾值。

不知我理解是否正确?

编译器版本如下:

Apple LLVM version 8.1.0 (clang-802.0.42)
Target: x86_64-apple-darwin16.6.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

本人初学 C,望指点!多谢多谢!

4887 次点击
所在节点    C
67 条回复
wind3110991
2017-06-24 15:37:54 +08:00
https://tanronggui.xyz/t/181639#reply21

2 年前我也问了同样的问题,看 1 楼和 18 楼的回答了
实际上 free 后 pi 变为了野指针了,不做 NULL 赋值容易产生异常,所以需要将指针地址置为 NULL

内存回收和空间分配一样,涉及到两块空间:虚拟空间与物理空间。虚拟空间是进程私有的;
当你 free 之后,虚拟空间中的内存已经回收了。但是物理内存上没有啊,XJB 解释的话,可以理解为物理内存上某个部分“依然没有去掉这块内容”。

而且你的指针持有了那个内存地址,当实际占用大小不满一个 page 的时候,页置换是不会发生的。

总而言之,谨记:当你 free 后,一定要记得置 pi 为 NULL,这个我们称之为防御性的代码,当你不复用这个 pi 指针还好,但是如果复用了,会出现不可预估的错误和异常
Abercrombie
2017-06-24 15:42:03 +08:00
上面大家说的已经差不多解释清楚了,我来说一点这个机制的应用吧。
我在 real time 系统上开发程序时,由于对程序的实时性要求很高,所以容不下一点错误的发生。
除了提高 cpu affinity,为了防止出现 page fault,所以会做一步 stack prefault,提前先申请一块较大的内存,然后释放,那么接下来程序会优先使用这块已释放掉的内存,保证了不会出现 page fault。这个应该属于 glibc 的实现机制
```
void stack_prefault(MAX_STACK_SIZE)
{
unsigned char *p;
p = (unsigned char*)malloc(MAX_STACK_SIZE);
memset(p, 0, MAX_STACK_SIZE);
free(p);
}
```
如果不是因为 free 掉之后的内存能够被快速再利用的这个机制,那么也无法实现 prefault 的效果了。
ycz0926
2017-06-24 16:32:36 +08:00
你的代码就躺在这个进程的内存上,你的代码和内存上的数据没有任何区别,你把指针移来移去,加载了再多的库,也分割不了你的 c 代码和内存的“粘性”
geelaw
2017-06-24 19:09:03 +08:00
@bp0

这个方法的可移植性 和 程序的可移植性 没什么关系,因为这个玩意儿是给 **调试** 用的。正常使用(而不是调试程序)的时候不会应用这个策略。

这里的“让它 crash ”是在调试阶段暴露问题,在生产阶段,因为一些优化的考虑,不可能做这么多防御措施。

如果你在书写可移植的代码,那么你可以在一个有这个调试功能的平台上完成调试之后到其他平台用。


@VYSE

(所以 0xdeadbeef 不能 mmap ?我不用 *nix 不太懂。)


@VYSE
@wind3110991

如果有两个指针指向同一个动态分配地址,那么仅仅设置一个指针为 nullptr 或者一个特殊的数是没有用的。

这种情况或许容易检测,但是更困难的是,你分配了一个数组,两个指针一个指向数组开头,一个指向数组里的另一个位置,然后你 free 数组开头并重置指针——这样虽然后一个指针并不等于被释放的指针,仍然变得无效。
jmp2x
2017-06-25 00:10:51 +08:00
1. 上面说的很多大多数都是猜测
2. 这部分内容属于 ptmalloc 堆内存分配内容,涉及到 ptmalloc 的缓存结构,分配 /释放算法
3. 需要考虑几个问题,分配的 malloc 堆块到底是以怎么样的结构体存在? glibc 怎么(记录)分配 malloc 堆块? glibc 怎么(回收)释放 malloc 堆块? 这几个问题都搞不清怎么谈
4. 源码地址: http://www.eglibc.org/cgi-bin/viewvc.cgi/branches/eglibc-2_19/libc/malloc/malloc.c?view=markup
VYSE
2017-06-25 03:00:55 +08:00
@geelaw #64 常见 OS 的内核地址范围,所以用户态不可 mmap,做安全的常用测试让 dangling 指针引发 crash.
https://en.wikipedia.org/wiki/Hexspeak

首先这是个安全问题,导致 crash 是最理想的情况,导致程序输出异常该庆幸,导致代码执行就杯具了.
举个例子, PingPongRoot 里的利用的内核 free 后置指针为 0x200200,结果用户态可以 mmap,然后 UAF....
这个问题还是 dangling pointer 能不能被再次使用,比如上面洞 Linux 的解法就是置 NULL 指针和使用前判断是否为 NULL.
假如仅仅置 NULL 指针而缺了后者,在 mmap_min_addr 配置为 0 的内核上仍然存在漏洞.
见 LINUS 解释为什么不限制 mmap 0 http://yarchive.net/comp/linux/address_zero.html
kljsandjb
2018-03-25 17:54:32 +08:00
@NullMan 释放了,可后来你又用了,所以用的这块 int 数据占用的空间

释放之后指针置 NULL 吧 :)

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://tanronggui.xyz/t/370623

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX