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

17 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).此未定义的行为分类为:可能的符合语言扩展

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

tem*_*def 10

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

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

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

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

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


Ant*_*ala 6

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

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

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

C11添加了一个可选的正交分类:有界未定义行为关键的未定义行为.有界未定义的行为是不执行越界存储的行为,即它不能导致值被写入内存中的任意位置.任何未定义的未定义行为的未定义行为是关键的未定义行为.

IFF __STDC_ANALYZABLE__定义,实施将符合附录N,其中有这样明确的名单关键未定义行为:

  • 对象在其生命周期之外被引用(6.2.4).
  • 对具有两个不兼容声明(6.2.7)的对象执行存储,
  • 指针用来调用其类型是未与被引用的类型(兼容的功能6.2.7,6.3.2.3,6.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.1,7.29.4).

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

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


例如,4标准提示,虽然在标准未定义的模式下行为未定义,但有些实现可能会定义其他标志的行为.例如glibc的支持更多的模式标志,如c,e,mx,并允许设置输入的字符编码与,ccs=charset改性剂(和使流分成宽模式马上).


sup*_*cat 5

某些程序仅用于已知有效的输入,或者至少来自可信赖的来源。其他人不是。某些在仅处理受信任数据时可能有用的优化在处理不受信任数据时是愚蠢和危险的。不幸的是,附件 L 的作者写得太含糊了,但明确的意图是让编译器在使用来自不可信来源的数据时,他们不会做某些愚蠢和危险的“优化”。

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

int32_t triplet_may_be_interesting(int32_t a, int32_t b, int32_t c)
{ 
  return a*b > c;
}
Run Code Online (Sandbox Code Playgroud)

从上下文调用:

#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;
}
Run Code Online (Sandbox Code Playgroud)

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

  1. 的平台上,其中无符号进行比较的速度比进行比较签署,编译器可以推断,因为没有的ab或者c可以是负的,的算术值a*b是非负的,并且因此它可以使用一个无符号的比较,而不是一个符号比较。即使__STDC_ANALYZABLE__非零,这种优化也是允许的。

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

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

在此示例中,如果整数溢出的影响仅限于产生可能无意义的值,并且如果examine_triplet()不必要的调用会浪费时间但无害,则编译器可能能够以在以下情况下不可能的方式进行有用的优化triplet_may_be_interesting编写它是为了不惜一切代价避免整数溢出。因此,积极的“优化”将导致代码效率低于编译器可能使用的自由度来提供一些松散的行为保证。

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