未指定、未定义和实现定义的行为 C语言 WIKI

12
尽管关于此主题的链接在SO上很丰富,但我认为缺少一份清晰易懂的解释,说明未指定行为(UsB)、未定义行为(UB)和实现定义行为(IDB)之间的区别,并详细说明任何用例和示例。
注意:我为了简洁起见,在这篇WIKI中编写了UsB首字母缩略词,但不要指望在其他地方看到它被使用。
我知道这可能看起来是其他帖子的重复(最接近的一个是this),但在任何人将其标记为重复之前,请考虑我已经找到的所有材料存在哪些问题(我将把这篇文章制作成社区WIKI)。
  • 过多的散乱示例。示例并不是坏事,但有时候人们找不到一个很好地适用于自己问题的示例,所以它们可能会让人感到困惑(尤其是对于新手来说)。

  • 示例通常只有代码,解释很少。在这些微妙的问题上,尤其是对于(相对)新手来说,一种更自上而下的方法可能更好:首先提供一个清晰、简单的解释和一个抽象(但不是法律主义)的描述,然后提供一些简单的示例,并解释为什么它们会触发某些行为。

  • 有些帖子经常混合使用C和C++示例。C和C++有时候对于他们认为的UsB、UB和IDB的定义不一致,因此一个示例可能会误导那些不精通两种语言的人。

  • 当给出UsB、UB和IDB的定义时,通常只是引用标准,这有时可能对新手来说不太清楚或难以理解。

  • 有时候标准的引用是不完整的。许多帖子只引用了与手头问题有关的部分标准,这是好的,但缺乏普遍性。此外,标准的引用通常没有任何解释(对于新手来说不好)。

鉴于我自己对这个主题并不是超级专家,我将创建一个社区WIKI,以便任何感兴趣的人都可以贡献和改进答案。

为了不破坏我的目的,即创建一个结构化的适合初学者的WIKI,我希望张贴者在编辑WIKI时遵循一些简单的指导方针:

  • 分类您的用例。 如果可能的话,请将您的示例/代码放在已存在的类别下,否则创建一个新的类别。

  • 首先是简单的描述。 首先用简单的话语描述(当然不要过于简单化 - 质量第一!)您试图说明的示例或观点。 然后提供代码示例或引用。

  • 引用参考标准。 不要发布各种标准的片段,而是给出清晰的引用(例如C99 WG14 / N ...第1.4.7节,第...段),并发布相关资源的链接,如果可能。

  • 优先选择免费在线资源。 如果您想引用书籍或非免费资源,那么可以(并且可能会提高WIKI的质量),但请尝试添加一些免费资源的链接。这对于ISO标准尤其重要。欢迎添加官方标准的链接,但请尽量添加等效的免费草案链接。请不要替换草案链接为官方标准的引用,请增加它们。甚至有些大学的计算机科学系都没有ISO标准的副本,更不用说大多数程序员了!

  • 仅在必要时发布代码。 仅在仅使用普通英语解释会很尴尬或不清楚时才发布代码。尽量将代码示例限制为一行代码。发布其他SO Q&A的链接。

  • 不要发布C ++示例。 我希望这成为C的常见问题解答(如果有人想开始一个C ++的双线程,那就太好了)。欢迎提供与C ++相关的差异,但仅作为侧注。也就是说,在您彻底说明C案例之后,如果这有助于C程序员转向C ++,则可以添加一些关于C ++的陈述,但我不希望看到超过20%的C ++内容的示例。通常,像“(在这种情况下,C ++的行为不同)”这样的简单说明加上相关链接就足够了。

由于我在SO上还比较新,希望这种方式发起问答没有违反任何规则。如果有的话,请见谅。版主可以让我知道。


1
这似乎对于SO来说有点宽泛。然而,与您的问题相关,我曾经偶然发现了这个有趣的列表:https://www.securecoding.cert.org/confluence/display/seccode/CC.+Undefined+Behavior - asveikau
@Pascal 请再次阅读我的帖子。我从未说过我完全诊断了定义缺乏的问题。我试图解释,我在这个主题上找到了太多信息来源,但它们缺乏组织,有时对新手来说定义过于技术化。至于这不是一个WIKI的事实,我可能误解了这个功能,但当我看到“WIKI”一词时,我认为它应该像一个WIKI一样使用,并且我写了一篇WIKI文章。如果SO wiki实际上不是一个wiki功能,那么很抱歉误解了,但任何人都可以建议关闭这篇文章。 - Lorenzo Donati support Ukraine
1
我喜欢你为此付出的努力,尽管我不确定整个事情是否非常适合在SO上。我更愿意将其视为一种外部资源,例如博客文章,人们可以在需要时链接到相关问题上。 - Andreas Grapentin
我在 Stack Overflow 上一遍又一遍地看到关于这个问题的提问。注意,不是特定的问题,而是表现出对基础知识缺乏理解的问题(这对初学者来说并不容易)。如果这似乎过于宽泛,我很抱歉。在发布之前,我也在 meta-SO 上进行了搜索,尽管有一些批评性的帖子,但并没有明确的使用 wiki 的规则。正如我在回答另一个评论时所说,任何人都可以投票关闭此问题(如果他们发现它真的与主题无关,我不会个人认为有什么问题;-)。另一方面,似乎有些用户发现它很有用,因为我一直收到一些积极的反馈。 - Lorenzo Donati support Ukraine
有没有更好的标签可以用于特定实现行为?标签中的缩写使它们很难被找到。当前状态下该标签是无用的。 - Charles
显示剩余3条评论
2个回答

