返回资讯中心
外部精选
软件工程
#开发者实时流

C 和 C++ 中未定义行为指南

来源标题:A Guide to Undefined Behavior in C and C++

C 和 C++ 属于强不安全语言,执行错误操作会导致整个程序失去意义,而非仅该操作结果不可预测,这类操作被称为具有未定义行为。C FAQ 定义未定义行为为:任何情况都可能发生,标准不作任何要求,程序可能编译失败、执行出错(崩溃或静默产生错误结果)等。

Hacker News 最新andsoitis2026/11/1 · 昨天30 分钟阅读

以下正文由 AI 翻译自 Hacker News 最新(原文版权归原站所有),可能存在偏差,重要内容请以原文为准。

编程语言通常区分正常的程序行为和错误行为。对于图灵完备的语言,我们无法可靠地在离线状态下判断一个程序是否有可能执行错误;我们只能运行它看看结果。

在安全的编程语言中,错误在发生时会被捕获。例如,Java 通过其异常系统在很大程度上是安全的。在不安全的编程语言中,错误不会被捕获。相反,在执行了错误操作之后,程序会继续运行,但以一种静默故障的方式运行,这可能在之后产生可观察的后果。Luca Cardelli 关于类型系统的文章对这些问题的介绍清晰而易懂。C 和 C++ 在很强的意义上是“不安全”的:执行错误操作会导致整个程序失去意义,而不只是错误操作产生一个不可预测的结果。在这类语言中,错误操作被说成具有未定义行为。

C FAQ 对“未定义行为”的定义是这样的:

任何事情都可能发生;标准不施加任何要求。程序可能编译失败,也可能执行不正确(要么崩溃,要么静默地产生错误结果),或者它可能碰巧完全按照程序员的意图执行。

这是一个很好的总结。几乎每个 C 和 C++ 程序员都明白,访问空指针和除以零是错误操作。另一方面,未定义行为的完整含义及其与激进优化编译器的交互,并没有得到充分的认识。本文探讨这些话题。

目前,我们可以忽略编译器的存在。只有“C 实现”——如果该实现符合 C 标准——在执行符合标准的程序时,其行为与“C 抽象机”相同。C 抽象机是 C 标准中描述的一个简单的 C 解释器。我们可以用它来确定任何 C 程序的含义。

程序的执行由简单的步骤组成,例如将两个数相加或跳转到一个标签。如果程序执行中的每一步都具有已定义行为,那么整个执行就是良定义的。注意,即使是良定义的执行,也可能由于未指定行为和实现定义行为而没有唯一结果;我们在这里忽略这两者。

如果程序执行中的任何一步具有未定义行为,那么整个执行就没有意义。这一点很重要:并不是说求值 (1<<32) 有一个不可预测的结果,而是说求值这个表达式的程序的整个执行都是无意义的。另外,也不是说执行在未定义行为发生之前是有意义的:坏的影响实际上可能先于未定义操作发生。

举一个简单的例子,看看这个程序:

#include <limits.h> #include <stdio.h> int main (void) printf ("%d\n", (INT_MAX+1) < 0); return 0;

这个程序是在要求 C 实现回答一个简单的问题:如果我们给最大可表示的整数加一,结果是负数吗?对于 C 实现来说,以下行为是完全合法的:

$ cc test.c -o test $ ./test 1

下面这个也是:

$ cc test.c -o test $ ./test 0

还有这个:

$ cc test.c -o test $ ./test 42

以及这个:

$ cc test.c -o test $ ./test Formatting root partition, chomp chomp

有人可能会说:这些编译器中有些行为不当,因为 C 标准规定关系运算符必须返回 0 或 1。但既然这个程序完全没有意义,实现就可以随心所欲。未定义行为压过 C 抽象机的所有其他行为。

真正的编译器会生成代码来啃掉你的磁盘吗?当然不会,但请记住,实际上未定义行为常常确实会导致坏事,因为许多安全漏洞最初都是具有未定义行为的内存操作或整数操作。例如,访问越界数组元素是经典的栈溢出攻击的关键部分。总结一下:编译器不需要生成格式化你磁盘的代码。而是,在越界数组访问之后,你的计算机会开始执行漏洞利用代码,而那段代码才会格式化你的磁盘。

人们非常常见地会说——或者至少会想——类似这样的话:

x86 的 ADD 指令用于实现 C 的有符号加法操作,当结果溢出时它具有二进制补码行为。我是在 x86 平台上开发的,所以当 32 位有符号整数溢出时,我应该可以期望二进制补码语义。

这是错误的。你实际上是在说这样的话:

有人曾经告诉我,在篮球运动中你不能抱着球跑。我拿了个篮球试了试,完全没问题。他显然不懂篮球。

(这个解释来自 Roger Miller,经由 Steve Summit 转述。)

当然,从物理上拿起篮球跑是完全可能的。在比赛中你也可能侥幸不被发现。然而,这是违反规则的;好球员不会这么做,差球员也不可能长期蒙混过关。在 C 或 C++ 中求值 (INT_MAX+1) 完全一样:它有时可能有效,但别指望一直能蒙混过去。情况其实有点微妙,让我们更详细地看看。

首先,是否存在保证有符号整数溢出时采用二进制补码行为的 C 实现?当然有。例如,许多编译器在关闭优化时会具有这种行为,而 GCC 有一个选项(-fwrapv)可以在所有优化级别上强制实施这种行为。其他编译器则默认在所有优化级别上都具有这种行为。

(原文较长,此处译出前 6000 字,完整内容请点「打开原文」。)

正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。

打开原文
去社区讨论 →