读取未初始化的 malloc() 内存是否会调用未定义的行为?

Does reading an uninitialized malloc() memory invoke Undefined Behaviors?

我知道这是一个非常基本的问题,可能有重复问题,但我找不到针对这个涉及标准的具体问题的严格答案。 (我看到有人说是UB,有人说不是)

如果我分配了一块内存但没有填充数据,

int* ptr = malloc(10 * sizeof(int));

然后尝试读取它,那里的值将是垃圾。

但这是否归类为未定义行为?或者它只是不好但至少不是 UB?

是的,你可以阅读。

内存管理器通过调用 malloc 为您提供了内存,所以现在它是您的了,您可以用它做任何想做的事。这包括阅读它,例如将其打印为整数数组。

Eric 讨论编译器可能会优化掉指令,但是,他的讨论是基于这样一个事实,即编译器知道 malloc 并且它 returns“未初始化”内存,即,用户程序未赋值的内存。

但本质上,malloc 只是一个 returns 对象的函数,编译器必须假设返回的对象具有有意义的值,就像用户函数 returns 一个目的。也不会有“带内”陷阱值,因为内存中的任何值都可以是合法值(使用 32 位 int 的所有 32 位)。只能有带外陷阱值,即额外的硬件,如额外的硬件位。但是后来陷阱值变成了运行-time.

如果用户使用 realloc 扩展了现有的内存块(即使是“无块”内存),那么编译器即使知道 realloc 也不能假设有关返回对象的任何信息,并且无法优化指令。编译器无法假设。

请注意,作为一种安全措施,内存管理器可能已将内存设置为某个未指定的值,以防止程序从其他用途留下的内存中读取数据。

总结

malloc 提供的读取未初始化内存的行为本身并不是未定义的。如果使用非字符类型读取包含陷阱表示的内存,可能会导致未定义的行为,但这只有在该类型具有陷阱表示时才会发生。 (大多数现代 C 实现没有整数类型的陷阱表示。)

然而,虽然它不是完全未定义的,但也不是完全定义的。实际读取内存不需要尝试读取未初始化的内存。

详情

C 2018 7.22.3.4 2 表示,malloc 函数的参数 size:

The malloc function allocates space for an object whose size is specified by size and whose value is indeterminate.

C 3.19.2 1 将不确定值定义为:

either an unspecified value or a trap representation

C 3.19.3 1 将未指定值定义为:

valid value of the relevant type where this document imposes no requirements on which value is chosen in any instance

这里没有任何内容使行为未定义。

根据 6.2.6.1 5,C 标准未定义读取具有非字符类型的陷阱表示的行为。因此,如果使用具有陷阱表示的类型读取内存,并且结果位恰好包含表示陷阱的值,则行为未定义。

整数类型的陷阱表示在现代 C 实现中很少见。许多年前,一些系统会保留某些位模式,例如 16 位 800016,以表示未初始化或无效的数据,并且尝试在算术中使用这样的值会生成陷阱。在某些类型 T 中没有陷阱表示的 C 实现中,通过类型 T 访问未初始化的数据不会遇到陷阱表示。因此结果必须是类型的未指定(因此有效)值。

此外,C 标准中没有任何其他内容会使此行为未定义。 6.3.2.1 2 中有一条规则,如果未获取其地址,则访问自动存储持续时间的未初始化对象具有未定义的行为。但是malloc提供的内存是有分配存储时长的,不是自动的。 (该规则适用于某些惠普硬件,具有将寄存器标记为未初始化并在使用时捕获的功能。)

此外,无论成员的类型如何,整个结构和联合永远不会是陷阱表示。现代 C 实现中最常见的陷阱表示是浮点信号 NaN(非数字)。

请注意,分配的内存中的值是未指定的,上面的定义指出“本文档对在任何情况下选择哪个值都没有强加要求。”这意味着如果你这样做:

unsigned *p = malloc(sizeof *p);
printf("%u\n", *p);
printf("%u\n", *p);

C 标准不要求在第一个 printf 中为 *p 选择哪个值,也不要求在第二个 printf、[=80] 中选择哪个值=]甚至不需要它们彼此相同。一个“未指定的值”可能表现得好像它有自己随时变化的位。因此,行为不是未定义的——它不允许“任何事情”发生在你的程序上;你的程序不能突然跳转到不同的函数或清除其他数据——但它也没有被定义为像内存中有固定值的位一样。

这意味着您无法可靠地读取未初始化的内存——不能保证内存读取产生实际位于物理内存中的位。

讨论

要了解为什么 C 标准允许程序表现得好像内存中的位可能会发生变化,请考虑以下代码:

unsigned a = *p + 3;
unsigned b = *p + 4;

对于正常情况下的代码,编译器可能会生成这样的程序集:

// As we start, registers r7, r8, and r9 already contain p,
// the address of a, and the address of b, respectively.
load  r3, (r7) // Get value of *p from memory.
add   r3, #3   // Add 3.
store r3, (r8) // Store sum to a.
load  r3, (r7) // Get value of *p from memory.
add   r3, #4   // Add 4.
store r3, (r9) // Store sum to b.

如果内存恰好包含 0,那么这些指令会将 3 存储在 a 中,将 4 存储在 b 中。然而,未初始化的内存不需要表现得好像它有一个固定值的规则意味着允许编译器的优化来消除加载指令。假设地,这可能会产生如下指令:

add   r3, #3   // Add 3.
store r3, (r8) // Store sum to a.
add   r3, #4   // Add 4.
store r3, (r9) // Store sum to b.

如果这个码序列开始的时候r3刚好包含0,那么a就会存入3,b就会存入7。没有可能的值 *p 会导致 *p + 3 为 3 而 *p + 4 为 7。所以这段代码就像 *p 自己改变了一样。

实际上,优化不仅会删除此处的加载指令,而且不会识别后续指令也与固定值断开连接并删除它们。然而,现实世界的优化比这更复杂。 C 标准授予的许可证允许编译器删除它可以识别出的未使用定义值的代码部分,即使它无法识别出程序的所有内容。

因为在某些情况下,保证 malloc() 存储的重复读取将产生一致的值,除非或直到它被写入,对于实现来说可能是有用的,但在某些情况下它可能是有用的对于不受此类保证约束的实施,任何特定实施是否应提供此类保证的问题是标准管辖范围之外的实施质量。

至于读取此类存储是否会引发除了产生无意义值之外的副作用,目前还不清楚。当然,捕获此类读取对于诊断实现可能很有用,但标准并未明确规定此类内容。另一方面,我不认为标准明确规定从 malloc 返回的存储不会表现得好像任意对象已写入其中,从而导致此类存储与任意有效类型相关联。这些问题再次归结为标准管辖范围之外的实施质量问题。