检查进位标志是否设置

14

如何使用内嵌汇编[gcc,intel,c]在操作后检查进位标志是否设置?


你想在 asm 代码块内测试它,还是想将进位标志的状态传递回嵌入 asm 代码的 C 代码中的某个地方? - Paul R
在汇编块内进行测试就足够了。将其传递出去不应该那么困难。 - hans
相关:从C程序读取标志寄存器 - 你可以使用GCC6的标志输出语法(例如:将条件标志用作GNU C内联asm输出)将汇编中的标志输出到C中,但是你不能让汇编读取FLAGS输入。在C中进行+<<操作没有任何明确定义的进位,并且可能会编译为LEA或不涉及标志的其他内容。或者被优化掉。 - Peter Cordes
5个回答

16

sbb %eax,%eax 如果进位标志位被设置,它将在eax中存储-1,如果未设置,则为0。不需要预先清除eax的值; 将eax从自己中减去会为您执行此操作。这种技术非常强大,因为您可以使用结果作为位掩码来修改计算结果,而无需使用条件跳转。

请注意,仅当内联asm块内执行的算术设置了进位标志时,才可以测试进位标志的有效性。您不能测试在C代码中执行的计算的进位,因为编译器可能会优化/重新排列会破坏进位标志的各种方式。


使用条件标志作为GNU C内联汇编输出展示了新的GCC6 "=@ccc" (cf_output) 语法,用于直接将CF或其他标志条件声明为编译器的输出,而不必在asm模板中将布尔值或整数实现为寄存器。关于您警告不要尝试读取由编译器生成的指令设置的FLAGS:从C程序读取标志寄存器有更多相关信息。 - Peter Cordes

11

通过有条件跳转指令jc(如果进位则跳转)或jnc(如果没有进位则跳转)。

或者您可以存储进位标志位,

;; Intel syntax
mov eax, 0
adc eax, 0 ; add with carry

“setc al”是将FLAGS条件转化为整数的标准方式(如果您希望其为32位,则需要在操作之前将EAX清零)。或者更好的方法是,使用现代编译器,例如GCC6,可以使用语法告诉编译器关于FLAGS的内容,这样就不必浪费指令来制作一个整数,编译器生成的代码将只是“test”。 使用条件标志作为GNU C内联asm输出 - Peter Cordes

6
然而,x86汇编器具有专门的快速 ALU标志测试指令SETcc,其中cc是所需的ALU标志。因此,您可以编写如下代码:
setc    AL                           //will set AL register to 1 or clear to 0 depend on carry flag

or

setc    byte ptr [edx]               //will set memory byte on location edx depend on carry flag

or even

setc    byte ptr [CarryFlagTestByte]  //will set memory variable on location CarryFlagTestByte depend on carry flag

使用SETcc指令,您可以测试标志位,如进位、零、符号、溢出或奇偶校验,一些SETcc指令允许同时测试两个标志位。 编辑: 添加了一个在Delphi中制作的简单测试,以消除fast一词的疑虑。
procedure TfrmTest.ButtonTestClick(Sender: TObject);
  function GetCPUTimeStamp: int64;
  asm
    rdtsc
  end;
var
 ii, i: int64;
begin
  i := GetCPUTimeStamp;
  asm
    mov   ecx, 1000000
@repeat:
    mov   al, 0
    adc   al, 0
    mov   al, 0
    adc   al, 0
    mov   al, 0
    adc   al, 0
    mov   al, 0
    adc   al, 0
    loop  @repeat
  end;
  i := GetCPUTimeStamp - i;

  ii := GetCPUTimeStamp;
  asm
    mov   ecx, 1000000
@repeat:
    setc  al
    setc  al
    setc  al
    setc  al
    loop  @repeat
  end;
  ii := GetCPUTimeStamp - ii;
  caption := IntToStr(i) + '  ' +  IntToStr(ii));
end;

使用指令setc的循环(1M次迭代)比使用adc指令的循环快5倍以上。

编辑:添加了第二个测试,测试结果存储在寄存器AL中,并累计在寄存器CL中,以使情况更加真实。

procedure TfrmTestOtlContainers.Button1Click(Sender: TObject);
  function GetCPUTimeStamp: int64;
  asm
    rdtsc
  end;

var
 ii, i: int64;
begin
  i := GetCPUTimeStamp;
  asm
    xor   ecx, ecx
    mov   edx, $AAAAAAAA

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

    shl   edx, 1
    mov   al, 0
    adc   al, 0
    add   cl, al

  end;
  i := GetCPUTimeStamp - i;

  ii := GetCPUTimeStamp;
  asm
    xor   ecx, ecx
    mov   edx, $AAAAAAAA

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

    shl   edx, 1
    setc  al
    add   cl, al

  end;
  ii := GetCPUTimeStamp - ii;
  caption := IntToStr(i) + '  ' +  IntToStr(ii);
