繁体   English   中英

(为什么)正在使用未初始化的变量未定义行为?

[英](Why) is using an uninitialized variable undefined behavior?

如果我有:

unsigned int x;
x -= x;

很明显,在这个表达式之后x应该为零,但我看的每个地方,他们都说这段代码的行为是未定义的,而不仅仅是x的值(直到减法之前)。

两个问题:

  • 这段代码的行为确实未定义吗?
    (例如,代码是否会在兼容系统上崩溃 [或更糟]?)

  • 如果是这样,为什么C 说行为是未定义的,而这里的x应该为零是完全清楚的?

    即在这里不定义行为有什么好处

显然,编译器可以简单地使用它认为在变量中“方便”的任何垃圾值,并且它会按预期工作……这种方法有什么问题?

是的,这种行为是未定义的,但原因与大多数人所知的不同。

首先,使用未初始化的值本身并不是未定义的行为,但该值只是不确定的。 如果该值恰好是该类型的陷阱表示,那么访问它就是 UB。 无符号类型很少有陷阱表示,所以在这方面你会相对安全。

使行为未定义的是变量的一个附加属性,即它“可以用register声明”,即它的地址永远不会被占用。 此类变量被特殊对待,因为有些体系结构具有真实的 CPU 寄存器,这些寄存器具有一种“未初始化”的额外状态,并且与类型域中的值不对应。

编辑:标准的相关短语是 6.3.2.1p2:

如果左值指定了一个自动存储持续时间的对象,该对象可以使用寄存器存储类声明(从未获取其地址),并且该对象未初始化(未使用初始化程序声明并且在使用之前未对其进行赋值) ),行为未定义。

为了更清楚,以下代码在所有情况下都是合法的:

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
  • 这里取了ab的地址,所以它们的值只是不确定的。
  • 由于unsigned char从来没有陷阱表示是不确定的值就是不确定的任何值unsigned char可能发生。
  • 最后a必须保持值0

Edit2: ab有未指定的值:

3.19.3未指定值
本国际标准对在任何情况下选择哪个值没有强加要求的相关类型的有效值

C 标准为编译器提供了很多执行优化的自由。 如果您假设一个简单的程序模型,其中未初始化的内存被设置为某种随机位模式,并且所有操作都按照它们的写入顺序执行,那么这些优化的结果可能会令人惊讶。

注意:下面的例子是有效的,因为x从来没有被占用过它的地址,所以它是“类似寄存器的”。 如果x的类型具有陷阱表示,它们也将是有效的; 对于 unsigned 类型,这很少是这种情况(它需要“浪费”至少一位存储空间,并且必须记录在案),而对于unsigned char不可能。 如果x具有有符号类型,则实现可以定义位模式,该位模式不是 -(2 n-1 -1) 和 2 n-1 -1 之间的数字作为陷阱表示。 请参阅Jens Gustedt 的回答

编译器尝试将寄存器分配给变量,因为寄存器比内存快。 由于程序使用的变量可能比处理器拥有的寄存器多,编译器执行寄存器分配,这导致不同的变量在不同的时间使用相同的寄存器。 考虑程序片段

unsigned x, y, z;   /* 0 */
y = 0;              /* 1 */
z = 4;              /* 2 */
x = - x;            /* 3 */
y = y + z;          /* 4 */
x = y + 1;          /* 5 */

当第 3 行被求值时, x还没有被初始化,因此(编译器的原因)第 3 行一定是某种侥幸,由于编译器不够聪明而无法弄清楚的其他条件,它不会发生。 由于第 4 行之后不使用z ,第 5 行之前也不使用x ,因此两个变量可以使用相同的寄存器。 所以这个小程序被编译成以下对寄存器的操作:

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;

x的最终值是r0的最终值, y的最终值是r1的最终值。 这些值是 x = -3 和 y = -4,而不是 5 和 4,如果x已正确初始化,则会发生。

有关更详细的示例,请考虑以下代码片段:

unsigned i, x;
for (i = 0; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

假设编译器检测到该condition没有副作用。 由于condition不会修改x ,编译器知道循环的第一次运行不可能访问x因为它尚未初始化。 因此循环体的第一次执行等效于x = some_value() ,无需测试条件。 编译器可能会像您编写的那样编译此代码

unsigned i, x;
i = 0; /* if some_value() uses i */
x = some_value();
for (i = 1; i < 10; i++) {
    x = (condition() ? some_value() : -x);
}

在编译器内部建模的方法是考虑任何依赖于x都有任何方便的值,只要x未初始化。 因为当未初始化的变量未定义时的行为,而不是变量仅具有未指定的值时,编译器不需要跟踪任何方便的值之间的任何特殊数学关系。 因此编译器可以这样分析上面的代码:

  • 在第一次循环迭代期间, x在计算-x未初始化。
  • -x具有未定义的行为,因此它的值是任何方便的。
  • 优化规则condition ? value : value condition ? value : value适用,所以这段代码可以简化为condition ; value condition ; value

当遇到您问题中的代码时,同一个编译器会分析,当计算x = - x时, -x的值是任何方便的。 因此可以优化分配。

我还没有寻找具有上述行为的编译器示例,但这是优秀编译器尝试进行的优化。 遇到一个我不会感到惊讶。 这是一个不太可信的例子,说明你的程序崩溃的编译器。 (如果您在某种高级调试模式下编译程序,这可能不是那么令人难以置信。)

这个假设的编译器映射不同内存页面中的每个变量并设置页面属性,以便从未初始化的变量中读取会导致调用调试器的处理器陷阱。 任何对变量的赋值首先要确保它的内存页被正常映射。 该编译器不会尝试执行任何高级优化——它处于调试模式,旨在轻松定位诸如未初始化变量之类的错误。 当计算x = - x ,右侧会导致陷阱并启动调试器。

是的,程序可能会崩溃。 例如,可能存在可能导致 CPU 中断的陷阱表示(无法处理的特定位模式),未处理可能会导致程序崩溃。

(C11 草案中的 6.2.6.1 说)某些对象表示不需要表示对象类型的值。 如果对象的存储值具有这样的表示形式并且被没有字符类型的左值表达式读取,则行为未定义。 如果这种表示是由没有字符类型的左值表达式修改对象的全部或任何部分的副作用产生的,则行为是未定义的。50) 这种表示称为陷阱表示。

