| title | date | categories | tags | ||
|---|---|---|---|---|---|
Number Systems  | 
2023-08-13 14:30:00 +0800  | 
  | 
  | 
计算机硬件使用 0 和 1 来存储信息。当我们需要存储的信息为数字(Number)时,我们可以使用不同的编码方式(Encoding)将相同的一个数字存储为不同的 01 排列,以满足不同的需求。同样,在面对计算机硬件中已经存在的 01 排列时,我们可以使用不同的解码方式(Decoding)将相同的 01 排列理解为不同的数字。
本文将对常见的整数编码解码方式(又称表示方法,Representation)进行梳理,并重点关注它们是如何从需求中诞生的。
在所有一切讨论开始以前,我们需要区分清楚两个概念:01 排列和编码解码方式。
单单从计算机硬件角度看,硬件中的 01 排列是没有意义的,它们只是单纯的 0 和 1。同样,硬件对 01 排列进行的一系列运算也是没有意义的,硬件只是根据数字电路的设计机械地对输入的 01 排列进行处理,最后输出另一串 01 排列。
01 排列的意义是由人类赋予的,准确来说,是编码解码方式赋予的。编码解码方式规定了人类在看到一串特定的 01 排列后,该用怎么样的方式进行理解。
面对一堆 0 和 1,最自然的解码方式就是把这些 0 和 1 当作一个正常的二进制数(Binary Number)。比如看到 01 排列 0101,我们会自然地将其当成二进制数 0101。
当我们想相加两个数字 0001 和 1001 的时候,我们很自然会使用二进制加法:
  1
0001
1001
----
1010
这样的二进制加法在计算机中是使用数字电路实现的。这样进行二进制加法的数字电路叫做加法器。
- 
加法器不关心解码方式
需要注意的是,加法器并不关心两个输入的 01 排列使用的解码方式。加法器只是机械地对输入的 01 进行操作,最后得到输出的 01 结果。
当然,我们人类是关心 01 排列的含义的。当我们人类使用 Unsigned Number 这种编码解码方式来看待加法器的操作时,我们会把
000110011010排列理解为数字 0001 1001 1010。 - 
溢出(Overflow)
计算机硬件一般使用有限的空间存储 01 排列。比如一台计算机只能一次存储 4 个 0 或 1,这时如果我们将 01 排列
10001000输入加法器,加法器只能输出 01 排列0000,第 4 位(从第 0 位开始)的 1 因为存不下,所以被丢掉了。这样的现象叫做溢出(Overflow)。需要注意的是,本来的运算结果是数字 1 0000,现在只能得到数字 0000,这样的溢出很明显产生了错误的运算结果。
 
我们很快会发现,这样的编码方式只能对正数进行编码,不能对负数进行编码。
为了表示负数,我们需要用一种方式在 01 的排列中表示出正负。
正负是 2 种状态,一位 0 和 1 也是 2 种状态。所以我们可以在 01 排列中用一位表示正负信息,用剩余的 01 排列表示绝对值大小。比如规定最高位表示正负信息,0 表示正,1 表示负,剩下的几位表示绝对值。这就是 Sign/Magnitude 编码方式。
用这种编码方式,-1 将被表示为 1001,1 将被表示为 0001,非常直观。
但是这种编码方式有几个问题:
- 
正 0 和 负 0
根据这种编码方式,
0000表示正 0,1000表示负 0。但是整数里只有一个整数 0。一个整数存在两种编码,在进行计算时可能会带来不必要的麻烦。 - 
不适用加法器
加法器对 Unsigned Number 编码解码方式是适用的,我们希望对 Sign/Magnitude 编码解码方式也适用,因为这样就可以复用加法器,而不需要再重新设计一套数字电路。
假设我们现在将 -1 加上 1。它们的二进制表示分别为
1001和0001。将这两个 01 排列输入加法器,加法器输出的 01 排列是1002。使用 Sign/Magnitude 解码方式,得到数字 -2。很明显,加法器不适用 Sign/Magnitude。所以我们如果要采用 Sign/Magnitude,我们必须重新设计一套数字电路来进行加法操作。
实际上,我们可以思考一下需要的加法数字电路的特点:
如果 01 排列的最高位相同,就把它们的其余位相加,最高位不变。其余位相加,同样可能产生溢出;
如果 01 排列的最高位不同,首先比较它们的其余位谁大,然后用大数减小数,结果的最高位和大数相同。
我们也可以思考一下需要的减法数字电路的特点:
假设排列
aaaa要减去bbbb,我们可以将bbbb的最高位取反得到b'bbb。这样我们就可以将aaaa和b'bbb输入上面所说的加法数字电路。所以实际上,减法可以复用上面说的加法数字电路。但是,这样一套加法数字电路仍然非常复杂,尤其是中间还涉及到大数减去小数的减法。计算机科学中一个非常重要的思想就是「复用」。在这里,我们同样希望适用于 Unsigned Number 的加法器可以适用于 Sign/Magnitude,但经过上面的讨论,答案是不适用。
我们可以尝试一下,看看能不能找到一种编码解码方式,它既可以表示负数,又可以适用加法器。
 
