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,望指点!多谢多谢!

4833 次点击
所在节点    C
67 条回复
catror
2017-06-23 20:38:24 +08:00
@geelaw 内存都释放了,后面有逻辑逻辑涉及到释放掉的内存就出错了
geelaw
2017-06-23 20:43:16 +08:00
@catror

举个例子来说,正是因为这里的指针没有意义了,下次使用就可能出错(有些调试器允许你检测到这种情况),所以这样可以看出程序的逻辑错误。在 free 之后无论是什么情况都立刻把指针设置为 nullptr 是一种鸵鸟战术,就像是在所有指针访问之前都测试是不是 nullptr 一样——对于本来不该是 nullptr 的指针,测试是不是 nullptr 并不能解释为什么这个指针变成了 nullptr。
azh7138m
2017-06-23 20:46:14 +08:00
@catror 如果出现两次 free 同一个地址,是你逻辑有问题,这个时候应该让问题暴露出来,而不是置 null 来规避问题
catror
2017-06-23 21:11:24 +08:00
@azh7138m @geelaw 使用空指针,free 空指针都会出错啊,不知道我是不是理解错你们的意思了,不过你们应该去搜一下野指针的危害…
catror
2017-06-23 21:21:30 +08:00
搜了一下,我上面说错了,这种叫悬垂指针,我搞错概念了,不叫野指针,野指针是没有初始化的指针,这两个都是 C 语言里面很经典的东西,同时也是能坑死人的东西
geelaw
2017-06-23 21:41:24 +08:00
@catror free(nullptr) 是没有任何效果的,不要想当然。
geelaw
2017-06-23 21:42:01 +08:00
@catror 没有区分 dangling pointer 和 uninitialised pointer 的必要——它们都存储着无效的值。
skadi
2017-06-23 21:42:37 +08:00
未定义行为
catror
2017-06-23 21:53:59 +08:00
@geelaw 噢噢,这倒没注意,一般内存释放前我也会判空,因为我无法保证下次换个其他的 malloc 库也会处理这种异常…
bp0
2017-06-23 22:17:22 +08:00
@geelaw 将 free 后的指针赋值成 NULL 的意义就是在于下面没有检查直接对指针进行读写操作。对指向 NULL 的指针进行读写操作会导致段错误。而如果没有将 free 后的指针赋值成 NULL,其会继续指向 malloc 时返回的地址。因为这个地址还是有效的,如果下面继续对其进行读写操作,是无法马上发现问题的。

尤其是涉及到多线程时,这个地址可能会被其他线程使用,而导致另外的线程异常。如果你只是排查另外线程的代码,是无法发现问题的根本原因的。
glogo
2017-06-23 22:31:35 +08:00
free 之后内存并不一定会被马上回收,马上回收也不一定保证帮你清零,而且 你的 free 之后并没有把相应的指针置为 NULL
geelaw
2017-06-23 22:49:28 +08:00
@bp0 如果你开启页末对齐,则你并不需要赋值为 nullptr 也能看出错误。参考 https://blogs.msdn.microsoft.com/oldnewthing/20170410-00/?p=95935

如果你设置为 nullptr,你就没法发现双重释放的错误。
ogfa
2017-06-23 23:21:51 +08:00
所以应该 free 过之后置为 1
这样双重 free 会出错,再次读写也出错
想判断是否 free 过也 ok,因为永远不会 malloc 出 1 的地址来
堪称最完美解决方案
tyfulcrum
2017-06-23 23:29:23 +08:00
@geelaw 如果 free 后置空和判断 nullptr 不是好的思路,那用什么思路比较好呢?这方面有什么比较好的资料推荐么?
bp0
2017-06-23 23:39:05 +08:00
@geelaw 你给的资料上面说的如何马上检查出是否访问超过分配大小的地址空间,而楼主是问为什么释放申请的内存后还能访问。 只要我继续访问申请的大小,页末对齐的方法就无法检查出来。因为我访问的就是 malloc 分配给我的地址。而且很怀疑这种方法的实用性,分配 1 个字节的空间也要占用一个页吗?如果没记错,一个页应该有 4kbytes 吧。