(此解释仅适用于unsigned int可以具有陷阱表示的平台,这在现实世界系统中很少见;有关详细信息,请参阅注释,并参考导致标准当前措辞的替代和可能更常见的原因。)

(此答案针对 C 1999。对于 C 2011,请参阅 Jens Gustedt 的答案。)

C 标准并没有说使用未初始化的自动存储持续时间对象的值是未定义的行为。 C 1999 标准在 6.7.8 10 中说,“如果没有明确初始化具有自动存储持续时间的对象,则其值是不确定的。” (这一段继续定义静态对象是如何初始化的,所以我们关心的唯一未初始化的对象是自动对象。)

3.17.2 将“不确定值”定义为“未指定值或陷阱表示”。 3.17.3 将“未指定值”定义为“本国际标准对在任何情况下选择哪个值没有要求的相关类型的有效值”。

因此,如果未初始化的unsigned int x具有未指定的值,则x -= x必须产生零。 这留下了它是否可能是陷阱表示的问题。 根据 6.2.6.1 5,访问陷阱值确实会导致未定义的行为。

某些类型的对象可能具有陷阱表示,例如浮点数的信号 NaN。 但是无符号整数是特殊的。 根据 6.2.6.2,无符号整数的 N 值位中的每一个都表示 2 的幂,并且值位的每个组合表示从 0 到 2 N -1 的值之一。 因此,无符号整数只能由于其填充位(例如奇偶校验位)中的某些值而具有陷阱表示。

如果在您的目标平台上,unsigned int 没有填充位,则未初始化的 unsigned int 不能具有陷阱表示,并且使用其值不会导致未定义的行为。

是的,它是未定义的。 代码可能会崩溃。 C 表示该行为是未定义的,因为没有特定理由对一般规则进行例外处理。 优点是与所有其他未定义行为情况相同的优点——编译器不必输出特殊代码来完成这项工作。

显然,编译器可以简单地使用它认为在变量中“方便”的任何垃圾值,并且它会按预期工作......这种方法有什么问题?

为什么你认为这不会发生? 这正是采取的方法。 编译器不需要让它工作,但不需要让它失败。

对于任何类型的任何变量,未初始化或由于其他原因持有不确定值,以下适用于读取该值的代码:

  • 如果变量具有自动存储持续时间并且没有获取其地址,则代码始终调用未定义的行为 [1]。
  • 否则,如果系统支持给定变量类型的陷阱表示,代码总是调用未定义的行为 [2]。
  • 否则,如果没有陷阱表示,则变量采用未指定的值。 无法保证每次读取变量时此未指定的值都是一致的。 但是,它保证不是陷阱表示,因此保证不会调用未定义的行为 [3]。

    然后可以安全地使用该值而不会导致程序崩溃,尽管此类代码不可移植到具有陷阱表示的系统。


[1]:C11 6.3.2.1:

如果左值指定了一个自动存储持续时间的对象,该对象可以使用寄存器存储类声明(从未获取其地址),并且该对象未初始化(未使用初始化程序声明并且在使用之前未对其进行赋值) ),行为未定义。

[2]:C11 6.2.6.1:

某些对象表示不需要表示对象类型的值。 如果对象的存储值具有这样的表示形式并且被没有字符类型的左值表达式读取,则行为未定义。 如果这种表示是由没有字符类型的左值表达式修改对象的全部或任何部分的副作用产生的,则行为是未定义的。50) 这种表示称为陷阱表示。

[3] C11:

3.19.2
不确定值
未指定的值或陷阱表示

3.19.3
未指定值
本国际标准对在任何情况下选择哪个值没有强加要求的相关类型的有效值
注意未指定的值不能是陷阱表示。

3.19.4
陷阱表示
不需要表示对象类型值的对象表示

虽然许多答案都集中在捕获未初始化寄存器访问的处理器上,但即使在没有此类陷阱的平台上,使用不特别努力利用 UB 的编译器也会出现古怪的行为。 考虑代码:

volatile uint32_t a,b;
uin16_t moo(uint32_t x, uint16_t y, uint32_t z)
{
  uint16_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z;
  return temp;  
}

像 ARM 这样的平台的编译器,其中除加载和存储之外的所有指令都在 32 位寄存器上运行,可以以等效于的方式合理地处理代码:

volatile uint32_t a,b;
// Note: y is known to be 0..65535
// x, y, and z are received in 32-bit registers r0, r1, r2
uin32_t moo(uint32_t x, uint32_t y, uint32_t z)
{
  // Since x is never used past this point, and since the return value
  // will need to be in r0, a compiler could map temp to r0
  uint32_t temp;
  if (a)
    temp = y;
  else if (b)
    temp = z & 0xFFFF;
  return temp;  
}

如果任一 volatile 读取产生非零值,则 r0 将加载范围为 0...65535 的值。 否则,它将产生调用函数时所持有的任何内容(即传递给 x 的值),这可能不是 0..65535 范围内的值。 该标准没有任何术语来描述类型为 uint16_t 但其值在 0..65535 范围之外的值的行为,只是说任何可能产生此类行为的操作都会调用 UB。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM