越界访问数组有多危险?

How dangerous is it to access an array out of bounds?

提问人:ChrisD 提问时间:3/27/2013 最后编辑:Jonathan LefflerChrisD 更新时间:12/23/2022 访问量:58280

问:

访问超出其边界(在 C 中)的数组有多危险?有时可能会发生我从数组外部读取的情况(我现在明白了,然后我访问了程序的其他部分甚至超出该部分使用的内存),或者我正在尝试为数组外部的索引设置值。程序有时会崩溃,但有时只是运行,只会给出意想不到的结果。

现在我想知道的是,这到底有多危险?如果它损坏了我的程序,那还不错。另一方面,如果它破坏了我的程序之外的某些东西,因为我以某种方式设法访问了一些完全不相关的内存,那么我想这是非常糟糕的。 我读过很多“任何事情都可能发生”、“分段可能是最不坏的问题”、“你的硬盘可能会变成粉红色,独角兽可能在你的窗户下唱歌”,这些都很好,但真正的危险是什么?

我的问题:

  1. 从数组外部读取值会损坏任何东西吗? 除了我的程序?我想只要看东西就可以了 不更改任何内容,或者它会更改“上次” 我碰巧到达的文件的 opened' 属性?
  2. 在数组之外设置值会损坏除我之外的任何东西吗? 程序?从这个 Stack Overflow 问题中,我收集到可以访问 任何内存位置,都没有安全保证。
  3. 我现在在 XCode 中运行我的小程序。这样做 在我的程序周围提供一些额外的保护,如果它不能 超出自己的记忆?它会伤害 XCode 吗?
  4. 关于如何安全地运行我固有的错误代码的任何建议?

我使用 OSX 10.7、Xcode 4.6。

C 数组 内存

评论

0赞 Hot Licks 3/27/2013
通常,操作系统将保护自身和其他进程免受渎职行为的侵害。不过,这并不是你一定想要严重依赖的东西。
7赞 DrummerB 3/27/2013
此外,在访问和数组索引越界(在 ram 中)时,您永远不会“碰巧到达”硬盘上的文件。
1赞 Bryan Chen 3/27/2013
我相信你在问 C 数组,对吧?所以这与 ObjC 无关,也与任何 IDE 无关。
18赞 phipsgabler 3/27/2013
这是我最喜欢的奇怪结果示例(它涉及堆栈,但我发现它真的很有启发性......
11赞 Dan Is Fiddling By Firelight 3/27/2013
xkcd.com/371

答:

1赞 jbgs 3/27/2013 #1

除了你自己的程序,我认为你不会破坏任何东西,在最坏的情况下,你会尝试从与内核未分配给你的进程的页面相对应的内存地址读取或写入,生成适当的异常并被杀死(我的意思是,你的进程)。

评论

3赞 Ed S. 3/27/2013
..什么?在你自己的进程中覆盖内存,用于存储一些稍后使用的变量......它现在神秘地改变了它的价值!我向你保证,追踪这些错误非常有趣。段错误将是最好的结果。-1
2赞 jbgs 3/27/2013
我的意思是,除了他自己的程序;)之外,他不会“破坏”其他进程
0赞 ChrisD 3/27/2013
我确实不在乎我是否破坏了自己的程序。我只是在学习,如果我访问任何超出数组范围的内容,程序显然是错误的。我只是越来越担心在调试我的创作时破坏其他东西的风险
0赞 ChrisD 3/27/2013
问题是:如果我尝试访问未分配给我的内存,我是否可以确定我的进程将被终止?(在OSX上)
3赞 jbgs 3/27/2013
几年前,我曾经是一个笨拙的 C 程序员。我访问了超出其边界的数组数百次。除了我的进程作系统杀死之外,什么也没发生。
8赞 mikyra 3/27/2013 #2

不以 root 或任何其他特权用户身份运行程序不会损害您的任何系统,因此通常这可能是一个好主意。

通过将数据写入某个随机内存位置,您不会直接“损坏”计算机上运行的任何其他程序,因为每个进程都在自己的内存空间中运行。

如果尝试访问任何未分配给进程的内存,操作系统将停止程序执行,并出现分段错误。

因此,直接(无需以 root 身份运行并直接访问 /dev/mem 等文件)不会有程序干扰操作系统上运行的任何其他程序的危险。

然而 - 可能这就是你听说过的危险 - 通过盲目地将随机数据意外地写入随机内存位置,你肯定会损坏任何你能损坏的东西。

例如,程序可能希望删除由存储在程序中某处的文件名给出的特定文件。如果不小心覆盖了文件名的存储位置,则可以删除一个非常不同的文件。

评论

1赞 John Bode 3/27/2013
但是,如果您 root(或其他特权用户)身份运行,请注意。缓冲区和数组溢出是一种常见的恶意软件漏洞。
0赞 ChrisD 3/27/2013
实际上,我用于所有日常计算的帐户不是管理员帐户(我使用OSX术语,因为这是我的系统)。你的意思是告诉我,我不可能通过尝试设置任何内存位置来损坏某些东西吗?这其实是个好消息!
0赞 mikyra 3/27/2013
如前所述,意外造成的最严重的伤害是您作为用户可能造成的最严重的伤害。如果您想 100% 确定不会破坏您的任何数据,您可能希望将不同的帐户添加到您的计算机并进行实验。
1赞 Keith Thompson 3/27/2013
@mikyra:只有当系统的保护机制100%有效时,这才是正确的。恶意软件的存在表明您不能总是依赖它。(我不想说这一定是值得担心的;程序可能会意外地利用恶意软件所利用的相同安全漏洞,但可能性不大。
1赞 mikyra 3/27/2013
此处的列表包括:从不受信任的来源运行代码。只需单击防火墙任何弹出窗口中的“确定”按钮,甚至无需阅读它的内容,或者如果无法建立所需的网络连接,则将其完全关闭。使用来自可疑来源的最新黑客修补二进制文件。如果主人自愿邀请任何双臂和超坚固的防卫门敞开的窃贼,这不是金库的错。
9赞 Udo Klein 3/27/2013 #3

你写:

我读过很多“任何事情都可能发生”、“细分可能是 最不坏的问题“,”你的硬盘可能会变成粉红色,独角兽可能会变成粉红色 在你的窗下唱歌“,这很好,但真正是什么 危险?

让我们这样说吧:装枪。将它指向窗外,没有任何特定的目标和火力。有什么危险?

问题是你不知道。如果你的代码覆盖了导致程序崩溃的内容,你没事,因为它会阻止它进入定义的状态。但是,如果它没有崩溃,那么问题就会开始出现。哪些资源在您的程序的控制之下,它可能对它们产生什么影响?我知道至少有一个主要问题是由这种溢出引起的。问题出在一个看似毫无意义的统计函数中,该函数搞砸了生产数据库的一些不相关的转换表。结果是事后进行了一些非常昂贵的清理工作。实际上,如果此问题会格式化硬盘,那么处理起来会便宜得多,也更容易处理......换句话说:粉红色的独角兽可能是你最不成问题的问题。

您的操作系统将保护您的想法是乐观的。如果可能的话,尽量避免越界写入。

评论

0赞 ChrisD 3/27/2013
好吧,这正是我所害怕的。我会“尽量避免越界写作”,但是,看看我过去几个月一直在做的事情,我肯定会做很多事情。你们是如何在没有安全练习方式的情况下在编程方面变得如此出色的?
4赞 Udo Klein 3/27/2013
谁说任何事情都是安全的;)
4赞 Richard Brown 3/27/2013 #4

NSArray在 Objective-C 中,被分配了一个特定的内存块。超出数组的边界意味着您将访问未分配给阵列的内存。这意味着:

  1. 此内存可以具有任何值。无法根据数据类型知道数据是否有效。
  2. 此内存可能包含敏感信息,例如私钥或其他用户凭据。
  3. 内存地址可能无效或受保护。
  4. 内存的值可能会发生变化,因为它正在被另一个程序或线程访问。
  5. 其他事物使用内存地址空间,例如内存映射端口。
  6. 将数据写入未知内存地址可能会使程序崩溃,覆盖操作系统内存空间,并且通常会导致太阳内爆。

从程序的角度来看,您总是想知道代码何时超出了数组的边界。这可能会导致返回未知值,从而导致应用程序崩溃或提供无效数据。

评论

0赞 DrummerB 3/27/2013
NSArrays有越界例外。这个问题似乎是关于C数组的。
0赞 ChrisD 3/27/2013
我确实是指 C 数组。我知道有 NSArray,但现在我的大部分练习都是用 C 语言进行的
26赞 trumpetlicks 3/27/2013 #5

通常,当今的操作系统(无论如何都是流行的操作系统)使用虚拟内存管理器在受保护的内存区域中运行所有应用程序。事实证明,简单地读取或写入已分配/分配给您的流程的区域之外的 REAL 空间中的位置并不是非常容易(本身)。

直接回答:

  1. 读取几乎永远不会直接损坏另一个进程,但是如果您碰巧读取了用于加密、解密或验证程序/进程的 KEY 值,它可能会间接损坏进程。如果根据正在读取的数据做出决策,则越界读取可能会对代码产生一些不利/意外的影响

  2. 通过写入内存地址可访问的位置来真正损坏某些东西的唯一方法是,如果您正在写入的内存地址实际上是硬件寄存器(实际上不是用于数据存储的位置,而是用于控制某些硬件的位置),而不是RAM位置。事实上,你通常不会损坏某些东西,除非你正在编写一些不可重写的一次性可编程位置(或类似性质的东西)。

  3. 通常,从调试器中运行会在调试模式下运行代码。在调试模式下运行往往会(但并非总是)在您做了一些被认为不合时宜或完全非法的事情时更快地停止代码。

  4. 永远不要使用宏,使用已经内置了数组索引边界检查的数据结构,等等。

附加我应该补充一点,上述信息实际上仅适用于使用具有内存保护窗口的操作系统的系统。如果为嵌入式系统编写代码,甚至是使用没有内存保护窗口(或虚拟寻址窗口)的操作系统(实时或其他)的系统编写代码,则在读取和写入内存时应更加谨慎。此外,在这些情况下,应始终采用 SAFE 和 SECURE 编码实践来避免安全问题。

评论

4赞 Nik Bougalis 3/27/2013
始终采用安全可靠的编码实践。
3赞 Eugene 3/27/2013
我建议不要对有缺陷的代码使用 try/catch,除非您捕获非常具体的异常并知道如何从中恢复。Catch(...) 是你可以添加到错误代码中的最糟糕的东西。
1赞 trumpetlicks 3/27/2013
@NikBougalis - 我完全同意,但如果操作系统不包括内存保护/虚拟地址空间,或者缺少操作系统,那就更重要了:-)
0赞 trumpetlicks 3/27/2013
@Eugene - 我从来没有注意到这对我来说是一个问题,但我同意你的看法,我已经把它编辑掉了:-)
0赞 ChrisD 3/27/2013
1)你的意思是损害,因为我会透露一些应该保密的东西?2)我不确定我是否明白你的意思,但我想我只是在尝试访问数组边界之外的位置来访问RAM?
135赞 Keith Thompson 3/27/2013 #6

就 ISO C 标准(语言的官方定义)而言,访问超出其边界的数组具有“未定义的行为”。这句话的字面意思是:

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

一份非规范性说明对此进行了扩展:

可能的未定义行为范围从忽略情况 完全具有不可预测的结果,在翻译过程中的行为 或以记录的方式执行程序,其特征 环境(无论是否发出诊断消息),以 终止翻译或执行(通过签发 诊断消息)。

