Attention people who think UB is fine

Attention people who think UB is fine

After my post on undefined behavior in C some people insisted that it’s demonstrably fine, we know what the code will do, and at worst we’ll get a segfault. This is false. Here’s an example.

在我发布关于 C 语言中未定义行为(UB)的文章后,有些人坚持认为这显然没问题,我们知道代码会做什么,最坏的情况也就是触发段错误(segfault)。这是错误的。以下是一个例子。

C code

#include<utility>
// Let's pretend this is a memory mapped device controlling
// motors and gears and stuff.
volatile int device_command = 0;

void serve_hot_chocolate() {
    device_command = 42;
    // Oh no, we spilled some hot chocolate, invoking UB.
    //
    // The definition of `std::unreachable()` is:
    // Invokes undefined behavior at a given point.
    std::unreachable();
}

void explode() {
    device_command = 3;
}

C 代码

#include<utility>
// 假设这是一个控制电机、齿轮等设备的内存映射设备。
volatile int device_command = 0;

void serve_hot_chocolate() {
    device_command = 42;
    // 糟糕,我们洒了一些热巧克力,触发了 UB。
    //
    // `std::unreachable()` 的定义是:
    // 在给定点调用未定义行为。
    std::unreachable();
}

void explode() {
    device_command = 3;
}

Example assembly output With GCC 16.2 with -O1. “serve_hot_chocolate()”: mov DWORD PTR “device_command”[rip], 42 “explode()”: mov DWORD PTR “device_command”[rip], 3 ret “device_command”: .zero 4

汇编输出示例(使用 GCC 16.2 和 -O1 优化)。 “serve_hot_chocolate()”: mov DWORD PTR “device_command”[rip], 42 “explode()”: mov DWORD PTR “device_command”[rip], 3 ret “device_command”: .zero 4

Ruh roh. Exploding chocolate! The function explode() will be executed, even though it was never called! godbolt link

糟糕。巧克力爆炸了!函数 explode() 竟然被执行了,尽管它从未被调用过!godbolt 链接

Another example: dereferencing a null pointer Some people insisted that dereferencing a null pointer is fine. It’ll just try to access address zero! And these people supposedly know what happens when you try: if nothing is mapped there, you’ll get a segfault. Again, not true.

另一个例子:解引用空指针 有些人坚持认为解引用空指针没问题。它只会尝试访问地址零!这些人自以为知道尝试这样做会发生什么:如果那里没有映射任何东西,你就会得到一个段错误。再次重申,这不是真的。

Let’s say we change std::unreachable() to int* ptr = nullptr; *ptr = 99; in the previous example. Now we get this: “serve_hot_chocolate()”: mov DWORD PTR “device_command”[rip], 42 mov DWORD PTR ds:0, 0 ud2

假设我们将上一个例子中的 std::unreachable() 改为 int* ptr = nullptr; *ptr = 99;。现在我们得到: “serve_hot_chocolate()”: mov DWORD PTR “device_command”[rip], 42 mov DWORD PTR ds:0, 0 ud2

Curious, eh? First it writes zero, not 99, to memory address zero. Then it crashes the program using an invalid instruction. The number 99 never appears in the generated program. But the null pointer write does. Presumably the null pointer write is done in order to improve debugger error messages.

很有趣,对吧?它首先向内存地址零写入了零,而不是 99。然后它使用一条无效指令使程序崩溃。数字 99 从未出现在生成的程序中,但空指针写入操作却出现了。推测空指针写入是为了改善调试器的错误信息。

Now let’s check clang with godbolt: “serve_hot_chocolate()”: mov dword ptr [rip + “device_command”], 42 “explode()”: mov dword ptr [rip + “device_command”], 3 ret “device_command”: .long 0

现在让我们用 godbolt 检查一下 clang 的结果: “serve_hot_chocolate()”: mov dword ptr [rip + “device_command”], 42 “explode()”: mov dword ptr [rip + “device_command”], 3 ret “device_command”: .long 0

Again we get exploding chocolate. Let’s do some pointer arithmetic on the null pointer Ok, so what happens if we add ptr += 10. That’s UB for three reasons: using a null pointer for anything other than comparison against other pointers, even if an object would be valid at the null pointer (which it’s not), creating a pointer that points outside that object, or if an array, beyond one-past the array, is UB, even if that were valid (which it’s not), we then dereference the address. Let’s try it.

再次出现了巧克力爆炸。让我们对空指针进行一些指针算术运算。 好的,那么如果我们加上 ptr += 10 会发生什么?这有三个原因导致 UB:将空指针用于除与其他指针比较之外的任何用途;即使空指针处存在有效对象(实际上并没有),创建指向该对象之外的指针;或者如果是数组,指向数组末尾之后的位置也是 UB。即使这些操作是有效的(实际上并不是),我们随后又解引用了该地址。让我们试试看。

“serve_hot_chocolate”: mov DWORD PTR “device_command”[rip], 42 mov DWORD PTR ds:40, 99 ret

“serve_hot_chocolate”: mov DWORD PTR “device_command”[rip], 42 mov DWORD PTR ds:40, 99 ret

Wow. Now it did what someone naively would expect, taking the code at face value. Lucky. Why did GCC (and clang, by the way) not detect that *(ptr + 10) = 99 is equally UB as *ptr = 99, and do the same thing? Wrong question. No compiler is obligated to “detect” UB at all.

哇。现在它做到了某人天真地预期的结果,即按字面意思理解代码。真幸运。为什么 GCC(顺便说一下,还有 clang)没有检测到 *(ptr + 10) = 99 和 *ptr = 99 一样都是 UB,并采取同样的做法呢?问题问错了。没有任何编译器有义务去“检测” UB。

Looks to me like GCC and clang, if we want to anthropomorphize them, saw “oh, null pointer dereference? Well that can’t happen, so I’ll stop emitting instructions for this function, to save on the binary size”. Because of the halting problem, it cannot in the general case know if the code will ever be executed. So what does UB do? Anything.

在我看来,如果我们想把 GCC 和 clang 拟人化,它们就像是看到了:“哦,空指针解引用?这不可能发生,所以我将停止为这个函数生成指令,以节省二进制文件的大小”。由于停机问题,编译器在一般情况下无法知道代码是否会被执行。那么 UB 会做什么?任何事情。

I have now demonstrated three things UB can do, when UB was triggered in the same place: Run whatever happens to be next in memory (exploding chocolate). Store a (for all intents and purposes) random value at address zero (wrote zero, not 99), and then deliberately crash. Do what one would naively expect: write the value to the address seen in the code at face value. And to think that some people don’t believe in nasal demons.

我已经演示了当 UB 在同一位置被触发时,它可能导致的三个结果: 运行内存中恰好位于其后的任何内容(巧克力爆炸)。 在地址零处存储一个(实际上是)随机的值(写入了零,而不是 99),然后故意崩溃。 做人们天真地预期的事:按字面意思将值写入代码中看到的地址。 真不敢相信还有人不相信“鼻中恶魔”(nasal demons,指 UB 导致编译器产生荒谬行为的隐喻)。

disqus has started showing ads. :-( Showing (probably incomplete) comments in a static read-only view. Click button to be able to leave comments. Show comments despite ads

disqus 开始显示广告了。:-( 在静态只读视图中显示(可能不完整的)评论。点击按钮即可发表评论。尽管有广告仍显示评论。