14

C标准将UsB、UB和IDB定义为如下所述:

未指定行为(UsB)

这是一种行为,标准提供了一些替代方案,实现必须选择其中之一,但它不强制规定选择方式和时间。换句话说,实现必须接受触发该行为的用户代码,而且必须遵守标准给出的其中一个替代方案。

请注意,实现无需记录有关所做选择的任何内容。 这些选择也可以是不确定的或者依赖于编译器选项(以未记录的方式)。

总结一下:标准提供了一些可能性可供选择,实现在选择特定替代方案及其应用时自由把握。

请注意,标准可能提供大量的替代方案。 典型示例是未明确初始化的局部变量的初始值。 标准规定只要它是变量数据类型的有效值,那么该值就是未指定的

具体来说,考虑一个int变量:实现可以选择任何int值,并且这个选择可以完全随机、不确定性,或者取决于实现的心情,无需记录任何有关它的信息。 只要实现在标准规定的限制内保持这种情况就可以了,用户不能投诉。

未定义行为(UB)

如其名称所示,这是一种C标准不强制或保证程序会或应该做什么的情况。所有赌注都取消了。这样的情况:

  • 使一个程序变得错误不可移植

  • 不需要从实现中绝对任何内容

这是一种非常恶劣的情况:只要存在具有未定义行为的代码段,整个程序都被认为是错误的,而且根据标准,实现可以完全忽略

换句话说,UB的存在允许实现完全忽略标准,只要涉及触发UB的程序即可。

请注意,此情况下的实际行为可能涵盖无限范围的可能性,以下绝不是全部列表:

  • 可能会出现编译时错误。
  • 可能会出现运行时错误。
  • 问题将被完全忽略(这可能会导致程序错误)。
  • 编译器将UB代码默默地丢弃为优化。
  • 您的硬盘可能会被格式化。
  • 您的计算机可能会清除您的银行账户并邀请你的女友约会。

希望最后两个(半认真的)内容可以让你对UB的严重性有正确的直觉感受。尽管大多数实现不会插入必要的代码来格式化您的硬盘,但真正的编译器确实会进行优化!

术语说明:有时人们会争论标准视为UB源的某些代码在他们的实现/系统/环境中以文档方式工作,因此它不能真正是UB。这种推理是错误的,但它是一个普遍(并且有点可以理解的)的误解:当UB(以及UsB和IDB)在C上下文中使用时,它是一个技术术语,其精确含义由标准定义。特别是,“未定义”一词失去了其日常含义。因此,展示出现错误或不可移植程序产生“明确定义”行为的示例是没有意义的。如果你尝试这样做,你真的就错过了重点。UB意味着你失去了标准的所有保证。如果您的实现提供了扩展,那么您的保证只有您的实现的保证。如果您使用该扩展,您的程序将不再是符合C程序(从某种意义上说,它不再是C程序,因为它不再遵循标准!)。

未定义行为的有用性

关于UB的一个常见问题是:“如果UB如此可怕,为什么标准没有规定在面对UB时实现会发出错误?”

首先,优化。允许实现不检查UB可能原因允许大量优化,使得C程序非常高效。这是C的一个特色,尽管它为初学者带来了许多陷阱。

其次,标准中存在UB使得符合标准的实现可以提供C扩展而不被视为整体不符合标准。

只要实现在符合程序方面表现得像强制性的一样,它本身就是符合的,尽管它可能提供可在特定平台上使用的非标准设施。当然,使用这些设施的程序将是不可移植的,并且将依赖于文档UB,即标准下的UB行为,但实现将其记录为扩展。

