不确定行为的不同分类是什么意思?

时间:2017-11-04 18:34:46

标签: c undefined-behavior c11

我正在阅读C11标准。根据C11标准,未定义的行为分为四种不同的类型。带括号的数字指的是C标准(C11)的子条款,用于标识未定义的行为。

示例1:程序尝试修改字符串文字(6.4.5)。此未定义的行为分类为:未定义的行为(需要信息/确认)

示例2:评估时左值不指定对象(6.3.2.1)。此未定义的行为分类为:严重未定义行为

示例3:除了允许类型(6.5)的左值之外,对象的存储值被访问。此未定义的行为分类为:有界未定义行为

示例4:mode函数调用中fopen参数指向的字符串与指定的字符序列之一(7.21.5.3)不完全匹配。此未定义的行为分类为:可能的符合语言扩展

分类的含义是什么?这些分类对程序员有什么影响?

5 个答案:

答案 0 :(得分:10)

我只能访问该标准的草稿,但从我正在阅读的内容来看,似乎这种未定义行为的分类并非标准强制要求,而且仅从编译器和环境的角度来看具体表明他们想要创建C程序,可以更容易地分析不同类型的错误。 (这些环境必须定义一个特殊符号__STDC_ANALYZABLE__。)

这里的关键思想似乎是“越界写入”,它被定义为一种写操作,它修改未以其他方式分配为对象一部分的数据。例如,如果你意外地破坏现有变量的字节,那不是一个超出范围的写入,但是如果你跳到一个随机的内存区域并用你最喜欢的位模式进行装饰你就会执行一个越界写入

如果结果未定义,则特定行为是有界未定义行为,但不会进行越界写入。换句话说,行为是未定义的,但您不会跳转到与任何对象或已分配空间无关的随机地址并将字节放在那里。行为是关键的未定义行为如果你得到未定义的行为,不能保证它不会进行越界写入。

然后标准继续谈论可能导致关键的未定义行为的原因。默认情况下,未定义的行为是有限的未定义行为,但是UB的异常是由内存错误引起的,例如访问解除分配的内存或使用未初始化的指针,这些指针具有严重的未定义行为。但请记住,这些分类只存在并且在C的实现环境中具有意义,它们选择专门分离出这些行为。除非你的C环境保证它是可分析的,否则所有未定义的行为都可以做任何事情!

我的猜测是,这适用于构建驱动程序或内核插件等环境,您希望能够分析一段代码并说“好吧,如果您要用脚射击某人你最好是你的脚你正在拍摄而不是我的!“如果用这些约束编译一个C程序,运行时环境可以检测很少的操作,这些操作是关键的未定义行为并将这些操作陷入操作系统,并假设所有其他未定义的行为最多会破坏与程序本身特别相关的内存。

答案 1 :(得分:6)

所有这些都是行为未定义的情况,即标准"imposes no requirements"。传统上,在未定义的行为中并考虑一个实现(即C编译器+ C标准库),可以看到两种未定义的行为:

  • 不会记录行为的构造,或者会记录导致崩溃的行为,或者行为会不稳定,
  • 构造标准未定义,但实现定义了一些有用的行为。

有时这些可以由编译器开关控制。例如。示例1通常总是导致不良行为 - 陷阱或崩溃,或修改共享值。早期版本的GCC允许一个具有-fwritable-strings的可修改字符串文字;因此,如果给出了该开关,则实现在这种情况下定义了行为。

C11添加了可选的正交分类:bounded undefined behaviourcritical undefined behaviour。有界未定义的行为是不执行out-of-bounds store的行为,即它不能使值写入内存中的任意位置。任何未定义的行为都不是bounded undefined behaviour is critical undefined behaviour