- 
正数加负数等于正数
我们先观察一个正数加负数结果为正数的例子,也就是被减数大于减数。我们可以尝试做变换,使减法变成加法。比较有用的变换是下面这种:
0001 0000 - 0000 0100 = 0001 0000 + (1111 1111 - 0000 0100) - 1 0000 0000 + 1这样的变换是绝对成立的,它只是先加上 1111 1111,然后把这个加上的 1111 1111 通过减 1 0000 0000 加 1 的方式减去。
我们可以看到
0001 0000 + (1111 1111 - 0000 0100)绝对要进位,因为被减数大于减数。而这个进的位正好可以用 1 0000 0000 减掉。最后面还有个 +1,我们就可以说把这个进位放到最低位上。总结起来,如果一个正数加负数结果为正数,那么我们可以给负数做
1111 1111 - 负数绝对值这样的变换。这个变换同样可以通过对负数绝对值每一位取反得到。变换好了以后,正数和负数的变换相加,然后在最低位加 1 就能得到结果。可以看到,每一步都没有用到减法。 - 
正数加负数等于负数
现在我们试试正数加负数结果为负数的例子,也就是被减数小于减数。我们同样给负数(减数)进行上面的变换,看看能得到什么。
0000 0100 - 0000 1000 = 0000 0100 + (1111 1111 - 0000 1000) - 1 0000 0000 + 1 = 结果 0000 0100 + (1111 1111 - 0000 1000) = 1111 1111 + 结果我们知道,结果肯定是负数。而
1111 1111 + 结果就是上面提到的变换。所以,在正数加负数结果为负数的例子中,我们也可以对负数(减数)进行变换。正数和负数(减数)的变换相加以后就是结果的变换。
 
- 
具体编码方式
我们可以看到上面提到的这个
1111 1111 - 负数绝对值变换对负数非常有用。根据上面提到的两个例子,我们可以定义这样的编码方式:正数使用 Unsigned Number 方式进行编码,负数使用
1111 1111 - 负数绝对值的方式进行编码。 - 
Ones' Complement 撇号位置
因为对负数编码时需要多个 1 作被减数,所以这种编码方式叫做 Ones' Complement,而不是 One's Complement。
 - 
