我正在通读C11标准。根据C11标准,未定义的行为被分为四种不同的类型。括号中的数字指的是C标准(C11)中标识未定义行为的子条款。
示例1:程序尝试修改字符串文字(6.4.5)。这种未定义的行为分为:未定义的行为(需要信息/确认)
示例2:左值在计算时不指定对象(6.3.2.1)。这种未定义的行为被分类为:严重的未定义的行为
示例3:对象的存储值不是由允许类型的左值访问的(6.5)。这种未定义的行为被分类为:有界未定义的行为
示例4:在调用fopen函数时,mode参数指向的字符串与指定的字符序列之一(7.21.5.3)不完全匹配。这种未定义的行为被分类为:可能的符合语言扩展
这些分类的含义是什么?这些分类对程序员意味着什么?
发布于 2017-11-05 02:49:30
我只能访问该标准的草案,但从我所阅读的内容来看,这种未定义行为的分类似乎并不是标准所强制的,只有从编译器和环境的角度来看才是重要的,这些编译器和环境特别表明他们想要创建可以更容易地分析不同类型错误的C程序。(这些环境必须定义一个特殊的符号__STDC_ANALYZABLE__。)
这里的关键思想似乎是“越界写”,它被定义为修改数据的写操作,否则这些数据不会作为对象的一部分分配。例如,如果您意外地损坏了现有变量的字节,这并不是越界写入,但是如果您跳到内存的随机区域并使用您喜欢的位模式来修饰它,那么您将执行越界写入。
如果结果是未定义的,则特定行为是有界未定义行为,但永远不会进行越界写入。换句话说,行为是未定义的,但您不会跳到与任何对象或分配的空间无关的随机地址,并将字节放在那里。如果你得到的未定义行为不能保证它不会做越界写入,那么一个行为就是关键的未定义行为。
然后,该标准继续讨论什么可能导致关键的未定义行为。默认情况下,未定义的行为是有界的未定义行为,但UB也有例外,如访问释放的内存或使用未初始化的指针等内存错误,这些行为具有关键的未定义行为。但是请记住,这些分类只存在于选择专门分离这些类型的行为的C实现的上下文中,并且具有意义。除非你的C环境保证它是可分析的,否则所有未定义的行为都可能做任何事情!
我的猜测是,这是为构建驱动程序或内核插件之类的环境而设计的,在这些环境中,您希望能够分析一段代码并说:“好吧,如果你要朝某人的脚开枪,最好是你的脚,而不是我的。”如果使用这些约束编译C程序,运行时环境可以检测极少数被允许为关键的未定义行为的操作,并使这些操作陷入操作系统,并假设所有其他未定义的行为最多只会破坏与程序本身特别相关的内存。
发布于 2017-11-05 02:49:41
所有这些都是行为未定义的情况,即标准"imposes no requirements"。传统上,在未定义的行为中,考虑一种实现(即C编译器+C标准库),可以看到两种未定义的行为:
有时,这些可以通过编译器开关来控制。例如,示例1通常会导致不好的行为-陷阱、崩溃或修改共享值。早期版本的GCC允许使用-fwritable-strings来修改字符串文字;因此,如果给定了该开关,则实现将定义该情况下的行为。
C11增加了一个可选的正交分类:和。有界未定义行为是指不执行out-of-bounds store的行为,即它不会导致值被写入内存中的任意位置。任何不是bounded undefined behaviour is critical undefined behaviour的未定义行为。
如果定义了 __STDC_ANALYZABLE__,则实现将符合appendix L,它具有以下 list of undefined behaviour:
对象在其生存期之外被引用(6.2.4).
*或指针减去数组对象和整数类型生成的结果指向数组对象之外,并用作要计算的一元*运算符的操作数。有人试图通过将左值与非常量限定类型(6.7.3).
jmp_buf变元调用longjmp函数,其中不存在在具有相应jmp_buf变元的程序的同一调用中对setjmp宏的最新调用,或者调用来自另一个执行线程,或者包含调用的函数已在中间终止执行,或者调用在具有可变修改类型的标识符的作用域内,并且执行已将该作用域留在指针的临时realloc值中,该指针引用由对(7.22.3).
函数的调用释放的空间。(7.22.3).
对于有界的未定义行为,除了不允许发生越界写入之外,该标准没有对施加任何要求。
示例1:字符串文字的修改也是。被归类为关键的未定义行为。示例4是critical undefined behaviour -该值不是标准库所期望的值。
例如4,标准提示,虽然在标准未定义的模式的情况下行为是未定义的,但有可能定义其他标志的行为的实现。例如glibc supports many more mode flags,如c,e,m和x,并允许使用,ccs=charset修饰符设置输入的字符编码(并立即将流设置为宽模式)。
发布于 2017-11-08 23:10:56
有些程序仅用于已知有效的输入,或者至少来自可靠的来源。其他人则不是。某些类型的优化在仅处理受信任的数据时可能有用,但在处理不受信任的数据时是愚蠢和危险的。不幸的是,附录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进行带符号比较。但是,可以进行一些优化,特别是当编译器内联函数调用时:
a、b或c都不能是负的,所以a*b的算术值是非负的,因此它可以使用无符号比较而不是有符号比较。即使__STDC_ANALYZABLE__为非零,这种优化也是允许的。x为非零,则x*123456的算术值将大于y的每个可能值,而如果x为零,则x*123456不会大于任何值。因此,它可以用简单的if (x)替换第二个if条件。即使__STDC_ANALYzABLE__不是零,这种优化也是允许的。x的任何值大于17395都会导致整数溢出,因此可以安全地假定x为17395或更小。因此,它可以无条件地执行my_array[x]++;。如果编译器要执行此优化,则不能使用非零值定义__STDC_ANALYZABLE__。附录L旨在解决后一种优化。如果一个实现可以保证溢出的影响将被限制为产生一个可能没有意义的值,那么对于代码来说,处理该值可能没有意义的可能性可能比防止溢出更便宜、更容易。然而,如果溢出可能导致对象的行为就好像它们的值被未来的计算破坏一样,那么程序将无法在事后处理像溢出这样的事情,即使在计算结果最终无关紧要的情况下也是如此。在此示例中,如果整数溢出的影响将仅限于产生可能无意义的值,并且如果不必要地调用examine_triplet()将浪费时间但否则将是无害的,则编译器可能能够以不计任何代价避免整数溢出的方式有效地优化triplet_may_be_interesting。因此,激进的“优化”将导致效率较低的代码,而编译器则利用其自由提供一些松散的行为保证。
如果附件L允许实现提供特定的行为保证(例如,溢出将产生可能无意义的结果,但没有其他副作用),那么它将更加有用。没有一组保证对所有程序都是最优的,但是附录L在其不切实际的拟议陷阱机制上花费的文本数量本可以更好地用于指定宏,以表明各种实现可以提供什么保证。
https://stackoverflow.com/questions/47114183
复制相似问题