双重释放的问题确实无法有效解决,不过个人认为,修改已经释放的内存中的内容是比双重释放更严重的 bug。
geelaw
2017-06-23 23:50:37 +08:00
@bp0 如果你认真读了那篇内容你会发现释放之后再访问也会出错,因为页面已经被 decomitted。这是一个调试功能,所以效率低一点并没有什么问题。

双重释放会导致堆损坏,而且双重释放是会导致访问已经释放的内存的——比如第一次释放后那个地址被分配给别人之后再第二次释放,实际拥有者不知道这个内存已经被扔了,再次访问就会有坏事发生。
geelaw
2017-06-23 23:52:18 +08:00
@tyfulcrum 取决于逻辑,不能一概而论,所以我说“任何情况(也就是不考虑具体情况)都在 free 之后立刻设置为 nullptr ”是不好的。
bp0
2017-06-24 00:15:01 +08:00
@geelaw 上面还说在 64 位机器上分配 3 个字节会有 13 个字节的 payload,这 13 个字节的 payload 的修改都可能无法查出,那么怎么检查修改分配给我的 3 个字节呢? 如果可以的话,麻烦你简单介绍一下 decomitted 的实现方式。

而且你说的双重释放可能导致的问题,让我想到将 free 后的指针设置成 NULL,恰恰可以避免了你说的严重错误。当然严格上说这种方法并没有解决这种错误。但是如果不将 free 后的指针设置成 NULL,如果后面再次 free 才会出现双重释放。也就是导致你说的问题。

我以前简单学习过 ptmalloc 的代码,至少在 ptmalloc 中有检查双重释放的代码的。而且针对上述问题,我更愿意用 Valgrind 这种工具去检查内存操作可能出现的问题。

说了这么多,原因只有一个,就是我无法想出将 free 的指针设置为 NULL 有什么不妥的或者是可能导致其他问题的地方。希望能跟你学习一下。
geelaw
2017-06-24 00:31:40 +08:00
@bp0 因为在这个模式下,如果你释放内存,整个 page 都会变得 invalid,就好像这个 page 从没用到过一样(但是 heap manager 会避免过早再次使用这个 page )。

举个例子,第一次分配 1 Kb,记在 p 里面,第二次又分配 1 Kb,记在 q 里面,假设第二次分配的那 1 Kb 在第一次后面那一个 page。

做这个:free(q); printf("%c\n", *q);
相当于:free(q); printf("%c\n", *(p + PageSize));

把 q 释放之后访问 q,是访问无效的 page,这里效果上等于 p buffer overrun,是会被检查到的。

至于一个 page 具体是怎么被 decomitted 的,这我并不关心。

**此外,free 掉一个指针之后设置为 nullptr,不代表原来其他指向它(或者这个数组其他位置)的指针也变成了 nullptr。**

如果你可以把所有和这块内存关联的指针都找到并设置为 nullptr,那恐怕是可以的——但是这样的入侵性很强,还有可能让你不知道什么时候一个指针就被篡改了。

至于一个 malloc 的实现可以抵抗双重释放,不代表书写双重释放的代码是可以接受的——这样的代码不可移植,而且在地址被重新利用的时候会引发更加糟糕的错误。彻底解决的方法是杜绝程序有逻辑问题,而不是掩盖这个问题。

扩展阅读
https://stackoverflow.com/questions/1879550/should-one-really-set-pointers-to-null-after-freeing-them
https://stackoverflow.com/questions/1025589/setting-variable-to-null-after-free
icedx
2017-06-24 01:05:59 +08:00
借楼问下为啥大部分开源项目用的都是 malloc 而不是 calloc

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

https://www.v2ex.com/t/370623

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

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

© 2021 V2EX