实际不用减法,用取反
另外我们可以发现,
1111 1111 - aaaa aaaa和aaaa aaaa每一位取反结果是一样的。为了不使用另外的减法电路,该编码方式通常通过每一位取反完成。 - 
验证 Ones' Complement 是否适用于加法器
我们重新验证一下上面两个例子。
在例子 1 中,0001 0000 要加 - 0000 0100,它们的 Ones' Complement 编码为
0001 00001111 1011。这两个 01 排列输入加法器,最高位肯定会进位,加法器会输出 01 排列1 0000 1011。加法器的输出还需要去掉1 0000 0000,再加上 1,得到 01 排列0000 1100,根据 Ones' Complement 方式进行解码,得到结果 0000 1100,正确。例子 2 中,0000 0100 要加 - 0000 1000,它们的 Ones' Complement 编码为
0000 01001111 0111。这两个 01 排列输入加法器,最高位肯定不会进位,加法器会输出 01 排列1111 1011。根据 Ones' Complement 方式进行解码,得到结果 - 0000 0100,正确。 - 
对加法器的输出处理
总结一下,两个 Ones' Complement 编码的 01 排列输入加法器,如果加法器的最高位有进位,那么要把这个进位去掉,然后加上 1,得到的 01 排列用 Ones' Complement 方式解码;如果加法器的最高位没有进位,那么输出的 01 排列可以直接用 Ones' Complement 方式解码。
 
我们限定 01 排列是 8 位的,那么 -0 的编码就是 1111 1111,-1 的编码就是 1111 1110,-127 是 1000 0000,-128 是 0111 1111;+127 是 0111 1111,+128 是 1000 0000。可以看到,产生了重复。为了根据最高位判断符号,所以 8 位 01 排列用这种解码方式取值范围是 -127~+127。
另外主要注意,0 有两种表示方法。
上面的两个例子中,我们已经讨论了正加负等于正,以及正加负等于负这两种情况,整数相加还有下面三种情况:
- 
正数加正数等于正数
这种情况没什么好讨论的,需要注意的是可能会产生溢出。
 - 
负数加负数等于负数
- 0000 1000 - 0000 0100 = 结果 (1111 1111 - 0000 1000) + (1111 1111 - 0000 0100) = 1111 1111 + 结果 + 1 0000 0000 - 1 (1111 1111 - 0000 1000) + (1111 1111 - 0000 0100) - 1 0000 0000 + 1 = 1111 1111 + 结果上面例子中,- 0000 1000 要加 - 0000 0100,它们的 Ones' Complement 编码为
1111 01111111 1011。这两个 01 排列输入加法器,最高位肯定会进位。因为1111 1111 - 负数1绝对值 + 1111 1111 - 负数2绝对值进位的条件是,两个负数绝对值加起来小于等于 1111 1110,即 254,而任何一个负数的绝对值小于等于 127,满足条件。加法器会输出 01 排列
1 1111 0010。加法器的输出还需要去掉1 0000 0000,再加上 1,得到 01 排列1111 0011,根据 Ones' Complement 方式进行解码,得到结果 - 0000 1100,正确。同样,这种情况可能也会产生溢出。
 - 
