-1

假设有一段代码:

mov al, 12
mov bl, 4
sub al, bl

在这种情况下,CF = 0 标志,但在我看来它应该等于 1,因为减法运算是在加法运算上实现的,并且处理器不知道我们将其作为输入提供什么,无论是有符号还是无符号数字,它只是做它的工作。

也就是说,上面的代码等价于以下内容:

在al寄存器中输入值12,即0000 1100

在bl寄存器中输入值4,即0000 0100

接下来是减法运算,因为第一个操作数是正数,所以没有转换为附加代码。由于第二个也是正数,所以也没有变换,但是由于进行了减法运算,所以将第二个操作数翻译成附加代码,处理器进行加法运算(减法是通过加法来实现的),即:

12: 0000 1100
-4: 1111 1100



12 - 4 = 12 + (-4) = 0000 1100 + 1111 1100 = 1 0000 1000

也就是说,我们得到了正确的结果 - 8,但如果调试它,CF = 0。这是为什么?超出位网格的那个放在 CF 中,但 CF = 0。

4

2 回答 2

4

阅读手册,该CF标志表示无符号溢出。但是,这并不意味着另外溢出。

您假设设置的第 8 位表示溢出,但是,减法则相反。如果没有设置,这将表明借位发生在位向上。

如果我们用 替换416那么我们会看到溢出:

00001100b - 00010000b = 00001100b + 11110000b = 11111100b = 252d

您可以看到没有进位到第 8 位,尽管发生了无符号溢出。CF 标志不是进行负数相加的结果。它只是表示减法溢出。在这种情况下,这意味着它与溢出位相反。


不同的架构对进位标志的处理方式不同。有些人用它来表示发生了借用,而有些人用它来表示将要借用的那个位。它们通常有一条指令(sbb在 x86 中),允许通过使用标志作为输入的一部分进行链接。

于 2020-11-20T18:11:19.477 回答
2

从小学开始 a - b = a + (-b)。从介绍到编程 -a = ~a + 1. 所以在逻辑上

        1
 00001100
+11111011
=========

并完成它

111111111
 00001100
+11111011
=========
 00001000

然后是架构问题,在你将进位和第二个操作数反转为减法的过程中。一些架构反转进位以使其代表借用标志,有些架构将其保持不变作为不借用标志。12 - 4 没有借用所以我们可以预测借用,但必须看架构文件才能看到。特别是诸如小于和大于以及检查哪些标志之类的事情...

此外,除了 msbit 的进位和进位之外的一侧是相同的,因此在这种情况下没有带符号的溢出(+8 可以用 8 位表示)。

维基百科指出 x86 使用借用标志

第一个使用该位作为借位标志,如果 a<b 在计算 a-b 时设置它,则必须执行借位。

 00111
  0010
+ 1011
======
  1110

2 - 4, 2 < 4, 进位为 0, 所以设置借位标志

这加上您的证据表明 x86 在减法的过程中反转进位位以指示借位。

你的实验看起来一切都很好。

请注意,这是二进制补码的美妙之处,即加法和减法运算使用相同的逻辑工作,并且不必知道有符号和无符号。使用不均匀大小(x 位乘以 x 位 = x+x 位。x+x 位除以 x 位)时乘法和除法确实关心有符号与无符号。他们确实经常添加有符号溢出和无符号溢出,以帮助有符号与无符号条件分支,它们非常关心有符号与无符号的有符号条件分支,并且相同风格的无符号大多数时间不是相同的标志。

编辑

我们仍然不理解您的困惑。即使实现为加法,处理器也知道它是什么操作,但它仍然是减法操作。想想如果用软件编写它,你会怎么做。

我的 8088 英特尔手册的旧打印副本说。

如果设置了 CF(进位标志),则结果的高位(8 位或 16 位)发生进位或借位。该标志由加减多字节数的指令使用。循环指令还可以通过将其放置在进位标志中来隔离内存或寄存器中的位。