这就是理论。现实情况如何?

在“最佳”情况下,您将访问当前正在运行的程序拥有的某些内存(这可能会导致程序行为异常),或者属于当前正在运行的程序(这可能会导致程序崩溃并出现分段错误)。或者,您可以尝试写入程序拥有的内存,但这被标记为只读;这也可能导致您的程序崩溃。

这是假设您的程序在尝试保护并发运行的进程彼此隔离的操作系统下运行。如果你的代码运行在“裸机”上,比如说它是操作系统内核或嵌入式系统的一部分,那么就没有这样的保护;您的行为不端代码本应提供这种保护。在这种情况下,损坏的可能性要大得多,在某些情况下,包括对硬件(或附近的事物或人员)的物理损坏。

即使在受保护的操作系统环境中,保护也并不总是 100%。例如,存在允许非特权程序获取 root(管理)访问权限的操作系统错误。即使使用普通用户权限,出现故障的程序也会消耗过多的资源(CPU、内存、磁盘),从而可能导致整个系统瘫痪。许多恶意软件(病毒等)利用缓冲区溢出来获得对系统的未经授权的访问。

(一个历史例子:我听说在一些具有核心内存的旧系统上,在紧密循环中重复访问单个内存位置可能会导致该内存块融化。其他可能性包括破坏 CRT 显示器,以及使用驱动器机柜的谐波频率移动磁盘驱动器的读/写磁头,使其穿过桌子并掉到地板上。

而且总是有天网需要担心。

底线是这样的:如果你可以编写一个程序来故意做一些坏事,那么至少从理论上讲,一个有缺陷的程序可能会意外地做同样的事情。

在实践中,在MacOS X系统上运行的有缺陷的程序不太可能做任何比崩溃更严重的事情。但是,要完全防止有缺陷的代码做非常糟糕的事情是不可能的。

评论

1赞 ChrisD 3/27/2013
谢谢,我其实完全理解这一点。但它立即引发了一个后续问题:一个初级程序员可以做些什么来保护他/她的计算机免受他/她自己可能可怕的创作的影响?在我彻底测试了一个程序之后,我可以在世界上释放它。但第一次试运行注定是一个不正确的程序。你们如何保护自己的系统安全?
7赞 Keith Thompson 3/27/2013
@ChrisD:我们往往很幸运。8-)} 说真的,现在的操作系统级保护相当不错。最坏的情况是,如果我写了一个意外的分叉炸弹,我可能不得不重新启动才能恢复。但是,对系统的真正损害可能不值得担心,只要你的程序没有试图在危险的边缘做一些事情。如果您真的担心,在虚拟机上运行该程序可能不是一个坏主意。
2赞 Keith Thompson 3/27/2013
另一方面,我看到在我使用的计算机上发生了很多奇怪的事情(损坏的文件、不可恢复的系统错误等),我不知道其中有多少可能是由某些 C 程序引起的,这些程序表现出可怕的未定义行为。(到目前为止,还没有真正的恶魔从我的鼻子里飞出来。
1赞 ChrisD 3/27/2013
谢谢你教我分叉炸弹 - 在尝试掌握递归:)时,我已经做了接近于此的事情
2赞 Mooing Duck 7/23/2014
scientificamerican.com/article/......因此,现代电子产品仍然可能发生火灾。
2赞 Kaz 3/27/2013 #7

我正在使用一个 DSP 芯片的编译器,该编译器故意生成代码,该代码会从 C 代码中访问数组末尾的代码,但不会!

这是因为循环的结构是这样的,因此迭代的末尾会为下一次迭代预取一些数据。因此,在上次迭代结束时预取的基准从未实际使用过。

像这样编写 C 代码会调用未定义的行为,但这只是标准文档的一种形式,它关注的是最大的可移植性。