实现定义的行为(IDB

这是一种类似于UsB的描述方式:标准提供了一些选择,实现选择其中一个,但实现必须记录选择的方式

这意味着阅读编译器文档的用户必须得到足够的信息来预测特定情况下会发生什么。

注意,未完全记录IDB的实现不能被视为符合规范。 符合规范的实现必须确切地记录标准声明IDB的任何情况下发生的情况。



未指定行为示例

评估顺序

函数参数

函数参数的评估顺序是未指定的EXP30-C

例如,在 c(a(),b()); 中,函数 a 在 b 之前还是之后被调用是未指定的。唯一的保证是两者在 c 函数之前都已被调用。



未定义行为示例

指针

取消引用空指针

空指针用于表示指针不指向有效内存,因此没有必要尝试通过空指针读取或写入内存。

从技术上讲,这是未定义行为。但是,由于这是一个非常常见的错误源,大多数C环境确保通过空指针取消引用的大多数尝试将立即使程序崩溃(通常是使用分段错误来杀死它)。由于涉及对数组和/或结构体的引用中的指针算术,因此此保护不完美,因此即使使用现代工具,取消引用空指针也可能会格式化硬盘。

取消引用未初始化的指针

与空指针相同,在显式设置其值之前取消引用指针也是UB。与空指针不同,大多数环境不提供任何安全网以防止这种类型的错误,除非编译器可以发出警告。如果您仍然编译代码,则很可能会遇到UB的所有不良后果。

取消引用无效指针

无效指针是包含未分配在任何内存区域内的地址的指针。创建无效指针的常见方法是调用 free() (调用后,指针将无效,这基本上就是调用 free() 的目的),或使用指针算术获得超出已分配内存块限制的地址。

取消引用指针UB的这种最邪恶的变体:没有安全网,没有编译器警告,只有代码可能做任何事情。通常,它确实:大多数恶意软件攻击使用此类程序中的UB行为使程序表现出他们想要的行为(例如安装特洛伊木马,键盘记录器,加密硬盘等)。取消引用无效指针可能导致格式化硬盘的可能性非常真实!

取消constness转换

如果我们将一个对象声明为const,我们向编译器承诺永远不会更改该对象的值。在许多情况下,编译器会发现这种无效的修改并警告我们。但是,如果我们像在此代码段中那样去掉它的常量性:

<code><code>int const a = 42;
...
int* ap0 = &a;      //< error, compiler will tell us
int* ap1 = (int*)&a; //< silences the compiler
...
*ap1 = 43;          //< UB ==> program crash?
</code></code>

编译器可能无法跟踪此无效访问,将代码编译为可执行文件后,只有在运行时才会检测到无效访问并导致程序崩溃。

类别2

在此处放置标题!

在此处放置您的说明!



实现定义行为示例

类别1

在此处放置标题!

在此处放置您的说明!


1
很遗憾,您提供的未指定行为示例并不完全正确。如果一个未初始化的变量可以使用“register”关键字声明,那么对它的访问就是未定义行为(UB),而不是UsB。 - Jens Gustedt
那么我对你使用的措辞有一些异议。“未定义行为”不是行为,而是其缺失。使程序出错的不是“未定义行为”,而是在标准没有定义特定行为的情况下,在某个上下文中使用某种构造等。 - Jens Gustedt
@Jens 谢谢您的评论!正如我所说,我不是超级专家,也不知道那个“register”东西(我从来没有写过需要它的C代码)。在我引用它的上下文中,我需要一个快速而简单的例子,这似乎很合适(正如我所说:C充满了陷阱!:-)。我会尝试在那个地方添加一些提示。 - Lorenzo Donati support Ukraine
@Jens 至于你的第二条评论:我并不完全相信我说错了什么。正如我所说,这应该是一个可以被新手理解的WIKI条目,而且我认为我的话很清楚。我会重新阅读整个内容,看看是否还能更加清晰,而不会让读者感到过于沉重。 - Lorenzo Donati support Ukraine
我将这个例子的编辑工作留给其他人。我专注于保持一般部分的整洁。我在问题中提出了一些指导方针,认为它们可以使其余部分更加统一和有结构。编辑者需要友善地在自己的写作风格和指导方针之间取得平衡,以便保持这个答案的整洁(希望如此)。正如我所说,任何人都可以贡献自己的意见。@Jens已经编辑并改进了我的初始答案,同时仍然专注于新手。这就是我一开始所期望的贡献! - Lorenzo Donati support Ukraine
显示剩余4条评论

2

N1570 是 ISO C 标准的草案,非常接近官方的 ISO 文档。

N1256 是早期的草案,包括了 C99 标准和三个技术勘误的更改。

附录 J 有 5 个部分,每个部分都收集了散布在标准其余部分中的信息:

  • J.1 未指定行为
  • J.2 未定义行为
  • J.3 实现定义行为
  • J.4 区域设置特定行为
  • J.5 常见扩展

很好的附加功能!谢谢! - Lorenzo Donati support Ukraine

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接