【问题标题】:Does reading an uninitialized malloc() memory invoke Undefined Behaviors?读取未初始化的 malloc() 内存会调用未定义的行为吗?
【发布时间】:2021-03-28 08:52:33
【问题描述】:

我知道这是一个非常基本的问题,并且可能存在重复,但我无法找到针对该标准的特定问题的严格答案。 (我看到有人说是UB,有人说不是)

如果我分配一块内存而不向其中填充数据,

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

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

但这是否被归类为未定义行为?还是只是很糟糕,但至少不是 UB?

【问题讨论】:

  • "然后尝试阅读" --> 如int?确信答案取决于如何读取数据。 IIRC,unsigned char 问题最少。

标签: c malloc undefined-behavior


【解决方案1】:

总结

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

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

详情

C 2018 7.22.3.4 2 说,malloc 函数与参数 size

malloc 函数为其size 由大小指定且值不确定的对象分配空间。

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

未指定的值或陷阱表示

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

本文档对在任何情况下选择哪个值没有要求的相关类型的有效值

其中没有任何东西使行为未定义。

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

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

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

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

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

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

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

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

讨论

要了解为什么 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 存储在 a4 中 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,则3 将存储在a 中,7 将存储在b 中。没有可能的值 *p 可能会导致 *p + 3 为 3 和 *p + 4 为 7。所以此代码的行为就像 *p 已自行更改。

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

【讨论】:

  • 这涵盖了各个方面。做得很好。所以用外行的话来说,未初始化的 malloc'ed 字节就像一个“巧克力盒——你永远不知道你会得到什么......”
  • 很好的答案!看起来在编辑中,您可能已经更改了一些细节。在a = (*p + 3 ) * 4; b = (*p + 5) * 6 的示例中,程序集似乎省略了乘法并正在计算b = (*p + 4)
  • @WilliamPursell:是的,我从更复杂的代码开始,并在编写时减少了它。已修复,谢谢。
  • 处理未初始化存储的最有用的实现方式取决于使用它的目的。因此,该标准允许希望为其客户提供服务的实现者可以选择以客户认为最有用的任何方式处理此类存储,因此允许那些希望执行“聪明”优化而破坏客户代码的人不考虑了解此类行为是否会使他们的编译器或多或少对客户有用。
【解决方案2】:

是的,您可以阅读。

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

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

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

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

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

【讨论】:

  • 不,您无法可靠地阅读它。根据我的回答,行为不是未定义的,但也没有定义为实际读取内存。
【解决方案3】:

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

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

【讨论】:

    猜你喜欢
    • 2013-08-12
    • 2015-04-19
    • 1970-01-01
    • 2016-01-28
    • 2021-10-05
    • 2020-02-12
    • 1970-01-01
    • 1970-01-01
    • 2022-01-08
    相关资源
    最近更新 更多