更常见的情况是,越界访问的程序没有得到巧妙的优化。这简直是越野车。代码获取一些垃圾值,与上述编译器的优化循环不同,代码随后在后续计算中使用该值,从而损坏了该值。

捕获这样的错误是值得的,因此即使仅仅因为这个原因,也值得使行为未定义:以便运行时可以生成诊断消息,例如“main.c 的第 42 行中的数组溢出”。

在具有虚拟内存的系统上,可能碰巧分配了一个数组,以便后面的地址位于虚拟内存的未映射区域中。然后,访问将轰炸程序。

顺便说一句,请注意,在 C 语言中,我们被允许创建一个指针,该指针位于数组的末尾。并且此指针必须比任何指针与数组内部的比较都大。 这意味着 C 实现不能将数组放在内存的末尾,其中 1 加号地址会环绕并且看起来比数组中的其他地址小。

然而,访问未初始化或越界值有时是一种有效的优化技术,即使不能最大限度地移植。例如,这就是为什么 Valgrind 工具在发生对未初始化数据的访问时不报告这些访问的原因,而仅在以后以可能影响程序结果的某种方式使用该值时才报告。你会得到一个诊断,比如“xxx:nnn 中的条件分支取决于未初始化的值”,有时很难追踪它的来源。如果所有此类访问都立即被捕获,则编译器优化的代码以及正确的手动优化代码将产生大量误报。

说到这一点,我正在使用来自供应商的一些编解码器,该编解码器在移植到 Linux 并在 Valgrind 下运行时会发出这些错误。但供应商说服了我,实际上只有几个位来自未初始化的内存,而这些被逻辑小心翼翼地避免了。只使用了值的好位,Valgrind 无法追踪到单个位。未初始化的材料来自读取编码数据比特流末尾的单词,但代码知道流中有多少位,并且不会使用比实际更多的比特。由于超出比特流阵列末端的访问不会对DSP架构造成任何损害(阵列之后没有虚拟内存,没有内存映射端口,并且地址不换行),因此它是一种有效的优化技术。

“未定义的行为”实际上并没有多大意义,因为根据 ISO C,简单地包含一个 C 标准中未定义的标头,或者调用程序本身或 C 标准中未定义的函数,都是未定义行为的例子。未定义的行为并不意味着“不被地球上的任何人定义”,只是“不被ISO C标准定义”。但是,当然,有时未定义的行为确实绝对不是由任何人定义的。

评论

0赞 supercat 7/12/2016
此外,如果存在至少一个程序,即使它名义上对标准中给出的所有实现限制征税,也要正确处理该程序,那么当提供给任何其他没有违反约束且仍然“合规”的程序时,该实现可以任意运行。因此,99.999% 的 C 程序(除了平台的“一个程序”之外的任何程序)都依赖于标准没有要求的行为。
0赞 Paul Floyd 3/2/2023
似乎不是一个值得信任的供应商。一般位准确的。这包括位域和二进制逻辑运算符。并非所有整数算术运算符都使用位 accuracry 进行跟踪,您需要该选项。不跟踪浮点的定义。memcheck--expensive-definedness-checks=yes
4赞 Aesin 3/27/2013 #8

在测试代码时,您可能想尝试使用 Valgrind 中的 memcheck 工具——它不会捕获堆栈帧中的单个数组边界冲突,但它应该捕获许多其他类型的内存问题,包括那些会导致单个函数范围之外的微妙、更广泛的问题的问题。

从手册:

Memcheck 是一个内存错误检测器。它可以检测 C 和 C++ 程序中常见的以下问题。

  • 访问不应访问的内存,例如堆块溢出和运行不足,堆栈顶部溢出,以及在释放内存后访问内存。
  • 使用未定义的值,即尚未初始化的值,或已从其他未定义值派生的值。
  • 不正确地释放堆内存,例如双重释放堆块,或者 malloc/new/new[] 与 free/delete/delete[] 的使用不匹配
  • 在 memcpy 和相关函数中重叠 src 和 dst 指针。
  • 内存泄漏。

伊塔:不过,正如 Kaz 的回答所说,它不是灵丹妙药,并不总是能提供最有用的输出,尤其是当你使用令人兴奋的访问模式时。

评论

0赞 ChrisD 3/27/2013
我怀疑 XCode 的分析器会找到其中的大部分?我的问题不是如何找到这些错误,而是执行仍然具有这些错误的程序是否对未分配给我的程序的内存有危险。我必须执行程序才能看到发生的错误
11赞 che 3/27/2013 #9

不检查边界会导致丑陋的副作用,包括安全漏洞。其中一个丑陋的问题是任意代码执行。在经典示例中:如果你有一个固定大小的数组,并且用于将用户提供的字符串放在那里,则用户可以给你一个字符串,该字符串会溢出缓冲区并覆盖其他内存位置,包括函数完成时 CPU 应返回的代码地址。strcpy()

这意味着您的用户可以向您发送一个字符串,该字符串将导致您的程序基本上调用 ,这会将其变成 shell,在您的系统上执行他想要的任何内容,包括收集您的所有数据并将您的计算机变成僵尸网络节点。exec("/bin/sh")

有关如何做到这一点的详细信息,请参阅粉碎堆栈以获得乐趣和利润

评论

0赞 ChrisD 3/28/2013
我知道我不应该访问超出边界的数组元素,感谢您强调这一点。但问题是,除了对我的程序造成各种伤害之外,我是否无意中超越了我的程序的记忆?我的意思是在OSX上。
0赞 che 3/28/2013
@ChrisD:OS X是一个现代操作系统,因此它将为您提供全面的内存保护。例如,你不应该被限制在你的程序被允许做什么。这不应该包括扰乱其他进程(除非你在root权限下运行)。
0赞 Ruslan 7/10/2015
我宁愿说在环 0 特权下,而不是 root 特权。
0赞 supercat 6/24/2016
更有趣的是,超现代的编译器可能会决定,如果代码在之前使用数组长度检查来执行或跳过一段代码后尝试通读,编译器应该可以无条件地自由运行其他代码,即使应用程序拥有数组之外的存储,并且读取它的效果是良性的, 但调用其他代码的效果不会。foo[0]foo[len-1]len
3赞 Dan Haynes 4/4/2013 #10

如果你曾经做过系统级编程或嵌入式系统编程,如果你写入随机的内存位置,可能会发生非常糟糕的事情。较旧的系统和许多微控制器使用内存映射 IO,因此写入映射到外设寄存器的内存位置可能会造成严重破坏,尤其是在异步完成的情况下。

一个例子是对闪存进行编程。存储芯片上的编程模式是通过将特定值序列写入芯片地址范围内的特定位置来启用的。如果另一个进程在写入芯片中的任何其他位置时,将导致编程周期失败。

在某些情况下,硬件会将地址包裹起来(地址的最高有效位/字节将被忽略),因此写入物理地址空间末尾之外的地址实际上会导致数据被写入到事物的中间。

最后,像 MC68000 这样的旧 CPU 可以锁定到只有硬件重置才能让它们再次运行的地步。几十年来没有对它们进行过研究,但我相信当它在尝试处理异常时遇到总线错误(不存在内存)时,它只会停止,直到断言硬件重置。

我最大的建议是为一个产品做一个公然的插头,但我对它没有个人兴趣,我与他们没有任何关系 - 但基于几十年的 C 编程和嵌入式系统,其中可靠性至关重要,Gimpel 的 PC Lint 不仅会检测这些错误, 它会通过不断喋喋不休地喋喋不休地谈论坏习惯,使你成为一个更好的 C/C++ 程序员。

我还建议您阅读 MISRA C 编码标准,如果您可以从某人那里获得副本。我没有看到任何最近的,但在过去,他们很好地解释了为什么你应该/不应该做他们所涵盖的事情。

