【发布时间】:2013-11-25 07:42:39
【问题描述】:
如何在 32 位机器上添加几个 32 位数字但不损失精度,即在 64 位“伪寄存器”eax:edx 中。使用 Intel 语法汇编器。
【问题讨论】:
-
循环添加它们,如果你得到一个 CF,添加到 edx 寄存器。
标签: assembly x86 nasm bigint extended-precision
如何在 32 位机器上添加几个 32 位数字但不损失精度,即在 64 位“伪寄存器”eax:edx 中。使用 Intel 语法汇编器。
【问题讨论】:
标签: assembly x86 nasm bigint extended-precision
如果我正确理解了这个问题,您有两个 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 noprefix) 通常不应有 %reg 名称,只有 mov ecx, [ebp-16]。 Clang 甚至会拒绝这样的 asm。但是旧的 GCC 在这方面做得不好,甚至在 -masm=intel 中也打印了装饰器。更新的 GCC 将打印正确的代码。
为了在 32 位机器上添加 64 位数字,您必须首先将 64 位数字的上半部分移入寄存器 eax,然后将下半部分移入 edx。操作此数字时,您必须跟踪该数字在 eax/edx 中的放置方式。
【讨论】:
假设您要添加的 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 时出现部分寄存器停顿。)
mov x, y / sar x, 31 为任何寄存器模拟 cdq,从而避免将数据混洗进出 EAX 和 EDX。 Pentium 4 之后的所有 CPU 都具有高效的固定计数班次。 (与 CDQ 不同,它会破坏 FLAGS,因此如果您试图通过覆盖添加源来保存 reg,则无法在 add 和 adc 之间执行此操作)