正数加负数等于 0
0000 1000 - 0000 1000 = 结果 0000 1000 + (1111 1111 - 0000 1000) - 1 0000 0000 + 1 = 结果 0000 1000 + (1111 1111 - 0000 1000) = 1111 1111 + 结果0000 1000 编码为
0000 1000,- 0000 1000 编码为1111 0111。输入加法器,最高位不进位。加法器输出1111 1111。进行解码,得 -0,正确。 
如果使用 Ones' Complement 编码解码方式,进行加法和减法都只需要加法器和取反器,相比 Sign/Magnitude 简单了太多。另外,Ones' Complement 也可以通过最高位判断正负。
但是,Ones' Complement 还是存在正 0 和负 0 的问题。
我们可以尝试一下,看看能不能找到一种编码解码方式,能完美解决 Sign/Magnitude 的两个缺点。
Ones' Complement 编码方式中 0 有两种编码。我们尝试对编码方式进行更改,让 0 只有一种编码。
我们可以对负数绝对值取反后再加一个 1,这样 -0 的 Ones' Complement 编码 1111 1111 就变成 0000 0000 了。+0 的编码还是 0000 0000。整数 0 就只有一种编码了。
接下来,我们先用这种变换方式尝试一下常见的整数运算情况。
- 
正数加负数等于正数
我们观察一个正数加负数结果为正数的例子,也就是被减数大于减数。运算时尽量使用上面提到的变换。
0001 0000 - 0000 0100 = 0001 0000 + (1111 1111 - 0000 0100 + 1) - 1 0000 0000 + 1 - 1我们可以看到
0001 0000 + (1111 1111 - 0000 0100 + 1)绝对要进位,因为1111 1111加 1 肯定会进位,并且被减数大于减数。而这个进的位正好可以用1 0000 0000减掉。最后面还有个+1和-1相互抵消。我们可以说忽略这个进位。总结起来,如果正数加负数结果为正数,那么我们可以给负数做
1111 1111 - 负数绝对值 + 1这样的变换。变换好了以后,正数和负数的变换相加,进位忽略就可以得到结果。可以看到,每一步都没有用到减法。 - 
正数加负数等于负数
现在我们试试正数加负数结果为负数的例子,也就是被减数小于减数。我们同样给负数(减数)进行上面的变换,看看能得到什么。
0000 0100 - 0000 1000 = 0000 0100 + (1111 1111 - 0000 1000 + 1) - 1 0000 0000 + 1 - 1 = 结果 0000 0100 + (1111 1111 - 0000 1000 + 1) = 1111 1111 + 结果 + 1我们可以看到
0000 0100 + (1111 1111 - 0000 1000 + 1)绝对不会进位,因为被减数减减数结果肯定小于等于 -1。我们知道,结果肯定是负数。而
1111 1111 + 结果 + 1就是上面提到的变换。总结起来,在正数加负数结果为负数的例子中,我们也可以对负数(减数)进行变换。正数和负数(减数)的变换相加以后就是结果的变换。
 
- 
具体编码方式
我们可以定义这样的编码方式:
正数使用 Unsigned Number 方式进行编码,负数使用
1111 1111 - 负数绝对值 + 1的方式进行编码。 - 
Two's Complement 撇号位置
因为对负数编码时,实际上是
2^n - 负数绝对值,只有一个 2,所以这种编码方式叫做 Two's Complement,而不是 Twos' Complement。 - 
从负数的编码得到负数绝对值
根据 Two's Complement 的定义,我们可以对一个负数的编码减 1,再取反,来得到负数的绝对值。
但实际上,根据上面的编码方式,我们还能得到
1111 1111 - 负数编码 + 1 = 负数绝对值。也就是说,对负数编码同样可以先取反,后加 1,得到负数绝对值。负数编码和负数绝对值,可以通过先取反,后加 1 的方式相互转换。
 - 
另一种转换方式
从 lsb 开始检查,发现第一个 1,把这个 1 前面直到 msb 的位全部取反。
 - 
验证 Two's Complement 是否适用于加法器
我们重新验证一下上面两个例子。
在例子 1 中,0001 0000 要加 - 0000 0100,它们的 Two's Complement 编码为
0001 00001111 1100。这两个 01 排列输入加法器,最高位肯定会进位,加法器会输出 01 排列1 0000 1100。加法器的输出还需要去掉1 0000 0000,得到 01 排列0000 1100,根据 Two's Complement 方式进行解码,得到结果 0000 1100,正确。例子 2 中,0000 0100 要加 - 0000 1000,它们的 Two's Complement 编码为
0000 01001111 1000。这两个 01 排列输入加法器,最高位肯定不会进位,加法器会输出 01 排列1111 1100。根据 Two's Complement 方式进行解码,得到结果 - 0000 0100,正确。 - 
对加法器的输出处理
总结一下,两个 Two's Complement 编码的 01 排列输入加法器,如果加法器的最高位有进位,那么无视该进位,得到的 01 排列用 Two's Complement 方式解码;如果加法器的最高位没有进位,那么输出的 01 排列可以直接用 Two's Complement 方式解码。
需要注意的是,Unsigned Number 编码输入加法器,如果加法器的最高位有进位,这个进位是不能被保存到结果里的,那么得到的结果是错误的,这叫溢出。
Two's Complement 编码输入加法器,如果加法器的最高位有进位,这个进位同样不能被保存到结果里,但是经过上面的讨论,我们知道得到的结果是正确的。
 
-1 是 1111 1111,-127 是 1000 0001,-128 是 1000 0000,-129 是 0111 1111。
+1 是 0000 0001,+127 是 0111 1111,+128 是 1000 0000。
为了保证负数最高位是 1,取值范围为 -128~+127。
另外需要注意,整数 0 的编码只有 0000 0000。
上面的两个例子中,我们已经讨论了正加负等于正,以及正加负等于负这两种情况,整数相加还有下面三种情况:
- 
正数加正数等于正数
这种情况没什么好讨论的,需要注意的是可能会产生溢出。
 - 
负数加负数等于负数
- 0000 0100 - 0000 0100 = 结果 (1111 1111 - 0000 0100 + 1) + (1111 1111 - 0000 0100 + 1) = 1111 1111 + 1111 1111 + 1 + 1 + 结果 (1111 1111 - 0000 0100 + 1) + (1111 1111 - 0000 0100 + 1) = 1111 1111 + 1 + 结果 + 1 0000 0000 (1111 1111 - 0000 0100 + 1) + (1111 1111 - 0000 0100 + 1) - 1 0000 0000 = 1111 + 结果 + 1上面例子中,- 0000 0100 要加 - 0000 0100,它们的 Two's Complement 编码为
1111 11001111 1100。这两个 01 排列输入加法器,最高位肯定会进位。因为1111 1111 - 负数1绝对值 + 1 + 1111 1111 - 负数2绝对值 + 1的取值范围是[1 0000 0000, 1 1111 1110]。加法器会输出 01 排列
1 1111 1000。加法器的输出还需要去掉1 0000 0000,得到 01 排列1111 1000,根据 Two's Complement 方式进行解码,得到结果 - 0000 1000,正确。同样,这种情况可能也会产生溢出。
 - 
正数加负数等于 0
0001 0000 - 0001 0000 = 0001 0000 + (1111 1111 - 0001 0000 + 1) - 1 0000 0000 + 1 - 10001 0000编码为0001 0000,- 0001 0000 编码为1111 0000。输入加法器,最高位一定进位。加法器输出1 0000 0000,去掉1 0000 0000,得到排列0000 0000。根据 Two's Complement 方式进行解码,得到结果 0000 0000,正确。 
上面已经说明,0 的 Two's Complement 编码是 0000 0000。用这个编码输入加法器,0 加 0,0 加负数,0 加正数都不需要进行讨论。
但是 0 的编码实际上是 1111 1111 - 0000 0000 + 1 得来的,我们下面讨论一下这种编码方式的 0 参与运算的情况:
- 
-0 加正数
- 0000 0000 + 0000 1000 = (1111 1111 - 0000 0000 + 1) + 0000 1000 - 1 0000 0000-0 和正数的 Two's Complement 输入加法器,最高位必定进位,不用管。
 - 
-0 加负数
- 0000 0000 - 0000 1000 = 结果 (1111 1111 - 0000 0000 + 1) + (1111 1111 - 0000 1000 + 1) - 1 0000 0000 = 1111 1111 + 结果 + 1前面两个括号相加取值范围是
[1 1000 0000, 1 1111 1111],最高位必定进位。-0 和负数的 Two's Complement 输入加法器,最高位必定进位,不用管。加法器输出就是结果的 Two's Complement 编码。
 - 
-0 加 -0
- 0000 0000 - 0000 0000 = - 0000 0000 (1111 1111 - 0000 0000 + 1) + (1111 1111 - 0000 0000 + 1) - 1 0000 0000 = 1111 1111 - 0000 0000 + 1这种情况我解释不来。
 
当加法器的输出对应的数字解码后不符合计算结果时,就是溢出。
- 
不会出现溢出的情况
一个正数加一个负数绝对不可能溢出。比如整数用 8 位存储,整数的取值范围是 -128~127。一个正数加一个负数的取值范围是
[1-128, 127-1],结果可以被表示。 - 
可能出现溢出的情况
正数加正数,负数加负数,都可能会溢出。
正数加正数,它们的 01 排列都是
0xxx xxxx,加法器如果输出1xxx xxxx,按照 Two's Complement 解码,得到的是负数,这很明显是错的,即发生了溢出。两个正数相加,最高位不可能进位,因为两个正数和的最大值就是
1111 1110。负数加负数,它们的 01 排列都是
1xxx xxxx,加法器如果输出0xxx xxxx,按照 Two's Complement 解码,得到的是正数,这很明显是错的,即发生了溢出。需要注意的是,两个负数的 01 排列相加,最高位肯定会进位,这个上面已经讨论过了,直接忽略就可以。
总结起来,如果两个数字符号相同,结果数字的符号却不一样,那么就发生了溢出。
 
通过上面的讨论,我们可以看到 Two's Complement 方式完美解决了 Sign/Magnitude 的两个缺点。而 Two's Complement 也是所有现代计算机编码有符号数的方式。
我们可以思考一下,运算操作最初的呈现形式是怎么样的。
最开始,一个程序员在源代码中,通过各种途径(初始化、计算、外界输入等)把两个值放在两个变量里,并对这两个变量进行加法或者减法,结果放在另一个变量里。即 c = a +/- b;。很明显,a 和 b 是有类型的,它们的类型规定了它们的大小和是否带符号。而通过类型大小和是否带符号,我们可以得到 a 和 b 的取值范围。
接下来,源代码会转化为汇编代码。c = a + b 会变成 add rc, ra, rb,c = a - b 会变成 sub rc, ra, rb。值得注意的是,如果变量 a 或 b 的类型是 signed,那么寄存器 ra 或 rb 的二进制是变量 a b 值的 two's complement 编码。如果变量 a 或 b 的类型是 unsigned,那么寄存器 ra 或 rb 的二进制是变量 a b 值的 unsigned 编码。
所以,如果变量 a b 是 signed,它们对应的加减汇编指令一共有这些情况:add 正 正;add 正 负;add 负 正;add 负 负;sub 正 正;sub 正 负;sub 负 正;sub 负 负。
如果变量 a b 是 unsigned,它们对应的加减汇编指令一共有这些情况:add rc, ra, rb;sub rc, ra, rb。
先看 signed,我们已经讨论过加法的三种情况,现在讨论减法的四种情况。
- 
正数减正数(正数加负数)
比如,现在有 c = 2 - 1,它的汇编指令为 sub rc, 0x0010, 0x0001。我们最后喂给加法器的 01 排列肯定一个要表示 2(即 0x0010),一个要表示 -1(即 0x1111)。现在问题来了,如何通过 1 的编码 0x0001,获得 -1 的编码 0x1111?即如何通过负数绝对值的编码获得负数的编码?
我们在上面讨论过,负数的编码和它绝对值的编码是可以通过取反再加一相互转换的。所以,ALU 在获取 0x0001 以后,也是先进行取反,然后加 1,最后和 0x0010 一起送入加法器。正数减正数的操作就被转化为正数加负数了。
- 
转化为正数加负数,不溢出
至于正数加负数的情况已经讨论过了,不再赘述。正数加负数也不会溢出。
注意,由负数绝对值编码获得负数编码不会出现问题。比如 4 个 bit,如果用 two's complement 理解,获得的值的范围是 -8~7。正数 1 到 7,都有对应的负数。
 
 - 
 - 