对于添加操作,如果 (1) 存在进位,则设置 cdf。对于本例中的子操作,如果有借入,则设置 cf。与您如何在逻辑中实现操作无关,这是标志的定义,因此,如果您构建减法逻辑(不太可能),那么如果您使用加法逻辑进行减法,您最终仍会反转某些内容,您最终会反转进位。(这两种情况你最终都会反转执行)。

使用 4 位,因为它都可以扩展到 n 位数。

在逻辑上你会做类似的事情

result = 0aaaa + 0iiii + 1;

或者

result = 1aaaa - 0bbbb;

其中iii =〜b;

我们以1-4为例

使用加法器实现减法 1 - 4 = 1 + (-4) 数学与计算机无关,比计算机早了几个世纪。

      1
  00001
+ 01011
========

填入

  00111
  00001
+ 01011
========
  01101

进位位为 0,因此进位标志 = ~0 = 1 表示发生借位。结果是 1101 (-3)。

使用带有小扭曲的小学减法,不必翻转操作数并否定结果

 10001
-01011
=======

因为这是一个文本表示,将使用 2 来表示 10 二进制,因为我无法适应它。

 10001
-00100
=======

这样做需要更多的工作,二进制补码特别使加减法更容易,因为您可以使用加法器进行减法,因此使用长减法执行此操作需要很多步骤。

我不用借钱也能走到这一步

 10001
-00100
=======
    01

感谢您的问题并在维基百科上查找借用,我现在知道他们称之为美国方法。

我需要从 16s 列中借一个来使 8s 列成为 2,因为fours 列有一个零,必须继续直到你达到一个非零然后从那里借。

 02001
-00100
=======
    01

然后我必须从八列中借用一些东西在四列中。

 01201
-00100
=======
    01

现在我们可以继续

 01201
-00100
=======
 01101

我们得到与使用加法方法相同的结果,第 16 列第 1 列需要在不反转操作数的情况下进行适当的借位,在小学我们会通过执行 4 - 1 来执行 1 - 4,然后否定结果

  4
- 1
=====
  3

我们会把它变成-3,因为你怎么能证明一个没有任何东西可以借用的借用(使用美国方法)?

所以两者都以 01101 结束,这是 0 的进位和 -3 的结果。但是因为定义说

如果设置了 CF(进位标志),则存在......借位,结果的高位

所以我们需要进位标志是1,所以

if(add) cf = carry_out;
if(sub) cf = ~carry_out;

您可以在一些与您在逻辑中看到的非常相似的 C 代码中自己实现所有这些。

说一个 4 位 alu 加法和两种可能的减法形式,进位标志表示借位(如果借位则设置)。

unsigned int c_flag;
unsigned int n_flag;
unsigned int z_flag;
unsigned int v_flag;
unsigned int alu_add ( unsigned int a, unsigned int b )
{
    unsigned int c;

    c = a + b;
    c_flag = (c>>4)&1;
    c &= 0xF;
    if(c) z_flag = 1; else z_flag = 0;
    n_flag = (c>>3)&1;
    v_flag = 0;
    if((a&8)==(b&8)) if((b&8)==(c&8)) v_flag = 1;
    return(c);
}
unsigned int alu_sub ( unsigned int a, unsigned int b )
{
    unsigned int c;

    b = (~b) & 0xF;
    c = a + b + 1;
    c_flag = ((~c)>>4)&1;
    c &= 0xF;
    if(c) z_flag = 1; else z_flag = 0;
    n_flag = (c>>3)&1;
    v_flag = 0;
    if((a&8)==(b&8)) if((b&8)==(c&8)) v_flag = 1;
    return(c);
}
unsigned int alu_sub_alt ( unsigned int a, unsigned int b )
{
    unsigned int c;

    c = (0x10|a) - b;
    c_flag = ((~c)>>4)&1;
    c &= 0xF;
    if(c) z_flag = 1; else z_flag = 0;
    n_flag = (c>>3)&1;
    v_flag = 0;
    if((a&8)==(b&8)) if((b&8)==(c&8)) v_flag = 1;
    return(c);
}

测试向量的选择

