首页 文章

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

提问于
浏览
67

如果我有:

unsigned int x;
x -= x;

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

两个问题:

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

  • 如果是这样, why 确实C表示行为未定义,当完全清楚 x 应该为零时?

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

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

7 回答

  • 20

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

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

    行为未定义的原因是您的变量的附加属性,即它“可能已使用 register " that is its address is never taken. Such variables are treated specially because there are architectures that have real CPU registers that have a sort of extra state that is " uninitialized声明”并且不对应于类型域中的值 .

    编辑:标准的相关短语是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相关类型的未指定值有效值,其中本国际标准未规定在任何情况下选择哪个值的要求

  • 13

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

    注意:以下示例仅有效,因为 x 从未获取其地址,因此它是“类似寄存器” . 如果 x 的类型具有陷阱表示,它们也将是有效的;这对于无符号类型来说很少见(它需要“浪费”至少一点存储空间,并且必须记录在案),而且 unsigned char 是不可能的 . 如果 x 具有签名类型,则实现可以将位模式定义为 - (2n-1-1)和2n-1-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() ,写入了's no need to test the condition. The compiler may compile this code as if you'

    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 的值是方便的 . 因此,可以优化分配 .

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

    这个假设的编译器将每个变量映射到不同的内存页面并设置页面属性,以便从未初始化的变量读取会导致调用调试器的处理器陷阱 . 首先对变量赋值,确保其内存页面正常映射 . 这个编译器在调试模式下没有't try to perform any advanced optimization — it',旨在轻松定位诸如未初始化变量之类的错误 . 当评估 x = - x 时,右侧会导致陷阱并且调试器将启动 .

  • 0

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

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

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

  • 74

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

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

  • 15

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

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

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

  • 5

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

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

  • 10

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

相关问题