发布模式下安全 Rust 中的有符号整数溢出是否被视为未定义行为?
Is signed integer overflow in safe Rust in release mode considered as undefined behavior?
Rust 在调试和发布模式下以不同方式处理有符号整数溢出。发生这种情况时,Rust 在调试模式下会崩溃,而在发布模式下会默默地执行二进制补码包装。
据我所知,C/C++ 将有符号整数溢出视为未定义行为,部分原因是:
- 在 C 标准化的时候,表示有符号整数的不同底层架构(例如 one's complement)可能仍在某处使用。编译器无法假设硬件如何处理溢出。
- 后来的编译器因此做出假设,例如两个正整数之和也必须为正,以生成优化的机器代码。
因此,如果 Rust 编译器确实对有符号整数执行与 C/C++ 编译器相同类型的优化,为什么 Rustonomicon 指出:
No matter what, Safe Rust can't cause Undefined Behavior.
或者即使 Rust 编译器不执行此类优化,Rust 程序员仍然不会期望看到有符号整数回绕。不能叫"undefined behavior"吗?
Q: So if Rust compilers do perform the same kind of optimization as C/C++ compilers regarding signed integers
Rust 没有。因为,正如您所注意到的,它无法执行这些优化,因为整数溢出定义明确。
对于发布模式下的添加,Rust 将发出以下 LLVM 指令(您可以查看 on Playground):
add i32 %b, %a
另一方面,clang 将发出以下 LLVM 指令(您可以通过 clang -S -emit-llvm add.c
检查):
add nsw i32 %6, %8
不同之处在于 nsw
(无签名换行)标志。按照指定 in the LLVM reference about add
:
If the sum has unsigned overflow, the result returned is the mathematical result modulo 2n, where n is the bit width of the result.
Because LLVM integers use a two’s complement representation, this instruction is appropriate for both signed and unsigned integers.
nuw
and nsw
stand for “No Unsigned Wrap” and “No Signed Wrap”, respectively. If the nuw
and/or nsw
keywords are present, the result value of the add is a poison value if unsigned and/or signed overflow, respectively, occurs.
有害值会导致未定义的行为。如果标志不存在,则结果明确定义为 2 的补码环绕。
Q: Or even if Rust compilers do not perform such optimization, Rust programmers still do not anticipate seeing a signed integer wrapping around. Can't it be called "undefined behavior"?
在此上下文中使用的 "Undefined behavior" 具有非常具体的含义,不同于这两个词的直观英语含义。 UB 在这里特别意味着编译器可以假设溢出永远不会发生,并且 if 会发生溢出,任何程序行为都是允许的。这不是 Rust 指定的内容。
然而,通过算术运算符 的整数溢出是 被认为是 Rust 中的一个错误。那是因为,正如您所说,通常不会预料到。如果你有意想要包装行为,有i32::wrapping_add
等方法。
一些额外的资源:
- RFC 560 指定了有关 Rust 中整数溢出的所有内容。简而言之:在调试模式下 panic,在发布模式下 2 的补码换行。
- Myths and Legends about Integer Overflow in Rust。关于此主题的精彩博客 post。
Rust 在调试和发布模式下以不同方式处理有符号整数溢出。发生这种情况时,Rust 在调试模式下会崩溃,而在发布模式下会默默地执行二进制补码包装。
据我所知,C/C++ 将有符号整数溢出视为未定义行为,部分原因是:
- 在 C 标准化的时候,表示有符号整数的不同底层架构(例如 one's complement)可能仍在某处使用。编译器无法假设硬件如何处理溢出。
- 后来的编译器因此做出假设,例如两个正整数之和也必须为正,以生成优化的机器代码。
因此,如果 Rust 编译器确实对有符号整数执行与 C/C++ 编译器相同类型的优化,为什么 Rustonomicon 指出:
No matter what, Safe Rust can't cause Undefined Behavior.
或者即使 Rust 编译器不执行此类优化,Rust 程序员仍然不会期望看到有符号整数回绕。不能叫"undefined behavior"吗?
Q: So if Rust compilers do perform the same kind of optimization as C/C++ compilers regarding signed integers
Rust 没有。因为,正如您所注意到的,它无法执行这些优化,因为整数溢出定义明确。
对于发布模式下的添加,Rust 将发出以下 LLVM 指令(您可以查看 on Playground):
add i32 %b, %a
另一方面,clang 将发出以下 LLVM 指令(您可以通过 clang -S -emit-llvm add.c
检查):
add nsw i32 %6, %8
不同之处在于 nsw
(无签名换行)标志。按照指定 in the LLVM reference about add
:
If the sum has unsigned overflow, the result returned is the mathematical result modulo 2n, where n is the bit width of the result.
Because LLVM integers use a two’s complement representation, this instruction is appropriate for both signed and unsigned integers.
nuw
andnsw
stand for “No Unsigned Wrap” and “No Signed Wrap”, respectively. If thenuw
and/ornsw
keywords are present, the result value of the add is a poison value if unsigned and/or signed overflow, respectively, occurs.
有害值会导致未定义的行为。如果标志不存在,则结果明确定义为 2 的补码环绕。
在此上下文中使用的Q: Or even if Rust compilers do not perform such optimization, Rust programmers still do not anticipate seeing a signed integer wrapping around. Can't it be called "undefined behavior"?
"Undefined behavior" 具有非常具体的含义,不同于这两个词的直观英语含义。 UB 在这里特别意味着编译器可以假设溢出永远不会发生,并且 if 会发生溢出,任何程序行为都是允许的。这不是 Rust 指定的内容。
然而,通过算术运算符 的整数溢出是 被认为是 Rust 中的一个错误。那是因为,正如您所说,通常不会预料到。如果你有意想要包装行为,有i32::wrapping_add
等方法。
一些额外的资源:
- RFC 560 指定了有关 Rust 中整数溢出的所有内容。简而言之:在调试模式下 panic,在发布模式下 2 的补码换行。
- Myths and Legends about Integer Overflow in Rust。关于此主题的精彩博客 post。