end;

使用SETcc指令的常规部分仍然比较快,快约20%。


你有证据称它们运行速度快吗?我已经有几个世代没有追踪最新的CPU了,但长期以来,这些被认为是缓慢的遗留操作码。 - R.. GitHub STOP HELPING ICE
1
@R.. 是的,SETcc指令虽然有点老旧,但比ADC或使用条件跳转指令如JC或JNC要快得多。 - GJ.
以上的测试是毫无意义的。首先,结果甚至没有被使用,因此任何延迟都将被掩盖。其次,非SETcc版本使用的是mov/adc而不是sbb,后者的字节数大约是前者的6倍,操作码数量也翻了一倍。如果您想对此进行基准测试,应该尝试使用实际利用结果进行计算的代码来测试两个版本。 - R.. GitHub STOP HELPING ICE
你仍在使用mov/adc而不是sbb。尝试使用sbb,然后从“累加器”(CL)中减去结果(它将为0或-1,因此在您进行减法时将添加0或1)。还要确保您使用最短的指令形式 - 我记不清了,但我似乎记得根据您使用的寄存器或寄存器大小,sbb可能更短。由于缓存问题,更紧密的循环可能会运行得更快。 - R.. GitHub STOP HELPING ICE
@R.: Agner Fog的指令表显示setcc为单uop,每个时钟周期2个吞吐量,延迟1个周期。所以它们很快。唯一的问题是它们只写入寄存器的低8位,因此您需要先执行xor reg,reg或在之后执行movzx eax,al。在某些英特尔CPU上,movzx之后避免了部分寄存器减速。在非英特尔处理器上,xor之前避免了对eax进行错误依赖,并且可以将其从循环中提取出来。在英特尔SnB系列上,sbb解码为2个uops,延迟2个周期。 - Peter Cordes
显示剩余2条评论

2
第一个函数执行无符号加法,然后使用进位标志(CF)测试溢出。volatile必须保留,否则优化器会重新排列指令,这几乎保证了错误的结果。我曾经看到过优化器将jnc更改为jae(也基于CF)。
/* Performs r = a + b, returns 1 if the result is safe (no overflow), 0 otherwise */
int add_u32(uint32_t a, uint32_t b, uint32_t* r)
{
    volatile int no_carry = 1;
    volatile uint32_t result = a + b;

    asm volatile
    (
     "jnc 1f          ;"
     "movl $0, %[xc]  ;"
     "1:              ;"
     : [xc] "=m" (no_carry)
     );

    if(r)
        *r = result;

    return no_carry;
}

下一个函数是针对有符号整数的。同样使用volatile。请注意,有符号整数的数学运算通过jno跳转到OF标志。我见过优化器将其更改为基于OF的jnb
/* Performs r = a + b, returns 1 if the result is safe (no overflow), 0 otherwise */
int add_i32(int32_t a, int32_t b, int32_t* r)
{   
    volatile int no_overflow = 1;
    volatile int32_t result = a + b;

    asm volatile
    (
     "jno 1f          ;"
     "movl $0, %[xo]  ;"
     "1:              ;"
     : [xo] "=m" (no_overflow)
     );

    if(r)
        *r = result;

    return no_overflow;
}

从大局来看,您可能会按以下方式使用这些功能。在同样的大局中,许多人可能会拒绝额外的工作和美学上的不美观,直到遭受溢出/包装/下溢的影响。

int r, a, b;
...

if(!add_i32(a, b, &r))
    abort(); // Integer overflow!!!

...

内联GCC汇编在GCC 3.1及以上版本可用。请参阅带有C表达式操作数的汇编指令,或搜索“GCC扩展汇编”。

最后,在Visual Studio中相同的代码(代码生成方面没有太大区别),但语法更容易,因为MASM允许您跳转到C标签:

/* Performs r = a + b, returns 1 if the result is safe (no overflow), 0 otherwise */
int add_i32(__int32 a, __int32 b, __int32* r)
{   
    volatile int no_overflow = 1;
    volatile __int32 result = a + b;

    __asm
    {
        jno NO_OVERFLOW;
        mov no_overflow, 0;
    NO_OVERFLOW:
    }

    if(r)
        *r = result;

    return no_overflow;
}

不足之处在于,上述MASM代码仅适用于x86汇编。对于x64汇编来说,没有内联函数,因此您将需要在汇编中编写代码(在单独的文件中),并使用MASM64进行编译。


