在 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;
然后我们从 struct
s 外部获取指向这些成员变量的指针:
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 比任何其他值更频繁地填充零吗?
最后一点,这里有一些相关问题的链接,但没有回答我的特定问题:
- C - freeing structs
- What REALLY happens when you don't free after malloc?
当一个动态分配的对象被释放后,它就不再存在了。任何后续访问它的尝试都具有未定义的行为。因此,这个问题是无稽之谈:分配的 struct
的成员在宿主结构的生命周期结束时不再存在,因此此时无法将它们设置或重置为任何内容。没有有效的方法来尝试确定此类 no-longer-existing 对象的任何值。
调用free
后,指针F
、p
和q
不再指向有效内存。尝试取消引用这些指针会调用 undefined behavior。事实上,这些指针的值在调用 free
之后变为 indeterminate,因此您也可以通过 read 调用 UB指针值。
因为取消引用这些指针是未定义的行为,编译器可以假设它永远不会发生并根据该假设进行优化。
也就是说,没有任何内容表明 malloc
/free
实现必须保留存储在已释放内存中的值不变或将它们设置为特定值。它可能会将其内部簿记状态的一部分写入您刚刚释放的内存中,也可能不会。您必须查看 glibc 的源代码才能确切了解它在做什么。
调用 free
时会发生两件事:
- 在 C 计算模型中,任何指向已释放内存的指针值(无论是它的开头,例如您的
F
,还是其中的内容,例如您的 p
和 q
) 不再有效。 C 标准没有定义当您尝试使用这些指针值时会发生什么,如果您尝试使用它们,编译器的优化可能会对您的程序的行为方式产生意想不到的影响。
- 释放的内存被释放用于其他目的。使用它的最常见的其他目的之一是跟踪可用于分配的内存。也就是说,实现
malloc
和free
的软件需要数据结构来记录哪些内存块被释放了等信息。当您 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
读取的值的总和;即使 p1
和 p2
相等,*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_size
和 mchunk_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
) 正在改变第三个值。
设置
假设我有一个 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;
然后我们从 struct
s 外部获取指向这些成员变量的指针:
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 比任何其他值更频繁地填充零吗?
最后一点,这里有一些相关问题的链接,但没有回答我的特定问题:
- C - freeing structs
- What REALLY happens when you don't free after malloc?
当一个动态分配的对象被释放后,它就不再存在了。任何后续访问它的尝试都具有未定义的行为。因此,这个问题是无稽之谈:分配的 struct
的成员在宿主结构的生命周期结束时不再存在,因此此时无法将它们设置或重置为任何内容。没有有效的方法来尝试确定此类 no-longer-existing 对象的任何值。
调用free
后,指针F
、p
和q
不再指向有效内存。尝试取消引用这些指针会调用 undefined behavior。事实上,这些指针的值在调用 free
之后变为 indeterminate,因此您也可以通过 read 调用 UB指针值。
因为取消引用这些指针是未定义的行为,编译器可以假设它永远不会发生并根据该假设进行优化。
也就是说,没有任何内容表明 malloc
/free
实现必须保留存储在已释放内存中的值不变或将它们设置为特定值。它可能会将其内部簿记状态的一部分写入您刚刚释放的内存中,也可能不会。您必须查看 glibc 的源代码才能确切了解它在做什么。
调用 free
时会发生两件事:
- 在 C 计算模型中,任何指向已释放内存的指针值(无论是它的开头,例如您的
F
,还是其中的内容,例如您的p
和q
) 不再有效。 C 标准没有定义当您尝试使用这些指针值时会发生什么,如果您尝试使用它们,编译器的优化可能会对您的程序的行为方式产生意想不到的影响。 - 释放的内存被释放用于其他目的。使用它的最常见的其他目的之一是跟踪可用于分配的内存。也就是说,实现
malloc
和free
的软件需要数据结构来记录哪些内存块被释放了等信息。当您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
读取的值的总和;即使 p1
和 p2
相等,*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_size
和 mchunk_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
) 正在改变第三个值。