assembly 无论是技术级还是位级(CPU寄存器级),溢出转换和隐式转换之间是否有区别?

c86crjj0  于 2022-11-13  发布在  其他
关注(0)|答案(1)|浏览(136)

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

在我目前的心理模型中,溢出是一种算术现象(当我们执行算术运算时发生),而隐式转换是一种赋值(初始化与否)现象(当我们进行赋值时,右手的值不适合左手的值)。
然而,我经常看到'overflow'和'implicit conversion'这两个概念交替使用,这与我所期望的不同。例如,learncpp团队的这句话,讨论了带符号int的overflow和'bit insufficiency':
当我们试图存储一个超出类型范围的值时,就会发生整数溢出(通常简称为溢出)。本质上,我们试图存储的数字需要比对象可用的位数更多的位来表示。在这种情况下,数据会丢失,因为对象没有足够的内存来存储所有内容[1]。
这是关于无符号整型溢出的:
如果我们尝试将数字280(需要9位来表示)存储在1字节(8位)无符号整数中,会发生什么情况?答案是overflow [2]*
特别是这个使用“模 Package ”的人:
这里有另一种方法来考虑同样的事情。任何大于类型所能表示的最大数的数都可以简单地“环绕”(有时称为“模回绕”)。255在1字节整数的范围内,因此255是合适的。然而,256在该范围之外,因此它会绕回值0。257绕回值1。280绕回值24 [2]。
在这种情况下,有人说,超过左手极限的赋值会导致溢出,但我希望在这种情况下,术语“隐式转换”。
我看到溢出这个术语也被用于算术表达式,它的结果超出了左手的限制。

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

我想是的。在参考文献[3]中的“数值转换-积分转换”一节中,对于无符号整数:
[...]得到的值是最小的无符号值,等于源值模2^n,其中n是用于表示目标类型[3]的位数。
对于签名(粗体我):
如果目标类型是已签名的,如果源整数可以在目标类型中表示,则该值不会更改。否则,结果由实现定义(直到C20)目标类型的唯一值等于源值模2n,其中n是用于表示目标类型的位数。(C20起).(注意,这与未定义的有符号整数算术溢出不同)[3]。
如果我们转到引用的部分(溢出),我们会发现(粗体我的):

无符号整数算术总是以2n为模执行,其中n是该特定整数的位数。[..]
当有符号整数算术运算溢出(结果不适合结果类型)时,行为未定义[4]。
对我来说,显然溢出是一种算术现象,而隐式转换是不适合的赋值中的现象。我的解释准确吗?

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

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

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

#include <iostream>
    #include <limits>
        
    int main(void) {
      long long x = std::numeric_limits<long long>::max();   
      int y = x;           
      //
      //
      long long k = std::numeric_limits<long long>::max();      
      ++k;                
    }

输出为:

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

可在https://godbolt.org/z/6j6G69bTP处检查
c++中y的复制初始化与MASM中的对应:

int y = x;
    mov eax, DWORD PTR x$[rsp]                                        
    mov DWORD PTR y$[rsp], eax

mov语句只是忽略了'x'的64位,只捕获了它的32位。它是从运算符dword ptr转换而来的,并将结果存储在32位eax寄存器中。mov语句既不设置溢出标志,也不设置进位标志。
c++中k的增量对应于MASM中的增量:

++k;

    mov rax, QWORD PTR k$[rsp]
    inc rax
    mov QWORD PTR k$[rsp], rax

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

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

备注

    • 显然,有一个关于unsigned的术语溢出的讨论,但这不是我要讨论的

参考文献
[1][4]第一次世界大战后,中国的经济迅速发展,人民生活水平不断提高,人民生活水平不断提高https://en.cppreference.com/w/cpp/language/operator_arithmetic#Overflows

qqrboqgw

qqrboqgw1#

让我们试着把它分解一下。我们必须从更多的术语开始。

理想算术

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

执行

  • carry out* 发生在加法运算中,最高有效位出现carry out。在带标志的体系结构中,这通常会导致进位标志置位。用无符号数计算时,出现carry out表示结果不符合输出寄存器的位数,因此不能代表理想的算术结果。

进位输出也用于多字运算中,在组成结果的字之间进位1。

溢出

在二进制补码计算机上,当一个加法得进位输出不等于最后一位得进位时,整数溢出.在有标志得体系结构中,这通常会导致溢出标志置位.当用有符号数进行计算时,溢出得出现表明结果不适合输出寄存器,因此不能代表理想得算术结果.
至于“结果不匹配”,它就像是有符号算术的进位。但是,当使用有符号数的多字算术时,仍然需要使用普通的进位将一个字进位到下一个字。
一些作者称执行为“无符号溢出”,溢出为“有符号溢出”。这里的意思是,在这样的术语中,溢出是指任何操作结果不可表示的情况。其他类型的溢出包括浮点溢出,在IEEE-754机器上通过饱和到+-无穷大来处理。

转换

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

  • extension 用于将类型转换为具有更多位的类型,指的是仅在最高有效位之后添加更多位。对于无符号数,添加零(零扩展)。对于有符号数,添加符号位的副本(符号扩展)。扩展始终保留扩展后的值。
  • truncation 用于将类型转换为更少位的类型,指的是从最高有效位开始删除位,直到达到所需的宽度。如果值可以用新类型表示,则它不变。否则,它将像模约简一样被改变。
  • saturation 用于将类型转换为相同位数或更少位数的类型,其工作方式类似于截断,但如果值不可表示,则会替换为目标类型的最小值(如果小于0)或最大值(如果大于0)。
  • reinterpretation 用于在具有相同位数的类型之间进行转换,它指的是将原始类型的位模式解释为新类型。在有符号类型和无符号类型之间执行此操作时,将保留新类型中可表示的值。(例如,非负有符号32位整数的位模式在解释为无符号32位整数时表示相同的数字。)
  • 隐式转换 * 就是程序员没有明确拼写的转换。有些语言(如C)有,有些没有。

当试图从一种类型或另一种类型进行转换而结果不可表示时,一些作者也将这种情况称为“溢出”,就像“有符号溢出”和“无符号溢出”一样。然而,这是由位宽变化引起的不同现象,而不是算术结果。因此,是的,你的解释是准确的。这是两个独立的现象,通过“结果值不符合类型”的共同概念联系在一起。
要了解这两者是如何相互联系的,您也可以将两个 n 位数的加法解释为临时产生一个 n + 1位数,这样加法总是理想的。然后,将结果 * 截断 * 为 n 位,并存储在结果寄存器中。如果结果不可表示,则发生 * 进位 * 或 * 溢出 *。进位输出位则正好是临时结果的最高有效位,该临时结果然后被丢弃以到达最终结果。

问题2

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

解释不正确,标志的存在是一个红鲱鱼。有两种架构,其中数据移动设置标志(如ARMv 6-M)和算术不设置标志(如x86当使用lea指令来执行它)或甚至没有标志(如RISC-V)的架构。
还请注意,(隐含的或非隐含的)并不一定会产生指令。符号扩展和饱和通常会产生指令,但是零扩展通常是通过确保寄存器的顶部是空的来实现的,这是CPU可能能够作为你想要执行的其他操作的副作用来实现的。截断可以通过忽略寄存器的顶部来实现。当然,一般来说,重新解释本质上也不会产生任何代码。
至于carry out和overflow,它们的发生取决于你用什么值来执行算术运算。这些事情只是发生了,除非你想检测它们的发生,否则不需要代码。这只是默认的事情。

相关问题