注意那些认为未定义行为没问题的人
在我关于 C 语言中的未定义行为的帖子之后,有些人坚持认为这显然没问题,我们知道代码会做什么,最坏的情况就是会出现段错误。这是错误的。这里有一个例子。 C 代码 #include<utility> // 我们假装这是一种控制电动机、齿轮等设备的内存映射设备。 volatile int device_command = 0 ; void serve_hot_chocolate () { device_command = 42 ; // 哦不,我们洒了一些热巧克力,触发了未定义行为。 // // `std::unreachable()` 的定义是: // 在给定点触发未定义行为。 std :: unreachable (); } void explode () { device_command = 3 ; } 示例汇编输出使用 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 哦哦。爆炸巧克力!即使从未被调用,explode() 函数也将被执行! godbolt 链接 另一个例子:解引用空指针 有些人坚持认为解引用空指针是没问题的。它只是试图访问地址零!这些人显然知道当你尝试时会发生什么:如果那里没有映射,你会得到一个段错误。再一次,这不是真的。假设我们将 std::unreachable() 更改为 int* ptr = nullptr; *ptr = 99; 在之前的例子中。现在我们得到这个: "serve_hot_chocolate()" : mov DWORD PTR "device_command" [rip], 42 mov DWORD PTR ds : 0 , 0 ud2 有趣吧?首先,它将零而不是 99 写入内存地址零。然后它使用无效指令崩溃程序。数字 99 从未出现在生成的程序中。但空指针的写入出现了。据推测,空指针的写入是为了改善调试器的错误信息。现在让我们检查 clang 和 godbolt : "serve_hot_chocolate()" : mov dword ptr [ rip + "device_command" ] , 42 "explode()" : mov dword ptr [ rip + "device_command" ] , 3 ret "device_command" : . long 0 我们又得到了爆炸巧克力。让我们对空指针进行一些指针算术 好吧,所以如果我们添加 ptr += 10 会发生什么。这是未定义行为,有三个原因:使用空指针进行任何除与其他指针比较以外的操作,即使在空指针(实际上并不)有效的情况下,创建指向该对象外部的指针,或者如果是数组,则超出数组内的一个元素,都是未定义的,即使那是有效的(实际上并不是),然后解引用该地址。让我们试试。 "serve_hot_chocolate" : mov DWORD PTR "device_command" [rip], 42 mov DWORD PTR ds : 40 , 99 ret 哇。现在它做了某人天真的期望,按字面意思处理代码。幸运。为什么 GCC(顺便说一下 clang)没有检测到 *(ptr + 10) = 99 和 *ptr = 99 一样是未定义的,并做同样的事情?错误的问题。没有任何编译器有义务“检测”未定义行为。从我的角度来看,GCC 和 clang 如果我们要使它们类人化,看到 “哦,空指针解引用?那是不能发生的,所以我会停止为这个函数生成指令,以节省二进制文件大小”。由于停机问题,它在一般情况下无法知道代码是否会被执行。那么,未定义行为会做什么?任何事情。我现在展示了未定义行为可以做的三件事,当未定义行为在同一位置被触发时:运行内存中下一个发生的事情(爆炸巧克力)。在地址零写入(在所有意图和目的上)随机值(写入零,而不是 99),然后故意崩溃。做一个人天真地期望的事情:将值写入按字面意思在代码中看到的地址。想想看,有些人不相信鼻腔恶魔。有人将此提交给 HN。 disqus 开始显示广告。 :-( 显示(可能不完整)评论的静态只读视图。点击按钮以能够留下评论。
阅读全文 →本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