【问题标题】:Adding 32-bit numbers on a 32 machine, widening to a 64-bit sum in two registers在 32 位机器上添加 32 位数字,在两个寄存器中扩展为 64 位和
【发布时间】:2013-11-25 07:42:39
【问题描述】:

如何在 32 位机器上添加几个 32 位数字但不损失精度,即在 64 位“伪寄存器”eax:edx 中。使用 Intel 语法汇编器。

【问题讨论】:

  • 循环添加它们,如果你得到一个 CF,添加到 edx 寄存器。

标签: assembly x86 nasm bigint extended-precision


【解决方案1】:

如果我正确理解了这个问题,您有两个 32 位整数相加,可能会得到一个 64 位整数。您希望在没有 32 位溢出的情况下执行此操作。

看看编译器做了什么:

$ cat add64.c
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>

int main (int argc, char **argv) {
    uint32_t a, b;

    a = strtoll(argv[1], NULL, 10);
    b = strtoll(argv[2], NULL, 10);
    printf("%lu + %lu = %llu\n", a, b, (uint64_t) a + b);
    return 0;
}
$ gcc -m32 -g -o add64 add64.c 
$ ./add64 3000000000 4000000000
3000000000 + 4000000000 = 7000000000
$ gcc -m32 -g -fverbose-asm -masm=intel -S add64.c
$ $EDITOR add64.s &
[5] 340
$

相关生成的程序集是:

    mov %ecx, DWORD PTR [%ebp-16]   # D.2300, a
    mov %ebx, 0 # D.2300,
    mov %eax, DWORD PTR [%ebp-12]   # D.2301, b
    mov %edx, 0 # D.2301,
    add %eax, %ecx  # D.2302, D.2300
    adc %edx, %ebx  # D.2302, D.2300

【讨论】:

  • 请注意,Intel-syntax (GAS .intel_syntax noprefix) 通常不应有 %reg 名称,只有 mov ecx, [ebp-16]。 Clang 甚至会拒绝这样的 asm。但是旧的 GCC 在这方面做得不好,甚至在 -masm=intel 中也打印了装饰器。更新的 GCC 将打印正确的代码。
【解决方案2】:

为了在 32 位机器上添加 64 位数字,您必须首先将 64 位数字的上半部分移入寄存器 eax,然后将下半部分移入 edx。操作此数字时,您必须跟踪该数字在 eax/edx 中的放置方式。

【讨论】:

    【解决方案3】:

    假设您要添加的 32 位数字在 EAX 和 EBX 中,并且是无符号的:

        xor edx,edx       ;Set edx to zero
        add eax,ebx
        adc edx,0         ;edx:eax = eax + ebx
    

    这与在加法之前将值零扩展为 64 位,然后进行 64 位加法基本相同。

    对于有符号整数,这将不起作用(例如“0 + (-1) != 0x00000000 + 0xFFFFFFFF != 0x00000000FFFFFFFF”),因为您需要符号扩展而不是零扩展。为此:

        cdq                 ;Set all bits in edx to the sign of eax
        xchg ebx,eax        ;eax = original ebx
        mov ecx,edx         ;ecx:ebx = original eax sign extended
        cdq                 ;edx:eax = original ebx sign extended
    
        add eax,ebx
        adc edx,ecx         ;edx:eax = eax + ebx
    

    “可能更慢,取决于它是哪个 CPU”(见注)替代方法是通过向它们添加 0x80000000 来强制它们进入无符号整数的范围,然后通过从中减去 2*0x80000000(或 0x0000000100000000)来纠正结果.减0x0000000100000000相当于高位dword减1,相当于高位dword加0xFFFFFFFF,所以可以:

        add eax,0x80000000
        add ebx,0x80000000
    
        xor edx,edx         ;Set edx to zero
        add eax,ebx
        adc edx,0xFFFFFFFF  ;edx:eax = eax + 0x80000000 + ebx + 0x80000000 + (-0x0000000100000000)
    

    注意:如果您关心性能;这种替代方法使您有机会将 0x80000000 添加到早期代码中的值(无论值来自何处),并且通常可以更快地结束(特别是如果多次使用相同的 32 位值,和/或如果添加可以免费并入其他计算中)。

    对于“混合类型”,您只需要将有符号值提升为 64 位。例如,如果 EAX 已签名而 EBX 未签名:

        cdq                 ;Set all bits in edx to the sign of eax
        add eax,ebx
        adc edx,0           ;edx:eax = eax + ebx
    

    当然,对于较新的 CPU,您会使用 64 位代码。对于无符号的 32 位值,默认情况下它们已经是零扩展的,您只需要一条 add rax,rbx 指令。对于已签名的号码,您可能需要签署扩展(如果您不能/没有事先签署扩展它们),例如:

        movsx rax,eax
        movsx rbx,ebx
        add rax,rbx
    

    【讨论】:

    • 您可以将adc edx,0 替换为setc dl,它在某些CPU 上具有相同的大小并且速度更快(在Sandybridge 之前的Intel,它引入了adc reg,0 single-uop special case)。您仍然需要在添加之前进行异或归零(为了正确并避免稍后在那些较旧的 Intel CPU 上读取 EDX 时出现部分寄存器停顿。)
    • @PeterCordes:是的。理想情况下,汇编器将具有窥视孔优化器(和“指令重新调度器”),因此您无需为微优化牺牲代码清晰度。 ;-)
    • 请注意,您可以使用mov x, y / sar x, 31 为任何寄存器模拟 cdq,从而避免将数据混洗进出 EAX 和 EDX。 Pentium 4 之后的所有 CPU 都具有高效的固定计数班次。 (与 CDQ 不同,它会破坏 FLAGS,因此如果您试图通过覆盖添加源来保存 reg,则无法在 add 和 adc 之间执行此操作)
    猜你喜欢
    • 1970-01-01
    • 2011-10-26
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-04
    • 2012-01-24
    • 2021-04-05
    相关资源
    最近更新 更多