正数减负数(正数加正数)
比如,现在有 c = 2 - (-1),它的汇编指令为 sub rc, 0x0010, 0x1111。我们最后喂给加法器的 01 排列肯定一个要表示 2(即 0x0010),一个要表示 1(即 0x0001)。现在问题来了,如何通过 -1 的编码 0x1111,获得 1 的编码 0x0001?即如何通过负数的编码获得它绝对值的编码?
我们在上面讨论过,负数的编码和它绝对值的编码是可以通过取反再加一相互转换的。所以,ALU 在获取 0x1111 以后,也是先进行取反,然后加 1,最后和 0x0010 一起送入加法器。正数减负数的操作就被转化为正数加正数了。
- 
转化为正数加正数,可能溢出
至于正数加正数的情况,已经讨论过了,不再赘述。正数加正数可能会溢出。
 - 
转化为正数加负数,肯定溢出
注意,一般情况下,正数减负数是可以转化为正数加正数的。但是,当负数是最小负数的时候,会出问题。比如 4 个 bit,如果用 two's complement 理解,获得的值的范围是 -8~7。-8 没有对应的正数。对 -8 的编码 1000 取反加 1 以后得到的还是 1000。
如果一个正数比如 2 要减 -8,那么最后喂到加法器里的是 0x0010 (2) 0x1000 (-8),加法器得到 0x1010 (-6),而正确结果是 10。实际上进行的操作是正数加负数。这样肯定是溢出的。
 
 - 
 - 
负数减负数(负数加正数)
比如,现在有 c = (-2) - (-1),它的汇编指令为 sub rc, 0x1110, 0x1111。我们最后喂给加法器的 01 排列肯定一个要表示 -2(即 0x1110),一个要表示 1(即 0x0001)。
- 
转化为负数加正数,不溢出
 - 
转化为负数加负数,不溢出
注意,一般情况下,负数减负数是可以转化为负数加正数的,负数加正数不会溢出。但是,当减数是最小负数的时候,需要特殊考虑。比如 4 个 bit,如果用 two's complement 理解,获得的值的范围是 -8~7。-8 没有对应的正数。对 -8 的编码 1000 取反加 1 以后得到的还是 1000。
比如一个负数 -2 要减 -8,那么最后喂到加法器里的是 0x1110 (-2) 0x1000 (-8),加法器得到 0x0110 (6),结果居然是对的,这是为什么?我们知道,如果我们进行的操作是 -2 加 -8,而最后结果是 6,那肯定发生溢出了。其实溢出就是进位没地方存了,如果进位有地方存的话,结果应该是 0x10110 (-10)。因为进位被丢掉了,所以结果被加上了 16,变成 0x0110。而 -2 + -8 加上 16,就是 -2 - (-8)。
 
 - 
 - 
负数减正数(负数加负数)
比如,现在有 c = (-2) - 1,它的汇编指令为 sub rc, 0x1110, 0x0001。我们最后喂给加法器的 01 排列肯定一个要表示 -2(即 0x1110),一个要表示 -1(即 0x1111)。
- 
转化为负数加负数,可能溢出
注意,由负数绝对值编码获得负数编码不会出现问题。比如 4 个 bit,如果用 two's complement 理解,获得的值的范围是 -8~7。正数 1 到 7,都有对应的负数。
所以负数减正数都可以转化为负数加负数,另外注意负数加负数可能会溢出。
 
 - 
 
然后再来看 unsigned。两个 unsigned 变量的加法很直观,直接喂给加法器就可以。如果加法器有 carry out,那么就有溢出。
重点是减法。下面对 unsigned number msb 是否是 1 分四种情况讨论。
- 
msb 非 1 - msb 非 1(正数减正数,结果正确)
这时,可以直接用 two's complement 解码 unsigned number 的 01 排列。
比如 0x0010 - 0x0001,最后喂给加法器的是 0x0010,-0x0001(即 0x1111)。
再比如 0x0001 -0x0010,最后喂给加法器的是 0x0001,0x1110,加法器输出 0x1111。
这种情况相当于正数减正数,结果正确。
 - 