不知道你,但是大约第二次或第三次我从任何应用程序收到核心转储或挂断时,我对任何公司生产它的看法都会下降一半。第 4 次或第 5 次,无论包装是什么,我都会变成货架,我把一根木桩穿过包装/光盘的中心,它进来只是为了确保它永远不会回来困扰我。

评论

0赞 supercat 6/24/2016
根据系统的不同,超出范围的读取也可能触发不可预知的行为,或者它们可能是良性的,尽管超出范围负载上的良性硬件行为并不意味着良性编译器行为。
0赞 supercat 5/25/2021 #11

具有两个或更多维度的数组会带来超出其他答案中提到的考虑因素。请考虑以下功能:

char arr1[2][8];
char arr2[4];
int test1(int n)
{
  arr1[1][0] = 1;
  for (int i=0; i<n; i++) arr1[0][i] = arr2[i];      
  return arr1[1][0];
}
int test2(int ofs, int n)
{
  arr1[1][0] = 1;
  for (int i=0; i<n; i++) *(arr1[0]+i) = arr2[i];      
  return arr1[1][0];
}

gcc 处理第一个函数的方式不允许尝试写入 arr[0][i] 可能会影响 arr[1][0] 的值,并且生成的代码无法返回硬编码值 1 以外的任何值。尽管该标准将 的含义定义为精确等价于 ,但 gcc 似乎以不同的方式解释数组边界和指针衰减的概念,这些概念涉及对数组类型的值使用 [] 运算符,而不是使用显式指针算术。array[index](*((array)+(index)))

0赞 AnArrayOfFunctions 12/23/2022 #12

我只想为这个问题添加一些实际的例子 - 想象一下下面的代码:

#include <stdio.h>

int main(void) {
    int n[5];
    n[5] = 1;

    printf("answer %d\n", n[5]);

    return (0);
}

具有未定义的行为。例如,如果启用 clang 优化 (-Ofast),则会产生如下结果:

answer 748418584

(如果你没有编译,可能会输出正确的结果answer 1)

这是因为在第一种情况下,对 1 的赋值实际上从未在最终代码中组装(您也可以查看 godbolt asm 代码)。

(但是,必须注意的是,按照这个逻辑,main 甚至不应该调用 printf,所以最好的建议是不要依赖优化器来解决你的 UB - 而是要知道有时它可能会以这种方式工作)

这里的要点是,现代 C 优化编译器将假设未定义行为 (UB) 永远不会发生(这意味着上面的代码类似于(但不相同):

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n[5];

    if (0)
        n[5] = 1;

    printf("answer %d\n", (exit(-1), n[5]));

    return (0);
} 

相反,这是完美的定义)。

这是因为第一个条件语句永远不会达到其真实状态(始终为 false)。0

在第二个参数上,我们有一个序列点,在该序列点之后,我们调用该序列点,程序在第二个逗号运算符中调用 UB 之前终止(因此定义良好)。printfexit

因此,第二个要点是,只要UB从未被实际评估过,它就不是UB。

此外,我没有看到这里提到有相当现代的 Undefined Behaviour 清理器(至少在 clang 上),它(使用选项 -fsanitize=undefined)将在第一个示例(但不是第二个示例)上给出以下输出:

/app/example.c:5:5: runtime error: index 5 out of bounds for type 'int[5]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.c:5:5 in 
/app/example.c:7:27: runtime error: index 5 out of bounds for type 'int[5]'
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior /app/example.c:7:27 in 

以下是 godbolt 中的所有示例:

https://godbolt.org/z/eY9ja4fdh(第一个示例,没有标志)

https://godbolt.org/z/cGcY7Ta9M(第一个示例和 -Ofast clang)

https://godbolt.org/z/cGcY7Ta9M(第二个示例和UB消毒剂)

https://godbolt.org/z/vE531EKo4(第一个示例和UB消毒剂)