读取不确定的值会调用UB?

时间:2016-11-14 08:58:38

标签: c undefined-behavior

SO上的各种受尊敬的高代表用户坚持认为阅读具有不确定价值的变量"总是UB"。那么在C标准中究竟提到了哪一个呢?

很明显,不确定的值可能是未指定的值或陷阱表示:

  

3.19.2
  不确定的价值
  要么是未指定的值,要么是陷阱表示

     

3.19.3
  未指明的价值
  本国际标准规定的相关类型的有效值   在任何情况下选择哪个值的要求
  注意未指定的值不能是陷阱表示。

     

3.19.4
  陷阱表示
  一个对象表示,不需要表示对象类型的值

同样清楚的是,读取陷阱表示会调用未定义的行为,6.2.6.1:

  

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

但是,不确定值不一定包含陷阱表示。实际上,对于使用两个补码的系统来说,陷阱表示非常罕见。

在C标准中,它实际上是说读取不确定的值会调用未定义的行为吗?

我正在阅读C11的非规范性附件J,并发现这确实被列为UB的一个案例:

  

使用具有自动存储持续时间的对象的值   不确定(6.2.4,6.7.9,6.8)。

但是,列出的部分无关紧要。 6.2.4仅规定有关生命时间和变量值变得不确定的规则。类似地,6.7.9涉及初始化并说明变量的值如何变得不确定。 6.8似乎大多无关紧要。这些部分都没有包含任何规范性文本,说访问不确定的值可能导致UB。这是附件J中的缺陷吗?

然而,6.3.2.1中有关于左值的一些相关的规范性文本:

  

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

但这是一种特殊情况,它仅适用于从未使用过地址的自动存储持续时间的变量。我一直认为6.3.2.1的这一部分是UB关于不确定值(不是陷阱表示)的唯一情况。但是人们一直坚持认为,它始终是UB"。那么这究竟提到了什么?

3 个答案:

答案 0 :(得分:0)

据我所知,标准中没有任何内容表明使用不确定的值始终是未定义的行为。

拼写为调用未定义行为的案例是:

  • 如果该值恰好是陷阱表示。
  • 如果不确定值是自动存储的对象。
  • 如果值是指向其生命周期已结束的对象的指针。

例如,C标准指定类型unsigned char没有填充位,因此它的值都不能成为陷阱表示。

memcpy等函数的可移植实现利用这一事实来执行任何值的副本,包括不确定的值。当用作包含填充位的类型的值时,这些值可能是陷阱表示,但当用作unsigned char的值时,它们只是未指定。

我认为假设如果可以调用未定义的行为,那么当程序没有安全的检查方式时,调用未定义的行为是错误的。请考虑以下示例:

int read(int* array, int n, int i)
{       
   if (0 <= i)
       if (i < n)
           return array[i];
   return 0;
}

在这种情况下,read函数无法安全地检查array是否确实属于(至少)长度n。显然,如果编译器将这些可能的UB 操作视为明确的UB ,那么编写任何指针代码几乎是不可能的。

更一般地说,如果编译器无法证明某些东西是UB,则必须假设它不是UB,否则可能会破坏符合规范的程序。

唯一可能被视为确定性的情况是自动存储对象的情况。我认为合理的原因是因为这些情况可以被静态拒绝,因为编译器需要的所有信息都可以通过本地流分析获得。

另一方面,将其声明为非自动存储对象的UB不会在编译或可移植性方面为编译器提供任何有用的信息(在一般情况下)。因此,标准可能没有提到这些情况,因为无论如何它都不会改变实际实现中的任何内容。

答案 1 :(得分:0)

为了最佳地融合优化机会和有用的语义,没有陷阱表示的类型应该将Indeterminate Values细分为三种类型:

  1. 第一次读取将产生任何可能由未指定的值产生的值  位模式;后续将保证产生相同的价值。  除了标准之外,这类似于“未指定的值”  通常不区分有和没有陷阱的类型  表示,以及标准要求“未指定”的情况  价值“它要求实施确保价值不是陷阱  表示;在一般情况下,这将需要一个  实现包括防止某些位模式的代码。

  2. 每次阅读都可以独立产生任何可能导致的值  未指定的位模式。

  3. 读取的值,以及对其执行的大多数计算的结果,  可能表现得非确定性,就像读取产生了任何一样  可能的价值。

  4. 不幸的是,标准没有做出这样的区分,而且有一些区别 对它所要求的不同意见。我建议#2应该是 默认情况下,代码应该可以指示所有代码所在的位置 需要强制编译器选择一个具体的值,并指出一个 编译器可能在其他地方使用#3风格的语义。例如,如果代码为 存储为:

    的不同16位值的集合
    struct COLLECTION { size_t count; uint16_t values[65536], locations[65536]; };
    

    保持每个i&lt;的不变量。 count,locations [values [i]] == i,it 应该可以通过设置“计数”来初始化这样的结构 为零,即使存储先前已被用作其他类型。 如果将强制转换指定为始终产生具体值,则需要代码 看看集合中是否有东西可以使用:

    uint32_t index = (uint32_t)(collection->locations[value]);
    if (index < collection->count && collections->values[index]==value)
      ... value was found
    

    每次从数组中读取一个项目时,上面的代码任意为“index”产生任意数字是可以接受的,但是第二行中“index”的两个使用都必须使用相同的值

    不幸的是,一些编译器编写者似乎认为编译器应将所有不确定值视为#3,而某些算法需要#1而某些算法需要#2,并且没有真正的方法可以区分不同的需求。

答案 2 :(得分:0)

3.19.2允许实现成为陷阱表示,读取和写入都是未定义的行为。

您的平台可能会为您提供保证(例如,整数类型永远不会有陷阱表示),但标准不需要,如果您依赖它,您的代码将失去一些可移植性。这是一个有效的选择,但不应该无知。

更多系统具有浮点类型的陷阱表示而不是整数类型,但C程序可以在跟踪寄存器有效性的处理器上运行 - 请参阅(Why) is using an uninitialized variable undefined behavior in C?。这种程度的自由度是C在许多硬件架构中广泛采用的主要原因。