msb 为 1 - msb 为 1(负数减负数,结果正确)
比如 10 - 9。它们的 01 排列为 1010 - 1001。
如果用 two's complement 理解,则 1010 表示 -0110,即 -6;1001 表示 -0111,即 -7。即 -6 - (-7)。最后输入加法器的 01 排列为 1010、0111,加法器输出 0001,即 1。
实际上,我们可以把 01 排列和对应的值列出来:
01 排列 two's complement unsigned 1111 -1 15 1110 -2 14 1101 -3 13 1100 -4 12 1011 -5 11 1010 -6 10 1001 -7 9 1000 -8 8 因为我们只是想要求差值,所以可以把 01 排列用 two's complement 理解。10 - 9 的差值和 -6 - (-7) 是一样的。
这种情况相当于负数减负数,结果正确。
 - 
msb 为 1 - msb 非 1(负数减正数,结果正确)
- 
有溢出,结果按 unsigned two's complement 解读都正确
比如 10 - 6。它们的 01 排列为 1010 - 0110。
如果用 two's complement 理解,则 1010 表示 -0110,即 -6;0110 表示 6。即 -6 - 6。最后输入加法器的 01 排列为 1010、1010,加法器输出 0100,即 4。可以看到,-6 - 6 应该等于 -12,但是加法器的结果是 4,很明显发生了溢出。但是 10 - 6 确实等于 4。这是为什么呢?
其实溢出就是存储数字的空间不够了,如果多一位来存储数字,那么加法器应该输出 10100,即 -12。但是因为少了一位,所以结果加上了 16,而 10 和 -6、11 和 -5 的差值也是 16。所以结果正确。
 - 
无溢出,结果按 unsigned 解读正确
上面的例子出现了溢出,我们来看一个没有溢出的例子。比如 14 - 3。它们的 01 排列为 1110 - 0011。如果用 two's complement 理解,即 -2 - 3。最后输入加法器的 01 排列为 1110 1101,加法器输出 1011,即 -5。如果用 unsigned 理解,即 14 - 3,最后结果 1011 就是 11。
因为这次没有溢出,所以用 two's complement 理解,运算是正确的。即加法器可以得到 -2 - 3 = -5。我们在两边都加上 16,即 14 - 3 = 11。
 
 - 
 - 
msb 非 1 - msb 为 1(正数减负数)
- 
被减数为 1000(结果按 two's complement 解读正确)
首先看被减数为 1000。比如 0110 - 1000。用 unsigned 理解是 6 - 8,用 two's complement 理解是 6 - (-8),喂给加法器的 01 排列是 0110 1000,加法器输出 1110。
实际上,加法器进行的操作就是 6 + (-8),输出的结果也是 6 + (-8) 的。被减数为 1000 时,运算结果正确。
 - 
被减数为其他,产生溢出(结果按 two's complement 解读正确)
当被减数是其他 msb 为 1 的 01 排列时。比如 0110 - 1011。用 unsigned 理解是 6 - 11,用 two's complement 理解是 6 - (-5)。喂给加法器的 01 排列是 0110 0101,加法器输出 1011。
用 two's complement 理解 6 - (-5) = -5,这肯定是不对的;但是用 unsigned 理解,6 - 11 = -5。因为计算产生了溢出,本来的结果 01011 的 0 被丢掉了,结果被减掉了 16。而 11 和 -5 也是差 16。
 - 
被减数为其他,不产生溢出(结果怎么解读都不正确)
当被减数是其他 msb 为 1 的 01 排列时。比如 0010 - 1101。用 unsigned 理解是 2 - 13,用 two's complement 理解是 2 - (-3)。喂给加法器的 01 排列是 0010 0011,加法器输出 0101。
用 two's complement 理解,2 - (-3) = 5。用 unsigned 理解,2 - 13 = 5,结果明显不对。
 
 -