你可以使用goto扩展来跳转到C标签,例如:asm volatile goto( "ja %l[clabel]" : : : "memory" : clabel );,其中clabel是C标签。 - Jens Munk
阅读下面@R..的答案似乎否定了你的函数。 “您应该知道,仅当内联asm块中执行算术运算时才可以测试进位标志的有效性。” 你确定吗? - DrBeco
@DrBeco - 是的,对于GCC来说,这取决于情况。GCC将确保您块中指令的“连续性”,但它可能会插入/交错自己的指令。如果GCC指令不修改CC,那么一切都会没问题。Microsoft的内联汇编程序不会受到GCC的限制。 - jww
没有必要使用volatile关键字编写如此大量的代码,因为编译器无法对其进行任何优化。最好编写类似于r = a + b; carry = (r < a) ;这样的普通C代码,编译器可以进行优化。有许多类似的C变体,适用于有符号、无符号、不同大小的操作数等情况。 - Pierre

-1
这可能会提供一个想法或解决方案,如果它是正确的。 在我发现内联汇编之前,我一直在为 wrap around 进行测试而苦苦挣扎。 我尝试使用各种边缘值进行测试,似乎工作正常。 程序从 cmdln 获取输入并将其转换为整数,然后输出十六进制和二进制值。

gcc 版本是 11.2.1。

$> gcc -Wall -std=c99 -O2 -o uilt uilt.c

代码片段:

size_t i = 0;
int mul = 10;
uint128_t sum = 0;
int int_array[48] = {0};

// fill arr. with ea. str val in argv[1] str. converted to int vals.
while (i < strlen(argv[1])) {
  // chk they are digit chars, if not, skip iter
  if (isdigit(argv[1][i]) == 0) {
    i++;
    continue;
  }
  int_array[i] = (argv[1][i] - 48);
  sum = int_array[i] + (sum * mul);

  /* check carry flag */
  __asm__ goto("jc %l0"
               : /* no outputs  */
               : /* no inputs   */
               : /* no clobbers */
               : carry);

  /* no carry */
  goto its_good;

 carry:
  system("clear");
  printf("\n\n\tERROR!!!\
        \n\n\t!!!!!!! uilt has ABORTED !!!!!!\
        \n\tCmdln arg exceeds 2^127 bit limit\
        \n\twhen converted from string to 127\
        \n\tbit unsigned __int128.\n\n");
  exit(1);

 its_good:
  i++;
 }

一些输出:

[jim@nitroII uiltDev]$ ./uilt 1

Dec: 1

Hex: 0x0001

Bin: 0x0001

[jim@nitroII uiltDev]$ ./uilt 255

Dec: 255

Hex: 0x00ff

Bin: 0x0000 1111 1111

[jim@nitroII uiltDev]$ ./uilt 18446744073709551616

Dec: 18446744073709551616

Hex: 0x0001 0000 0000 0000 0000

Bin: 0x0001 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000

[jim@nitroII uiltDev]$ ./uilt 340282366920938463463374607431768211455

Dec: 340282366920938463463374607431768211455

Hex: 0x0000 ffff ffff ffff ffff ffff ffff ffff ffff

Bin: 0x0000 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

Dec: 340282366920938463463374607431768211456

        ERROR!!!            

        !!!!!!! uilt has ABORTED !!!!!!            
        Cmdln arg exceeds 2^127 bit limit            
        when converted from string to 127            
        bit unsigned __int128.

不幸的是,这通常是不安全的,只是碰巧能够工作。无法告诉GCC,C中最后一个+之后的CF是asm语句的输入。如果是这种情况,那只是运气好而已。GCC可以轻松地使用LEA或优化SIMD或其他不涉及x86 add指令的东西。或者在您的asm语句之前运行一些其他指令,例如add rsp,16(如果有很多参数的函数调用进行了清理)(或者32位模式下只有少数)。 - Peter Cordes
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心中找到有关如何编写良好答案的更多信息。 - Community
1
@Peter,非常感谢你提供的所有信息。我会尝试通过阅读建议并实施if(sum < a)来修复它。你是完全正确的,我只是改变了最后一位数字。 - scurvydog
@Peter,你在评论3中提到检查前导位数。你是指“前导(或最高位)比特”吗?我试图使用bsr进行内联操作来实现这一目的。也许可以使用位掩码?shr和位掩码呢? - scurvydog
除了我正在进行的工作中有关于处理前导零的情况的 TODO 注释,这会扰乱长度计算,因为我们实际上需要最重要的非零位,如果它很长的话。 (00123 总是可以,所以 len < 10 检查仍然可以通过)。否则,与 memchr 类似,但是匹配不等于 0,就像你使用 _mm_cmpeq_epi8 / _mm_movemask_epi8(并循环如果全部为 1)/ ~mask / bsftzcnt 找到第一个非 '0' 位置。 - Peter Cordes
显示剩余6条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接