在 C 中,为什么结构的第一个成员在使用 free() 释放时经常 "reset" 到 0?

In C, why are the first members of a struct frequently "reset" to 0 when it is deallocated with free()?

设置

假设我有一个 struct father,它有一个成员变量,例如 int,另一个 struct(所以 father 是一个嵌套的 struct ).这是一个示例代码:

struct mystruct {
    int n;
};

struct father {
    int test;
    struct mystruct M;
    struct mystruct N;
};

在main函数中,我们用malloc()分配内存来创建一个新的struct father类型的结构体,然后我们填充它的成员变量和它的子成员变量:

    struct father* F = (struct father*) malloc(sizeof(struct father));
    F->test = 42;
    F->M.n = 23;
    F->N.n = 11;

然后我们从 structs 外部获取指向这些成员变量的指针:

    int* p = &F->M.n;
    int* q = &F->N.n;

之后我们打印free(F)执行前后的值,然后退出:

    printf("test: %d, M.n: %d, N.n: %d\n", F->test, *p, *q);
    free(F);
    printf("test: %d, M.n: %d, N.n: %d\n", F->test, *p, *q);
    return 0;

这是示例输出(*):

test: 42, M.n: 23, N.n: 11
test: 0, M.n: 0, N.n: 1025191952

*: 使用 gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

pastebin 上的完整代码:https://pastebin.com/khzyNPY1

问题

那是我用来测试如何使用 free() 释放内存 的测试程序。我的想法(来自阅读 K&R“8.7 示例 - 存储分配器”,其中实现并解释了 free() 的一个版本)是,当你 free()struct 时,你是几乎只是告诉操作系统或程序的其余部分,您将不会在先前分配给 malloc() 的内存中使用特定的 space。那么,释放那些内存块后,成员变量中应该有垃圾值,对吧?我可以在测试程序中看到 N.n 发生的情况,但是,随着我 运行 越来越多的样本,很明显在绝大多数情况下,这些成员变量被“重置”为 0超过任何其他“运行dom”值。我的问题是:这是为什么? 是因为 stack/heap 比任何其他值更频繁地填充零吗?


最后一点,这里有一些相关问题的链接,但没有回答我的特定问题:

当一个动态分配的对象被释放后,它就不再存在了。任何后续访问它的尝试都具有未定义的行为。因此,这个问题是无稽之谈:分配的 struct 的成员在宿主结构的生命周期结束时不再存在,因此此时无法将它们设置或重置为任何内容。没有有效的方法来尝试确定此类 no-longer-existing 对象的任何值。

调用free后,指针Fpq不再指向有效内存。尝试取消引用这些指针会调用 undefined behavior。事实上,这些指针的值在调用 free 之后变为 indeterminate,因此您也可以通过 read 调用 UB指针值。

因为取消引用这些指针是未定义的行为,编译器可以假设它永远不会发生并根据该假设进行优化。

也就是说,没有任何内容表明 malloc/free 实现必须保留存储在已释放内存中的值不变或将它们设置为特定值。它可能会将其内部簿记状态的一部分写入您刚刚释放的内存中,也可能不会。您必须查看 glibc 的源代码才能确切了解它在做什么。

调用 free 时会发生两件事:

  • 在 C 计算模型中,任何指向已释放内存的指针值(无论是它的开头,例如您的 F,还是其中的内容,例如您的 pq) 不再有效。 C 标准没有定义当您尝试使用这些指针值时会发生什么,如果您尝试使用它们,编译器的优化可能会对您的程序的行为方式产生意想不到的影响。
  • 释放的内存被释放用于其他目的。使用它的最常见的其他目的之一是跟踪可用于分配的内存。也就是说,实现mallocfree的软件需要数据结构来记录哪些内存块被释放了等信息。当您 free 内存时,该软件通常会为此目的使用部分内存。这可能会导致您看到的变化。

