0

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

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

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

然后尝试读取它,值会有垃圾。

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

4

3 回答 3

5

概括

读取由 提供的未初始化内存的行为本身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 位 8000 16来表示未初始化或无效的数据,并且尝试在算术中使用这样的值会产生陷阱。在某些类型 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标准没有要求*p在第一个中选择printf哪个值,也没有在第二个中选择哪个值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 存储a4b 中。但是,未初始化的内存不需要表现得好像它具有固定值的规则意味着允许编译器的优化来消除加载指令。假设,这可能会导致如下指令:

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 + 47。因此,此代码的行为好像*p已自行更改。

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

于 2021-03-28T09:55:55.463 回答
0

是的,你可以阅读它。

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

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

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

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

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

于 2021-03-28T09:08:52.290 回答
-1

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

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

于 2021-03-31T23:06:57.627 回答