Iff __STDC_ANALYZABLE__已定义,实施将符合appendix L,其中definitive list 严重未定义的行为:

  • 对象在其生命周期之外被引用(6.2.4)。
  • 对具有两个不兼容声明(6.2.7)的对象执行存储,
  • 指针用于调用类型与引用类型(6.2.76.3.2.36.5.2.2)不兼容的函数。
  • 在评估(6.3.2.1)时,左值不指定对象。
  • 程序尝试修改字符串文字(6.4.5)。
  • 一元*运算符的操作数具有无效值(6.5.3.2)。
  • 将指针加到或减去数组对象和整数类型会产生一个指向的结果 超出数组对象,并用作一元*的操作数 被评估的运算符(6.5.6)。
  • 尝试通过使用左值来修改使用const限定类型定义的对象 非const限定类型(6.7.3)。
  • 标准库中定义的函数或宏的参数具有无效值或函数不期望的类型 具有可变数量的参数(7.1.4)。
  • 使用longjmp参数调用jmp_buf函数,其中setjmp宏的最新调用在同一个调用中 具有相应jmp_buf参数的程序不存在, 或者调用来自另一个执行线程,或者 包含调用的函数已终止执行 临时的,或者调用是在标识符的范围内 可变修改的类型和执行已经将该范围留在了 临时(7.13.2.1)。
  • 使用指向通过调用free或realloc函数释放的空间的指针的值(7.22.3)。
  • 字符串或宽字符串实用程序函数访问对象末尾之外的数组(7.24.17.29.4)。

对于有界未定义的行为,标准不强加任何要求除了不允许发生越界之外。

示例1:字符串文字的修改也是。归类为关键的未定义行为。示例4也是关键的未定义行为 - 该值不是标准库所期望的值。

对于示例4,标准提示虽然在模式未由标准定义的情况下行为未定义,但是存在可以定义其他标志的行为的实现。例如glibc supports many more mode flags,例如cemx,并允许使用,ccs=charset修饰符设置输入的字符编码(并立即将流放入宽模式)。

答案 2 :(得分:4)

某些程序仅用于已知有效的输入,或至少来自可信赖的来源。其他人不是。当与不受信任的数据一起使用时,在处理仅受信任数据时可能有用的某些类型的优化是愚蠢且危险的。不幸的是,附件L的作者过于模糊地写了它,但明确的意图是允许编制者他们不会做某些优化""使用来自不可靠来源的数据时,这是愚蠢和危险的。

考虑函数(假设" int"是32位):

int32_t triplet_may_be_interesting(int32_t a, int32_t b, int32_t c)
{ 
  return a*b > c;
}

从上下文调用:

#define SCALE_FACTOR 123456
int my_array[20000];
int32_t foo(uint16_t x, uint16_t y)
{
  if (x < 20000)
    my_array[x]++;
  if (triplet_may_be_interesting(x, SCALE_FACTOR, y))
    return examine_triplet(x, SCALE_FACTOR, y);
  else
    return 0;
}

当编写C89时,32位编译器处理该代码的最常见方式是进行32位乘法,然后与y进行有符号的比较。但是,有一些优化是可能的,特别是如果编译器内联函数调用:

  1. 在无符号比较比签名比较快的平台上,编译器可以推断,因为abc都不是负数,所以算术值a*b的结果是非负的,因此可能使用无符号比较而不是签名比较。即使__STDC_ANALYZABLE__非零,也可以进行此优化。

  2. 编译器同样可以推断,如果x非零,x*123456的算术值将大于y的每个可能值,如果x 1}}为零,然后x*123456不会大于任何值。因此,它可以简单地用if替换第二个if (x)条件。即使__STDC_ANALYzABLE__非零,也可以进行此优化。

  3. 编写者要么将其仅用于受信任数据,要么错误地认为聪明性和愚蠢是反义词,可能会推断,因为任何大于17395的x值都会导致整数溢出,x可以安全地假定为17395或更低。因此,它可以无条件地执行my_array[x]++;。如果编译器执行此优化,则可能无法将__STDC_ANALYZABLE__定义为非零值。 这是附件L旨在解决的后一种优化。如果实现可以保证溢出的影响将限制为产生可能无意义的值,则可能更便宜,更容易代码来处理可能的值是没有意义的,而不是防止溢出。如果溢出可能导致对象的行为就好像它们的值被未来的计算所破坏一样,那么,即使在结果的情况下,程序也无法处理溢出之类的事情。计算最终会变得无关紧要。

  4. 在这个例子中,如果整数溢出的影响仅限于产生可能无意义的值,并且如果不必要地调用examine_triplet()会浪费时间但是否则会无害,编译器可能能够有用地优化triplet_may_be_interesting,如果编写它是为了不惜一切代价避免整数溢出而无法实现。激进 &#34;优化&#34;因此,编译效率会低于使用编译器而不是使用其自由提供一些松散的行为保证的编译器。

    如果附件L允许实现提供特定的行为保证(例如,溢出将产生可能无意义的结果,但没有其他副作用),则附件L将更有用。对于所有程序而言,没有一套保证是最佳的,但附件L在其不切实际的建议陷阱机制上花费的文本数量可能更好地用于指定宏以指示各种实现可以提供什么保证。

