当前位置: 首页 > 知识库问答 >
问题:

在释放模式下的安全信任中的有符号整数溢出被认为是未定义的行为吗?

萧元徽
2023-03-14

Rust在调试和发布模式下处理有符号整数溢出的方式不同。当它发生时,Rust在调试模式下会恐慌,而在发布模式下会默默地执行两个补码的包装。

据我所知,C/C将有符号整数溢出视为未定义的行为,部分原因是:

  1. 在C标准化的时候,表示有符号整数的不同底层架构,例如补码,可能仍在某个地方使用。编译器不能假设硬件中如何处理溢出。
  2. 后来的编译器因此做出假设,例如两个正整数的总和也必须是正的,以生成优化的机器代码。

因此,如果Rust编译器确实在有符号整数方面执行与C/C编译器相同的优化,为什么The Rustonomicon声明:

无论如何,Safe Rust都不会导致未定义的行为。

或者,即使 Rust 编译器不执行此类优化,Rust 程序员仍然不希望看到有符号整数环绕。难道不能称之为“未定义的行为”吗?

共有1个答案

杜志
2023-03-14

问:因此,如果 Rust 编译器确实执行与 C/C 编译器相同的有符号整数优化

生锈没有。因为,正如您所注意到的,它无法执行这些优化,因为整数溢出是明确定义的。

对于发布模式下的添加,Rust会发出以下LLVM指令(可以在操场上检查):

add i32 %b, %a

另一方面,叮当声将发出以下 LLVM 指令(您可以通过叮当声 -S -emit-llvm add.c 进行检查):

add nsw i32 %6, %8

区别在于nsw(无符号包装)标志。如LLVM参考中关于add的规定:

如果总和有无符号溢出,则返回的结果是模2n的数学结果,其中n是结果的位宽度。

因为LLVM整数使用二的补码表示法,所以此指令适用于有符号整数和无符号整数。

nuwnsw分别代表“无签名包装”和“无签名包装”。如果存在 nuw 和/或 nsw 关键字,则如果分别发生无符号和/或有符号溢出,则添加的结果值为毒物值。

毒值是导致未定义行为的原因。如果不存在标志,则结果定义为2的补码包装。

Q: 或者,即使Rust编译器不执行此类优化,Rust程序员也不会看到有符号整数的环绕。难道不能称之为“未定义的行为”吗?

在这个上下文中使用的“未定义行为”有一个非常具体的含义,这与这两个单词的直观英语含义不同。UB在这里特别意味着编译器可以假设永远不会发生溢出,如果发生溢出,则允许任何程序行为。这不是Rust指定的。

但是,通过算术运算符的整数溢出在 Rust 中被认为是一个错误。这是因为,正如你所说,这通常是没有预料到的。如果您有意想要包装行为,可以使用诸如 i32::wrapping_add之类的方法。

一些额外的资源:

  • RFC 560指定了Rust中关于整数溢出的所有内容。简而言之:调试模式下的恐慌,发布模式下的补码包装
  • 关于锈蚀中整数溢出的神话和传说。关于这个话题的博客帖子不错
 类似资料:
  • 最近,有符号整数溢出在C和C中没有正式定义,这引起了很多关注。然而,给定的实现可能会选择定义它;在C语言中,实现可以设置

  • 未定义行为的一个例子是在flow上的整数行为 有没有一个历史的或者(甚至更好!)造成这种差异的技术原因是什么?

  • 众所周知,有符号整数溢出是一种未定义的行为。但是在C 11文档中有一些有趣的东西: 带符号整数类型,宽度分别为8、16、32和64位,不带填充位,并使用2的补码表示负值(仅在实现直接支持该类型时提供) 参见链接 我的问题是:既然标准明确规定,、、和负数是2的补码,那么这些类型的溢出仍然是一种未定义的行为吗? 编辑我检查了C 11和C11标准,以下是我的发现: C 11,§18.4.1: 标题定义了

  • C99标准中哪里说有符号整数溢出是未定义的行为? 我看到关于无符号整数溢出的评论定义得很好(看看为什么定义了无符号整数溢出行为,但没有定义有符号整数溢出?)在第6.2.5节中: 涉及无符号操作数的计算永远不会溢出,因为不能由结果无符号整数类型表示的结果将被减少为比结果类型可以表示的最大值大一的数的模。 但是我在附录J中查看了未定义的行为,我只在列表中看到了这些类似的项目: 具有有符号提升类型的表达

  • 尽管标题出现了,但这并不是一个哲学问题。 从未初始化的数组读取 使用错误数据 使用不可移植构造。(即内存分配的细节1) 导致具有的行为 标准没有要求产生可预测的效果 我会称之为“未定义的行为”。但也许我错过了什么(?) null null

  • 这是一个例子来说明我的问题,其中涉及一些更复杂的代码,我不能在这里张贴。 这个程序在我的平台上包含未定义的行为,因为会在第三个循环上溢出。 这会使整个程序有未定义的行为,还是只有在溢出真正发生之后?编译器可能会发现会溢出,这样它就可以声明整个循环未定义,并且不用费心运行printfs,即使它们都发生在溢出之前? (标记的C和C虽然不同,但我对这两种语言的答案感兴趣,如果它们不同的话。)