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

时间:2012-08-14 23:47:43

标签: c undefined-behavior initialization

如果我有:

unsigned int x;
x -= x;

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

两个问题:

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

  • 如果是这样, 为什么 C会说行为未定义,当x非常清楚时这里应该为零?

    即。通过不在此定义行为给出的优势是什么?

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

7 个答案:

答案 0 :(得分:78)

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

首先,使用单位化值本身并不是未定义的行为,但该值只是不确定的。如果该值恰好是该类型的陷阱表示,那么访问它就是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 未指定的值
    本国际标准对哪个值没有要求的相关类型的有效值   在任何情况下都会被选中

答案 1 :(得分:21)

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

注意:以下示例仅有效,因为x从未获取其地址,因此它是“类似寄存器”。如果x的类型具有陷阱表示,它们也将是有效的;对于无符号类型,这种情况很少发生(它需要“浪费”至少一点存储空间,并且必须记录在案),unsigned char是不可能的。如果x具有签名类型,则实现可以定义不是 - (2 n-1 -1)和2 n-1 <之间的数字的位模式/ sup> -1作为陷阱表示。请参阅Jens Gustedt's answer

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

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已正确初始化,则这些值为x = -3和y = -4,而不是5和4。

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

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

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

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

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

答案 2 :(得分:16)

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

  

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

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

答案 3 :(得分:14)

(这个答案解决了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的每个N值位表示2的幂,并且值位的每个组合表示从0到2 N -1的值之一。因此,无符号整数只能由于填充位中的某些值(例如奇偶校验位)而具有陷阱表示。

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

答案 4 :(得分:11)

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

  

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

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

答案 5 :(得分:6)

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

  • 如果变量具有自动存储持续时间 没有采用其地址,则代码始终会调用未定义的行为[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
  陷阱表示
  一个对象表示,不需要表示对象类型的值

答案 6 :(得分:1)

虽然许多答案都集中在陷阱未初始化注册访问的处理器上,但即使在没有此类陷阱的平台上也会出现奇怪的行为,使用的编译器并没有特别努力来利用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。