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

Meh*_*dad 77 c initialization undefined-behavior

如果我有:

unsigned int x;
x -= x;
Run Code Online (Sandbox Code Playgroud)

很明显,x 应该这样表达后是零,但到处看,他们说的行为,这种代码是不确定的,而不是仅仅值x(直到减法之前).

两个问题:

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

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

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

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

Jen*_*edt 83

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

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

行为未定义的原因是您的变量的附加属性,即它"可能已声明为register",即它的地址永远不会被采用.这些变量是专门处理的,因为有些架构具有真正的CPU寄存器,这些寄存器具有一种"未初始化"的额外状态,并且不对应于类型域中的值.

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

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

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

unsigned char a, b;
memcpy(&a, &b, 1);
a -= a;
Run Code Online (Sandbox Code Playgroud)
  • 这里是a和的地址b,因此它们的价值是不确定的.
  • 由于unsigned char从未有过表示不确定值的陷阱表示,unsigned char因此可能会发生任何值.
  • 最后a 必须保持价值0.

Edit2: a并且b具有未指定的值:

3.19.3 相关类型的未指定值
有效值,其中本国际标准未规定在任何情况下选择哪个值的要求

  • 也许我错过了一些东西,但在我看来,'unsigned`s肯定会有陷阱表示.你能指出标准的那部分吗?我在§6.2.6.2/ 1中看到以下内容:"对于除了无符号字符**以外的无符号整数类型,对象表示的位应分为两组:值位和填充位(不需要任何后者)... ...这应该被称为值表示.任何填充位的值都是未指定的.⁴⁴⁾"与评论说:"pad填充位的某些组合可能会产生陷阱表示". (5认同)
  • 继续评论:"填充位的某些组合可能会生成陷阱表示,例如,如果一个填充位是奇偶校验位.无论如何,对有效值的算术运算不能生成陷阱表示,而不是作为异常条件的一部分,例如溢出,这对于无符号类型不会发生." - 这很棒**一旦**我们有一个有效的值可以使用,但不确定的值**可能在初始化之前是一个陷阱表示(例如,奇偶校验位设置错误). (5认同)
  • @conio除了`unsigned char`之外的所有类型都是正确的,但这个答案是使用`unsigned char`.但请注意:严格符合程序可以计算`sizeof(unsigned)*CHAR_BIT`并根据`UINT_MAX`确定特定实现不可能具有`unsigned`的陷阱表示.在该程序做出该决定之后,它可以继续执行此答案与`unsigned char`完全相同的操作. (4认同)
  • @JensGustedt:"memcpy"不是一个分心,即如果它被`*&a =*&b;`替换,你的例子仍然不适用. (3认同)
  • @R ..我不确定了.关于C委员会的邮件列表正在进行讨论,似乎所有这些都是一个大混乱,即预期的行为与实际写入的内容之间存在巨大差距.但更清楚的是,访问内存为`unsigned char`并因此`memcpy`有帮助,`*&`的内存不太清楚.一旦这个问题得到解决,我会报告. (3认同)
  • 阅读更多内容后,故事变得更加复杂。C 委员会对[缺陷报告#451](http://www.open-std.org/Jtc1/sc22/WG14/www/docs/dr_451.htm)(和#260,链接在那里)的回应表明不确定值允许在没有程序直接操作的情况下出现变化。我想,委员会答复中的这一陈述和其他陈述意味着“a -= a”仍然会导致不确定的值,即使它不是真正的 UB。你是否不同意,并认为我的观点有偏差? (3认同)
  • NaT 是硬件寄存器的状态。我认为这就是对象*“可以用寄存器存储类声明”*这个想法的起源。一旦您通过内存以字节形式访问数据,它就不能具有 NaT 状态,这就是整个想法。 (2认同)
  • 至少在C11标准的草案中,附件J.2在未定义行为列表中包括"在不确定时使用具有自动存储持续时间的对象的值".现在这个附件不是规范性的,并且不清楚标准机构是否同意所引用的部分,所以它可能在J.2中声称过多.这是你的立场吗?因为我读J.2说,即使是`memcpy`例子也会有UB. (2认同)
  • @BenVoigt,附件 J 不是规范性的。事实上,你应该将其理解为“在某些情况下......” (2认同)

Gil*_*il' 23

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

注意:以下示例仅有效,因为x从不使用其地址,因此它是"类似寄存器".如果x具有陷阱表示的类型,它们也将是有效的; 这对于无符号类型来说很少见(它需要"浪费"至少一点存储空间,并且必须记录在案),而且不可能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 */
Run Code Online (Sandbox Code Playgroud)

当评估第3行时,x尚未初始化,因此(编译器的原因)第3行必须是由于编译器不够聪明的其他条件而不能发生的某种侥幸.由于z未在第4行之后使用,并且x未在第5行之前使用,因此可以对两个变量使用相同的寄存器.所以这个小程序编译成寄存器上的以下操作:

r1 = 0;
r0 = 4;
r0 = - r0;
r1 += r0;
r0 = r1;
Run Code Online (Sandbox Code Playgroud)

最终值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);
}
Run Code Online (Sandbox Code Playgroud)

