溢出和隐式转换之间有什么区别,无论是技术级别还是位级别(cpu-寄存器级别)?

Man*_*uel 6 c++ assembly integer-overflow language-lawyer implicit-conversion

(我是新手,所以说的可能有不准确的地方)

\n

在我当前的思维模型中,溢出是一种算术现象(当我们执行算术运算时发生),而隐式转换是一种赋值(初始化与否)现象(当我们对右手的值不进行赋值时发生)适合左侧值。

\n

然而,我经常看到“溢出”和“隐式转换”这两个概念可以互换使用,与我的预期不同。例如,来自 learncpp 团队的这段引用,谈论了有符号 int 的溢出和“位不足”:

\n
\n

当我们尝试存储超出类型范围的值时,就会发生整数溢出(通常简称为溢出)。本质上,我们试图存储的数字需要比对象可用的位数更多的位数来表示。在这种情况下,数据会丢失,因为对象没有足够的内存来存储所有内容[1]。

\n
\n

这是关于 unsigned int 的溢出:

\n
\n

如果我们尝试将数字 280(需要 9 位来表示)存储在 1 字节(8 位)无符号整数中,会发生什么情况?答案是溢出 [2]*

\n
\n

尤其是这个,他使用“模换行”:

\n
\n

这里\xe2\x80\x99是思考同一件事的另一种方式。任何大于该类型可表示的最大数字的数字都简单地 \xe2\x80\x9c 包裹 \xe2\x80\x9d (有时称为 \xe2\x80\x9cmodulo 包裹 \xe2\x80\x9d)。255 在 1 字节整数范围内,因此 255 就可以了。然而,256 超出了范围,因此它回绕到值 0。257 回绕到值 1。280 回绕到值 24 [2]。

\n
\n

在这种情况下,据说超出左手限制的赋值会导致溢出,但我希望在这种情况下使用术语“隐式转换”。

\n

我看到术语“溢出”也用于其结果超出左侧限制的算术表达式。

\n
\n

1 隐式转换和上溢/下溢之间有技术上的区别吗?

\n

我想是这样。在参考文献 [3] 的“数值转换 - 积分转换”部分中,对于无符号整数:

\n
\n

[...] 结果值是等于源值模 2^n\n 的最小无符号值,其中 n 是用于表示目标类型的位数 [3]。

\n
\n

对于签名(粗体我的):

\n
\n

如果目标类型有符号,并且源整数可以在目标类型中表示,则该值不会更改。否则,结果是实现定义的(直到 C++20)目标类型的唯一值等于源值模 2n\n,其中 n 是用于表示目标类型的位数。(自 C++20 起)。**\n(请注意,这与有符号整数算术溢出不同,后者是未定义的)**[3]。

\n
\n

如果我们转到引用的部分(溢出),我们发现(粗体是我的):

\n
\n

无符号整数算术始终以 2n\n 为模进行计算,其中 n 是该特定整数的位数。[..]

\n

当有符号整数算术运算溢出(结果不适合结果类型)时,行为是未定义的 [4]。

\n
\n

对我来说,显然溢出是一种算术现象,而隐式转换是不适合的赋值中的一种现象。我的解释准确吗?

\n
\n

2 在位级别(CPU)上隐式转换和溢出之间有什么区别吗?

\n

我也这么认为。我远不擅长 C++,更不擅长汇编,但作为一个实验,如果我们使用 MSVC(标志 /std:c++20)和 MASM(宏汇编)检查下面代码的输出特别是检查标志寄存器,如果是算术运算或者是赋值(“隐式转换”),会出现不同的现象。

\n

(我检查了 Visual Studio 2022 调试器中的标志寄存器。下面的程序集实际上与调试时的程序集相同)。

\n
    #include <iostream>\n    #include <limits>\n        \n    int main(void) {\n      long long x = std::numeric_limits<long long>::max();   \n      int y = x;           \n      //\n      //\n      long long k = std::numeric_limits<long long>::max();      \n      ++k;                \n    }\n
