(为什么)正在使用未初始化的variables未定义的行为?
如果我有:
unsigned int x; x -= x;
很明显,在这个expression式之后x
应该是零,但是在我看来,他们说这个代码的行为是不确定的,不仅仅是x
的值(直到减法之前)。
两个问题:
-
这个代码的行为确实没有定义?
(例如,代码可能会在兼容的系统上崩溃[或更糟])? -
如果是这样的话,那么为什么 C说这个行为是不确定的,当时这个
x
应该是零呢?ie这里没有定义行为的优点是什么?
显然,编译器可以简单地使用它在variables中被认为“方便”的任何垃圾值,并且可以按预期工作……这种方法有什么问题?
是的,这种行为是不确定的,但是由于不同于大多数人知道的原因。
首先,使用单位化的值本身不是未定义的行为,但值是简单的不确定的。 如果值恰好是该types的陷阱表示,那么访问它就是UB。 无符号types很less陷阱表示,所以你会相对安全的一面。
是什么使行为未定义是你的variables的一个附加属性,即它“可以用register
声明”,即它的地址永远不会被使用。 这样的variables被特别对待,因为有一些体系结构具有真实的CPU寄存器,这些寄存器具有一种“未初始化”的额外状态,并且不对应于types域中的值。
编辑:标准的相关短语是6.3.2.1p2:
如果左值指定一个可以用寄存器存储类声明的自动存储持续时间的对象(从来没有取得地址),并且该对象是未初始化的(没有用初始化器声明,并且在使用之前没有对它进行赋值),行为是不确定的。
为了使其更清楚,以下代码在任何情况下都是合法的:
unsigned char a, b; memcpy(&a, &b, 1); a -= a;
- 在这里,
a
和b
的地址被采用,所以它们的值只是不确定的。 - 由于
unsigned char
永远不会有陷阱表示,不确定的值只是没有指定,任何值的unsigned char
可能会发生。 - 最后
a
值必须为0
。
编辑2: a
和b
有未指定的值:
3.19.3 未指定的值
本国际标准在任何情况下都没有要求select哪个值的相关types的有效值
C标准为编译器提供了很多自由度来执行优化。 如果您假设一个未初始化的内存被设置为某种随机位模式并且所有操作按照它们被编写的顺序执行的程序的幼稚模型,则这些优化的后果可能是令人惊讶的。
注意:下面的例子是唯一有效的,因为x
从来没有得到它的地址,所以它是“注册状的”。 如果x
的types具有陷阱表示,它们也是有效的; 这对于无符号types来说很less(它需要“浪费”至less一个存储位,并且必须被logging),对于unsigned char
是不可能的。 如果x
具有带符号的types,则实现可以将不是介于 – (2 n-1 -1)和2 n-1 -1之间的数字的位模式定义为陷阱表示。 请参阅Jens Gustedt的回答 。
编译器尝试将寄存器分配给variables,因为寄存器比内存更快。 由于程序可能使用比处理器有更多寄存器的variables,因此编译器会执行寄存器分配,这将导致不同的variables在不同的时间使用相同的寄存器。 考虑程序片段
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
,所以两个variables都可以使用相同的寄存器。 所以这个小程序被编译到寄存器上的以下操作:
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()
,不需要testing条件。 编译器可以像编写代码一样编译这些代码
unsigned i, x; i = 0; /* if some_value() uses i */ x = some_value(); for (i = 1; i < 10; i++) { x = (condition() ? some_value() : -x); }
在编译器内build模的方式是 ,只要x
未初始化,任何取决于x
值都有便利 。 因为当一个未初始化的variables是未定义的,而不是仅仅具有未指定值的variables时,编译器不需要跟踪任何方便的值之间的任何特殊的math关系。 因此编译器可以这样分析上面的代码:
- 在第一次循环迭代期间,
x
被评估的时间-x
被初始化。 -
-x
具有未定义的行为,所以它的值是任何方便的。 - 优化规则
condition ? value : value
condition ? value : value
适用,所以这个代码可以简化为condition ; value
condition ; value
。
当遇到你的问题中的代码时,同样的编译器会分析,当x = - x
被计算时, -x
的值是无论如何方便的。 所以分配可以被优化掉。
我没有find一个如上所述的编译器的例子,但这是编译器试图做的优化。 遇到一个我不会感到惊讶。 这是一个与你的程序崩溃的编译器不太合理的例子。 (如果你在某种高级debugging模式下编译你的程序,这可能不太合理。)
此假设编译器将每个variables映射到不同的内存页面,并设置页面属性,以便从未初始化的variables中读取会导致调用debugging器的处理器陷阱。 对variables的任何赋值首先确保其内存页面正常映射。 此编译器不会尝试执行任何高级优化 – 它处于debugging模式,旨在轻松find诸如未初始化的variables之类的错误。 当x = - x
被评估时,右边会产生一个陷阱,并且debugging器启动。
是的,程序可能会崩溃。 例如,有可能是陷阱表示(特定的位模式不能处理),这可能会导致CPU中断,未处理可能会导致程序崩溃。
(6.2.6.1在C11后期草案中说过)某些对象表示不需要表示对象types的值。 如果对象的存储值具有这种表示forms,并且由不具有字符types的左值expression式读取,则行为是未定义的。 如果这样的表示是由副作用产生的,该副作用通过不具有字符types的左值expression式来修改对象的全部或任何部分,则行为是不确定的.50)这样的表示称为陷阱表示。
(这个解释只适用于unsigned int
可以有陷阱表示的平台,这在现实世界的系统中是罕见的;请参阅注释以获取详细信息,并将其引用到导致标准当前措辞的替代或更为常见的原因。
(这个答案的地址是C 1999.对于C 2011,请参阅Jens Gustedt的答案。)
C标准没有说使用未初始化的自动存储持续时间的对象的值是未定义的行为。 C 1999标准在6.7.8 10中说:“如果具有自动存储持续时间的对象没有被显式初始化,则其值是不确定的。”(这一段继续定义如何初始化静态对象,所以只有未初始化的对象我们关心的是自动对象。)
3.17.2将“不确定值”定义为“未指定值或陷阱表示”。 3.17.3将“未指定的值”定义为“本标准对任何情况下select的值没有要求的相关types的有效值”。
所以,如果未初始化的unsigned int x
有一个未指定的值,那么x -= x
必须产生零。 这就产生了一个问题:它是否可能是一个陷阱代表。 访问陷阱值会导致未定义的行为,按照6.2.6.1 5。
某些types的对象可能具有陷阱表示,例如浮点数字的信号NaN。 但是无符号整数是特殊的。 根据6.2.6.2,无符号整数的N个值中的每一个都表示2的幂,并且每个值位的组合代表0到2 N -1中的一个值。 因此,无符号整数只能由于其填充位(如奇偶校验位)中的某些值而具有陷印表示。
如果在目标平台上,unsigned int没有填充位,则未初始化的unsigned int不能有陷阱表示,并且使用它的值不会导致未定义的行为。
是的,这是不确定的。 代码可能会崩溃。 C说这种行为是不确定的,因为没有任何特殊的理由可以对一般规则作出例外。 优点与其他未定义行为的情况相同 – 编译器不必输出特殊的代码就可以实现这一点。
显然,编译器可以简单地使用它在variables中被认为“方便”的任何垃圾值,并且可以按预期工作……这种方法有什么问题?
你为什么认为这不会发生? 这正是采取的方法。 编译器不需要使其工作,但它不是要求使其失败。
对于任何types的variables,由于没有初始化,或者由于其他原因,都有一个不确定的值,下面的代码适用于读取该值的代码:
- 如果variables具有自动存储持续时间并且没有地址,代码将始终调用未定义的行为[1]。
- 否则,如果系统支持给定variablestypes的陷阱表示,则代码将始终调用未定义的行为[2]。
-
否则,如果没有陷阱表示,variables将采取未指定的值。 每次读取variables时,不能保证这个未指定的值是一致的。 但是,它保证不是一个陷阱表示,因此保证不调用未定义的行为[3]。
然后可以安全地使用该值而不会导致程序崩溃,尽pipe这样的代码对于具有陷阱表示的系统是不可移植的。
[1]:C11 6.3.2.1:
如果左值指定一个可以用寄存器存储类声明的自动存储持续时间的对象(从来没有取得地址),并且该对象是未初始化的(没有用初始化器声明,并且在使用之前没有对它进行赋值),行为是不确定的。
[2]:C11 6.2.6.1:
某些对象表示不需要表示对象types的值。 如果对象的存储值具有这种表示forms,并且由不具有字符types的左值expression式读取,则行为是未定义的。 如果这样的表示是由副作用产生的,该副作用通过不具有字符types的左值expression式来修改对象的全部或任何部分,则行为是不确定的.50)这样的表示称为陷阱表示。
[3] C11:
3.19.2
不确定的价值
要么是未指定的值,要么是陷阱表示3.19.3
未指定的值
本国际标准在任何情况下都没有要求select哪个值的相关types的有效值
注意未指定的值不能是陷阱表示。3.19.4
陷阱表示
不需要表示对象types的值的对象表示
虽然许多答案都集中在处理器上,而这些处理器陷入了未初始化的寄存器访问中,但即使在没有这种陷阱的平台上,也可能会出现古怪的行为,使用不费力地开发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之间的值。 标准没有任何术语来描述types为uint16_t的值的行为,但是其值在0到65535范围之外的值的行为,除了说任何可能产生这种行为的行为调用UB。