【问题标题】:Avoiding undefined behaviour when accessing uninitialised variables访问未初始化的变量时避免未定义的行为
【发布时间】:2016-07-31 14:20:03
【问题描述】:

假设我定义了一个unsigned char foo;,标准保证它没有陷阱表示。根据this answer,在获取地址之前访问它仍然是未定义的行为。这是来自 N1570,“6.3.2.1 左值、数组和函数指示符”第 2 段的参考:

如果左值指定一个自动存储持续时间的对象, 可以用寄存器存储类声明(从来没有 它的地址被占用),并且该对象未初始化(未声明 使用初始化程序并且之前没有对其进行分配 使用),行为未定义。

但是,我想知道volatile unsigned char foo; 是否会调用未定义的行为?将 volatile 限定的变量存储在寄存器中似乎是不可能的。


另外,下面的代码定义好了吗?

unsigned char foo;
*&foo -= *&foo;

这个问题来自第102个脚注:

102) ...... *&E 是函数指示符或等于 E 的左值。......

【问题讨论】:

  • w.r.t volatileregister 和“存储在寄存器中”在 C 中是完全不相关的概念,其中一个并不意味着另一个。 6.7.3:“具有 volatile 限定类型的对象可能会以实现未知的方式进行修改.. 构成对具有 volatile 限定类型的对象的访问是实现定义的。”即volatile 的意义在于超越标准定义的行为。
  • “在地址被占用之前访问它” - 你从哪里得到的?除非使用了变量的地址(例如,应用了& 运算符),否则变量甚至不需要有地址。而*& 与不应用它们中的任何一个相同,请阅读整个脚注 102。
  • @Olaf 但是标准只是写了“......可以用寄存器存储类声明(从未使用过它的地址),......”,并且使用的地址是不需要
  • register 只是防止地址被占用,因此您不能对其应用&。拥有auto volatile register 变量没有任何意义。这不能在外部进行更改(没有链接等),如果编译器可以证明它没有效果,编译器甚至可能会对其进行优化(为什么使用此类代码的延迟循环在非常激进的编译器上可能会出现问题)。其实光靠auto volatile已经很没用了。
  • @Olaf 你的意思是auto volatile register限定符/说明符几乎等同于volatile,唯一的区别是不能取定义的变量的地址?

标签: c language-lawyer undefined-behavior c11


【解决方案1】:

首先要澄清的是,6.3.2.1 中引用的部分解决了一种特殊情况,您有一个未初始化的变量,它不一定分配在一个地址上,因为该地址从未被占用。访问这样的对象是明确未定义的行为。这就是链接的答案所关心的。

如果该特殊情况不适用,因为地址被占用,那么相关部分将是 6.7.9/10:

如果具有自动存储持续时间的对象未初始化 明确地说,它的值是不确定的。

不确定值3.19.2的定义:

未指定的值或陷阱表示

未指定值3.19.3的定义:

本国际标准不强制规定的相关类型的有效值 在任何情况下选择哪个值的要求

注意未指定的值不能是陷阱表示。

最后说访问陷阱表示的部分是 UB,6.2.6.1/5,强调我的:

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

总而言之,不确定的值不一定是陷阱表示,因此,访问未初始化的变量不一定是未定义的行为。


据我所知,标准中没有明确说明无符号字符不能包含陷阱表示。 6.2.6.2 只说它们不能有填充位。尽管在实践中这意味着unsigned char 不能保存陷阱表示,因为为了这样做,它需要填充位或有符号格式。所以在字里行间,我认为可以安全地假设unsigned char 不能包含陷阱表示。


但是,我想知道是否 volatile unsigned char foo;调用未定义的行为?

声明一个变量本身不会调用未定义的行为。如果您访问变量而不对其进行初始化,那么在正常情况下,该值将是不确定的。如果这是否是陷阱表示,则取决于实现。

但是,volatile 是一种特殊情况,不适用。相反,您有 6.7.3/7: "... 什么构成对对象的访问 具有 volatile 限定的类型是实现定义的。”

所以它只是实现定义的行为。不管是什么类型。


另外,下面的代码定义好了吗?

unsigned char foo;
*&foo -= *&foo;

foo 具有不确定的值。它的地址被占用了。因此,这是否是未定义的行为取决于给定系统上的不确定值是否是陷阱表示。如上所述,我认为unsigned char 永远不能成为陷阱表示。

然而您使用了-= 运算符,这将使该表达式等同于

*&foo = *&foo - *&foo; 

二进制- 运算符在计算期间将两个操作数提升为int。如果一开始没有陷阱表示,那么现在提升的操作数中可能会有一些,它们是有符号类型的,可能带有填充位。

意味着这个特定的表达式可以调用未定义的行为。

【讨论】:

  • 提升无符号字符如何合法地产生除 0 到 CHAR_MAX 之间的数字以外的任何内容?
  • @supercat 实际上不会。但我不认为它指定了在未初始化的变量被提升的情况下任何填充位会发生什么。也许他们也没有初始化,在这种情况下他们可以持有一个陷阱表示。
  • 对于“unsigned char”类型的变量,我认为变量产生其他任何东西的余地不大。对于任何其他类型,这取决于您是询问 1990 年代编译器作者还是超现代编译器作者。 1990 年代的人会说,实现的一般情况行为的规范隐含地定义了未定义行为,而超现代编译器编写者坚持让程序员忽略直接生成的代码“自然工作”的极端情况将阻碍“优化",并且标准声称某事是......
  • ...未定义的行为应该被视为授予编译器编写者不可撤销的权利,可以用它做任何他们想做的事情。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-01-28
  • 1970-01-01
  • 2019-03-19
  • 2020-02-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多