答案 3 :(得分:3)

根据cppreference

严重的未定义行为

  

严重UB是未定义的行为,可能执行内存写入或   易失性存储器读出任何对象的边界。有一个程序   关键的未定义行为可能容易受到安全漏洞的影响。

只有以下未定义的行为才是至关重要的:

  
      
  • 访问其生命周期之外的对象(例如通过悬空指针)
  •   
  • 写入声明不兼容的对象
  •   
  • 函数调用函数指针,其类型与它指向的函数类型不兼容
  •   
  • 评估左值表达式,但不指定对象尝试修改字符串文字
  •   
  • 解除引用无效(null,indeterminate等)或者过去结束指针
  •   
  • 通过非常量指针修改const对象
  •   
  • 调用带有无效参数的标准库函数或宏
  •   
  • 调用具有意外参数类型的可变标准库函数(例如,使用类型的参数调用printf   与其转换说明符不匹配)
  •   
  • longjmp,其中没有setjmp调用调用范围,跨线程或从VM类型的范围内。
  •   
  • 对free或realloc
  • 释放的指针的任何使用   
  • 任何字符串或宽字符串库函数都可以访问数组越界
  •   

有界未定义的行为

  

有界UB是未定义的行为,无法执行非法内存   写,虽然它可能陷阱,可能产生或存储不确定   值。

未列为关键的所有未定义行为都是有界限的,包括

  
      
  • 多线程数据竞赛
  •   
  • 使用具有自动存储持续时间的不确定值
  •   
  • 严格别名违规
  •   
  • 未对齐的对象访问
  •   
  • 签名整数溢出
  •   
  • 未测序的副作用修改相同的标量或修改并读取相同的标量
  •   
  • 浮动到整数或指针到整数转换溢出
  •   
  • 按位移位或按位计数过大
  •   
  • 整数除零
  •   
  • 使用void表达式
  •   
  • 直接分配或不精确重叠对象的memcpy
  •   
  • 限制违反
  •   
  • 等。所有未定义的行为都不在关键列表中。
  •   

答案 4 :(得分:2)

我正在阅读C11标准。根据C11标准,未定义的行为分为四种不同的类型。

我想知道你在读什么。 2011 ISO C标准没有提到这四种不同行为的不同分类。实际上,它在 not 中明确表达了不同类型的未定义行为之间的区别。

这是ISO C11第4节第2段:

  

如果“必须”或“不应”要求出现在a。之外   违反了约束或运行时约束,行为是   未定义。此处另有说明未定义的行为   国际标准中的“未定义行为”或由   省略任何明确的行为定义。没有   这三者之间的重点不同;他们都描述了“行为   这是未定义的“。

您引用的所有示例都是未定义的行为,就标准而言,这意味着不多于或少于:

  

行为,使用不可移植或错误的程序构造或   错误的数据,本国际标准没有规定   要求

如果您有其他参考,讨论了不同类型的未定义行为,请更新您的问题以引用它。那么您的问题将是关于该文档的分类系统意味着什么,而不是(仅仅)关于ISO C标准。

您的问题中的一些措辞与C11附件L“可分析性”中的某些信息类似(对于符合C11实施而言是可选的),但您的第一个示例是指“未定义的行为(需要信息/确认)” “,”确认“一词在ISO C标准中没有出现。