Run Code Online (Sandbox Code Playgroud)\n

输出是:

\n
    y$ = 32\n    k$ = 40\n    x$ = 48\n    main PROC\n    $LN3:\n      sub rsp, 72 ; 00000048H\n      call static __int64 std::numeric_limits<__int64>::max(void) ; \n      std::numeric_limits<__int64>::max\n      mov QWORD PTR x$[rsp], rax\n      mov eax, DWORD PTR x$[rsp]\n      mov DWORD PTR y$[rsp], eax\n      call static __int64 std::numeric_limits<__int64>::max(void) \n      ; std::numeric_limits<__int64>::max\n      mov QWORD PTR k$[rsp], rax\n      mov rax, QWORD PTR k$[rsp]\n      inc rax\n      mov QWORD PTR k$[rsp], rax\n      xor eax, eax\n      add rsp, 72 ; 00000048H\n      ret 0\n    main ENDP\n
Run Code Online (Sandbox Code Playgroud)\n

可以在https://godbolt.org/z/6j6G69bTP检查

\n

c++ 中 y 的复制初始化对应于 MASM 中的复制初始化:

\n
    int y = x;\n    mov eax, DWORD PTR x$[rsp]                                        \n    mov DWORD PTR y$[rsp], eax\n\n
Run Code Online (Sandbox Code Playgroud)\n

mov 语句只是忽略“x”的 64 位并仅捕获其 32 位。它是从操作符dword ptr转换而来的进行转换并将结果存储在 32 位 eax 寄存器中。\nmov 语句既不设置溢出标志也不设置进位标志。

\n

c++中k的增量对应于MASM中的增量:

\n
    ++k;\n\n    mov rax, QWORD PTR k$[rsp]\n    inc rax\n    mov QWORD PTR k$[rsp], rax\n\n
Run Code Online (Sandbox Code Playgroud)\n

当执行inc语句时,溢出标志(有符号溢出)被设置为1。

\n

对我来说,虽然您可以通过不同的方式实现 (mov) 转换,但使用 mov 变体和算术溢出的转换之间存在明显的区别:算术设置标志。我的解释准确吗?

\n
\n

笔记

\n
    \n
  • *显然有一个关于术语“无符号溢出”的讨论,但这不是我正在讨论的内容
  • \n
\n
\n

参考

\n

[1] https://www.learncpp.com/cpp-tutorial/signed-integers/ \n[2] https://www.learncpp.com/cpp-tutorial/unsigned-integers-and-why-to-避免它们/ \n[3] https://en.cppreference.com/w/cpp/language/implicit_conversion \n[4] https://en.cppreference.com/w/cpp/language/operator_arithmetic#Overflows

\n

fuz*_*fuz 4

让我们尝试分解它。我们必须从更多的术语开始。

\n

理想算术

\n

理想算术是指数学中所涉及的数字是真正的整数且其大小没有限制的算术。在计算机上实现算术时,整数类型的大小通常受到限制,只能表示有限范围的数字。它们之间的算术不再理想,因为某些算术运算可能会产生无法用您使用的类型表示的值。

\n

执行

\n

另外,当最高有效位有进位时,就会发生进位在具有标志的架构中,这通常会导致设置进位标志。当使用无符号数进行计算时,进位的存在表明结果不适合输出寄存器的位数,因此不代表理想的算术结果。

\n

进位还用于多字算术中,在组成结果的字之间进位 1。

\n

溢出

\n

在二进制补码机器上,整数溢出当加法的进位不等于最后一位的进位时,整数在具有标志的体系结构中,这通常会导致设置溢出标志。当使用有符号数进行计算时,溢出的存在表明结果无法放入输出寄存器,因此不能代表理想的算术结果。

\n