1 0  1( 1)  0( 0) add =  1( 1) cZnV sub =  1( 1) cZnv sub_alt =  1( 1) cZnV
1 1  1( 1)  1( 1) add =  2( 2) cZnV sub =  0( 0) cznv sub_alt =  0( 0) cznV
1 2  1( 1)  2( 2) add =  3( 3) cZnV sub = 15(-1) CZNv sub_alt = 15(-1) CZNv
1 3  1( 1)  3( 3) add =  4( 4) cZnV sub = 14(-2) CZNv sub_alt = 14(-2) CZNv
1 4  1( 1)  4( 4) add =  5( 5) cZnV sub = 13(-3) CZNv sub_alt = 13(-3) CZNv
1 5  1( 1)  5( 5) add =  6( 6) cZnV sub = 12(-4) CZNv sub_alt = 12(-4) CZNv
1 6  1( 1)  6( 6) add =  7( 7) cZnV sub = 11(-5) CZNv sub_alt = 11(-5) CZNv

我们这里没有人可以说我们可以访问生产中实际英特尔芯片的源代码,因为我们将受到各种方式的 NDA 或员工合同等的保护。

但是今天处理器的代码将用一些 HDL 编写,我怀疑英特尔不会直接使用 Verilog,而是使用其他一些内部语言,然后编译为 Verilog,然后将 Verilog 提供给综合工具,该工具最终决定如何处理事情。

你可以做这样的事情

assign fadder_out       = { 1'd0,a} + {1'd0,b_not} + 5'b00001;

或类似的东西

assign fadder_out       = { 1'd1,a} - {1'd0,b};

接着

assign cf = alu_op == add ? fadder_out[4] : 
            alu_op == sub ? ~fadder_out[4] ;

为什么英特尔选择将 cf 表示为借用标志而不是原始执行?可能是因为 8080 做到了,因为 8008 做到了,因为 4004 做到了。我怀疑当时大楼里的人已经不能问了,如果他们是的话,他们可能无法联系到。

选择确实是任意的,只要您的条件和借位减去(如果有的话)都符合相同的选择,那么任何一种情况都可以正常工作,因为我们知道,因为世界上一定比例的处理器采用原始执行,我们可以认为它是不借(如果借则为 0),其余的称其为借(如果借则为 1)。

甚至早在 8086 之后,布局/掩模都是在绘图台上手工完成的,每一层、每个晶体管都是手工绘制的。您是否使用减法在逻辑中实现减法,是否使用加法器等。在 RISC 之前使用 CISC 的一个重要原因是因为您没有在原始门中实现整个该死的东西,您实现了具有大量布线的模块,并且您通过基于 rom 的状态机来提供它。CISC指令导致ROM中的偏移被执行,微码如果你愿意或状态机输入然后导致门在这里和那里翻转,减法可能意味着反转b操作数,反转进位,锁存加法器输出,锁存反转进位出去。加法可以是不反转 b 操作数,不反转进位,锁存加法器输出,锁存不反转进位。

或者他们可能已经为加法中的减法实现了一个单独的块,就像 XOR 和 OR 和 AND 等有一个单独的块一样。它们都可以以任何形式(反转或不反转)输入 a 和 b 操作数因为它们的执行和零标志等通常是特定于操作的,并且在 x86 的情况下。

所以我们不知道。4004 可能 8008 可能 8080 可能还有一些 8086 已经被执行visual6502(如果不是其他)的人切片和扫描,如果您想了解历史,也许您实际上可以查看大门,看看它是如何实现的。您还可以查看 8080、8008 和 4004 以查看它们是否有标志(很可能),如果有,它们是否指定了借用标志等等。

如果将 cf 定义为 1 用于借位,则无论它在逻辑中实现为加法还是减法,如果操作是减法、借位减法或比较,则需要将进位反转为 cf。

您在以下问题中调查了 4 + (-12) 和 4 - 12 之间的差异。一个使用加法指令,另一个使用减法指令,如果减去 cf = ~carry_out,如果上面使用了两个逻辑模型中的任何一个. 如果您有一个 result = 0aaaa - 0bbbb 导致结果为 1(也许是奥地利方法),那么该逻辑可以传递该位。我无法想象也无法展示它。

于 2020-11-20T18:25:37.423 回答