假设编译器检测到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);
}
Run Code Online (Sandbox Code Playgroud)

这可能会在编译器中进行建模的方法就是要考虑的是,根据任何值x任何值是方便,只要x是未初始化.因为未初始化变量未定义时的行为,而不是仅具有未指定值的变量,编译器不需要跟踪任何方便值之间的任何特殊数学关系.因此编译器可以用这种方式分析上面的代码:

  • 在第一次循环迭代期间,被评估x的时间未初始化-x.
  • -x 具有未定义的行为,因此它的价值是任何方便的.
  • 优化规则适用,因此可以将此代码简化为.condition ? value : valuecondition; value

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

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

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


eq-*_*eq- 16

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

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

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

  • @Vlad Lazarenko:Itanium CPU对每个整数寄存器都有一个NaT(非Thing)标志.NaT标志用于控制推测执行,并且可能在使用前未正确初始化的寄存器中停留.从具有NaT位集的这样的寄存器读取会产生异常.请参阅http://blogs.msdn.com/b/oldnewthing/archive/2004/01/19/60162.aspx (6认同)
  • 我讨厌有人侮辱其他人,因为有人不理解这个问题.行为是否未定义完全取决于标准所说的内容.哦,对于eq的场景,没有什么实际可行的......它完全是人为的. (4认同)
  • 好吧,理论上你可以有一个编译器决定只使用28位整数(在x86上)并添加特定的代码来处理每个加法,乘法(等等)并确保这4位不被使用(否则发出一个SIGSEGV) ).未初始化的价值可能会导致这种情况. (3认同)
  • @VladLazarenko:这是关于C,而不是特定的CPU.任何人都可以轻而易举地设计一个具有整数位模式的CPU,这会让它变得疯狂.考虑一个在寄存器中有"疯狂位"的CPU. (2认同)
  • 那么,我可以说,在整数和x86的情况下,行为是明确定义的吗? (2认同)
  • PS David Schwartz在另一个答案下的想法是一个更实际的想法,并建议另一个......假设物理内存在初始化或写入之前没有分配给虚拟地址; 然后访问未初始化的变量可能会导致访问冲突. (2认同)

Eri*_*hil 13

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

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


Dav*_*rtz 11

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

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

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

  • 1)当然,他们可以.但是我想不出任何能让这更好的论点.2)平台知道无法依赖未初始化内存的值,因此可以自由更改它.例如,它可以在后台将零未初始化的内存归零,以便在需要时准备好使用归零页面.(考虑是否发生这种情况:1)我们读取要减去的值,比如我们得到3. 2)页面变为零,因为它未初始化,将值更改为0. 3)我们进行原子减法,分配页面并制作价值-3.哎呀.) (7认同)
  • 因为你只是声称有一般规则,而不是参考它.因此,它只是"权威证明"的尝试,这不是我对SO的期望.而且没有有效地争论为什么这不是一个非特定的价值.在一般情况下,这是UB的唯一原因是`x`可以声明为`register`,即它的地址永远不会被采用.我不知道你是否意识到这一点(如果你有效地隐藏它),但正确答案必须提到它. (3认同)
  • -1因为你没有给你的索赔任何理由.在某些情况下,期望编译器只获取写入内存位置的值是有效的. (2认同)
  • 这个答案是不正确的,它指出“是的,它是未定义的。” 正如我和 Jens Gustedt 的答案所示(引用了 C 标准,该答案未提供),获取未初始化对象的值本身并不会导致未初始化的行为。在 C 1999 中,仅当满足某些其他条件时才会发生未定义的行为,而大多数常见系统上的整数类型不满足这些条件。请参阅 Jens Gustedt 对 C 2011 情况的回答。 (2认同)

Lun*_*din 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
陷阱表示
一种对象表示,不需要表示对象类型的值

  • 顺便说一句,这是为什么应该始终使用 `stdint.h` 而不是 C 的本机类型的一个很好的理由。因为 `stdint.h` 强制使用 2 的补码并且没有填充位。换句话说,`stdint.h` 类型不允许充满废话。 (3认同)
  • @Vality在现实世界中,所有计算机中的99.9999%是没有陷阱表示的二进制补码CPU。因此,没有陷阱表示是常态,并且讨论此类现实计算机上的行为非常重要。假设使用异国情调的计算机是正常的做法是没有帮助的。现实世界中的陷阱表示非常罕见,以至于术语“陷阱表示”在标准中的存在通常被认为是从1980年代继承的标准缺陷。如对个人补码和数字计算机的支持。 (2认同)
  • 委员会对缺陷报告的答复再次说:“问题2的答案是,对不确定值执行的任何操作都将具有不确定值。” 和“问题3的答案是,当对不确定的值使用库函数时,它们将表现出不确定的行为。” (2认同)
  • DR 451和260 (2认同)