对于 \xe2\x80\x9c 结果不适合,\xe2\x80\x9d 它就像有符号算术的进位。然而,当使用有符号数的多字算术时,您仍然需要使用正常的进位将一个进位到下一个字。

\n

一些作者将执行 \xe2\x80\x9cunsigned 溢出\xe2\x80\x9d 和溢出 \xe2\x80\x9csigned 称为溢出。\xe2\x80\x9d 这里的想法是,在这样的命名法中,溢出指的是操作的结果是不可表示的。其他类型的溢出包括浮点溢出,在 IEEE-754 机器上通过饱和到 +-Infinity 进行处理。

\n

转换

\n

转换是指采用一种数据类型表示的值并用另一种数据类型表示。当涉及的数据类型是整数类型时,通常通过扩展、 截断饱和重新解释来完成

\n
    \n
  • 扩展用于将类型转换为具有更多位的类型,指的是在最高有效位之后添加更多位。对于无符号数,添加零(零扩展)。对于有符号数,添加符号位的副本(符号扩展)。扩展始终保留扩展的值。
  • \n
  • 截断用于将类型转换为位数较少的类型,是指从最高有效位中删除位,直到达到所需的宽度。如果该值可以用新类型表示,则它不会改变。否则它将像模数减少一样改变。
  • \n
  • 饱和度用于将类型转换为相同数量或更少位数的类型,其工作方式类似于截断,但如果该值不可表示,则将其替换为最小(如果小于 0)或最大(如果大于 0)值目的地类型。
  • \n
  • 重新解释用于在相同位数的类型之间进行转换,指将原始类型的位模式解释为新类型。在有符号和无符号类型之间执行此操作时,会保留新类型中可表示的值。(例如,非负有符号 32 位整数的位模式在解释为无符号 32 位整数时表示相同的数字。)
  • \n
\n

隐式转换只是在程序员没有明确说明的情况下发生的转换。有些语言(如 C)有这些,有些则没有。

\n

当尝试从一种类型或另一种类型进行转换并且结果无法表示时,一些作者也将这种情况称为 \xe2\x80\x9coverflow,\xe2\x80\x9d ,就像 \xe2\x80\x9csigned Overflow\ xe2\x80\x9d 和 \xe2\x80\x9cunsigned 溢出。\xe2\x80\x9d 然而,这是由位宽变化引起的不同现象,而不是算术结果。所以是的,你的解释是准确的。这是两个独立的现象,通过 \xe2\x80\x9c 结果值不适合类型的共同想法相关。\xe2\x80\x9d

\n

要了解两者是如何相互关联的,您还可以将两个n位数字的加法解释为产生一个临时的n + 1 位数字,这样加法始终是理想的。然后,结果被截断n位并存储在结果寄存器中。如果结果不可表示,则根据所需的符号,发生进位溢出。进位位恰好是临时结果的最高有效位,然后将其丢弃以获得最终结果。

\n

问题2

\n
\n

对我来说,虽然您可以通过不同的方式实现 (mov) 转换,但使用 mov 变体和算术溢出的转换之间存在明显的区别:算术设置标志。我的解释准确吗?

\n
\n

这种解释是不正确的,并且标志的存在是一种转移注意力的行为。既有数据移动设置标志的体系结构(例如 ARMv6-M),也有算术不设置标志的体系结构(例如,使用指令lea执行指令时的 x86)或什至没有标志(例如 RISC-V)。

\n

另请注意,转换(隐式或非隐式)不一定会产生指令。符号扩展和饱和通常可以实现,但零扩展通常是通过确保寄存器的顶部是清晰的来实现的,CPU 可以作为您想要执行的其他操作的副作用来执行此操作。可以通过仅忽略寄存器的顶部部分来实现截断。一般来说,重新解释就其本质而言当然不会生成任何代码。

\n

至于进位和溢出,它们的出现取决于您执行算术运算的值。这些都是刚刚发生的事情,除非您想检测它们的发生,否则不需要任何代码。这只是默认的事情。

\n