释放的内存也可能被程序中的其他东西使用。在没有信号处理程序或类似东西的 single-threaded 程序中,通常没有软件会 运行 在 free 和你显示的 printf 参数的准备之间,所以没有别的会如此快速地重用内存——被 malloc 软件重用是对您观察到的情况最有可能的解释。但是,在多线程程序中,内存可能会立即被另一个线程重用。 (实际上,这可能不太可能,因为 malloc 软件可能会优先为单独的线程保留单独的内存池,以减少必要的 inter-thread 同步量。)

该标准未指定在释放后使用指向已分配存储空间指针的程序的行为。实现可以通过指定比标准要求更多的程序的行为来自由扩展语言,标准的作者旨在鼓励实现之间的多样性,这些实现将支持市场指导的 quality-of-implementation 基础上的流行扩展。一些带有指向死对象指针的操作得到了广泛支持(例如,给定 char *x,*y; 如果程序执行 free(x); y=x;x 已经 non-null,不考虑在 y 初始化后是否有任何事情发生,但大多数实现会扩展语言以保证如果从未使用 y 这样的代码将无效)但取消引用这些指针通常不是。

请注意,如果要将指向已释放对象的同一指针的两个副本传递给:

int test(char *p1, char *p2)
{
  char *q;
  if (*p1)
  {
    q = malloc(0):
    free(q);
    return *p1+*p2;
  }
  else
    return 0;
}

分配和释放 q 的行为完全有可能扰乱已分配给 *p1(以及 *p2)的存储中的位模式,但编译器不需要允许这种可能性。编译器可能会 return 从 malloc/free 之前的 *p1 读取的值与从 *p2 之后的 *p2 读取的值的总和;即使 p1p2 相等,*p1+*p2 应该始终为偶数,此总和也可能是奇数。

除了未定义的行为和标准可能规定的任何其他内容之外,由于动态分配器是一个程序,固定了一个特定的实现,假设它不根据外部因素做出决定(它没有)行为完全是确定性的。

真实答案:您在这里看到的是 glibc 分配器内部工作的效果(glibc 是 Ubuntu 上的默认 C 库)。

分配块的内部结构如下(source):

struct malloc_chunk {
    INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
    INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
    struct malloc_chunk* fd;                /* double links -- used only if free. */
    struct malloc_chunk* bk;        
    /* Only used for large blocks: pointer to next larger size.  */
    struct malloc_chunk* fd_nextsize;       /* double links -- used only if free. */
    struct malloc_chunk* bk_nextsize;
};

在内存中,当块被使用时(不是空闲的),它看起来像这样:

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of previous chunk, if unallocated (P clear)  |
        +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             Size of chunk, in bytes                     |A|M|P| flags
  mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
        |             User data starts here...                          |

mchunk_prev_sizemchunk_size 之外的每个字段仅在块空闲时才会填充。这两个字段就在用户可用缓冲区之前。用户数据在 mchunk_size 之后开始(即在 fd 的偏移处),并且可以任意大。 mchunk_prev_size 字段保存前一个块的大小(如果它是空闲的),而 mchunk_size 字段保存块的实际大小(至少比请求的大小多 16 个字节)。

库本身的注释中提供了更详尽的解释 here(如果您想了解更多,强烈建议阅读)。

当您 free() 一个块时,为了簿记目的,需要做出很多关于将该块“存储”在何处的决定。通常,释放的块根据它们的大小被排序到双链表中,以优化后续分配(可以从这些列表中获得合适大小的已经可用的块)。您可以将其视为一种缓存机制。

现在,根据您的 glibc 版本,它们的处理方式和内部实现可能略有不同 is quite complex,但您的情况是这样的:

struct malloc_chunk *victim = addr; // address passed to free()

// Add chunk at the head of the free list
victim->fd = NULL;
victim->bk = head;
head->fd = victim;

因为你的结构基本上等同于:

struct x {
    int a;
    int b;
    int c;
}

并且由于在您的机器 sizeof(struct malloc_chunk *) == 2 * sizeof(int) 上,第一个操作 (victim->fd = NULL) 有效地清除了结构的前两个字段的内容(请记住,用户数据恰好从 fd),而第二个 (victim->bk = head) 正在改变第三个值。