如何在汇编中将两个十六进制 128 位数字相乘

Posted

技术标签:

【中文标题】如何在汇编中将两个十六进制 128 位数字相乘【英文标题】:How can I multiply two hex 128 bit numbers in assembly 【发布时间】:2017-04-09 07:04:23 【问题描述】:

我在内存中有两个 128 位的十六进制数,例如(小端序):

x:0x12 0x45 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
y:0x36 0xa1 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

我必须在这两个数字之间执行无符号乘法,所以我的新数字将是:

z:0xcc 0xe3 0x7e 0x2b 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

现在,我知道我可以将 x 和 y 的一半移动到 raxrbx 寄存器中,例如,执行 mul 操作,并对另一半执行相同的操作。问题是这样做我失去了结转,我不知道如何避免这种情况。我面临这个问题大约有 4 个小时,我能看到的唯一解决方案是二进制转换 (and shl,1)。

你能就这个问题给我一些意见吗? 我认为最好的解决方案是按标准时间计算一个字节。

【问题讨论】:

x86 提供 64 × 64 → 128 乘法,结果在 rdx:rax 中。您可以使用该操作来获得所需的结果。如果您有兴趣,我可以提供详细信息。请告诉我你想要有符号还是无符号乘法。 很高兴。乘法必须是无符号的。但我必须做一个 128 x 128 -> 128 乘法。如果它溢出,我只保留最低部分(这是正常的)并设置 OF 标志。告诉我 简单:最大数为(2^128)-1 @Davide 你对我的回答满意吗?您还需要其他信息吗? @AdrianColomichi 这里的数字太短了,帮不上忙,简单的方法已经只有 3 个乘法 【参考方案1】:

令μ = 264,那么我们可以将你的128位数字ab分解成a = a1μ + a2b = b1μ + b2。然后我们可以通过首先计算部分积来计算 c = ab 与 64 · 64 → 128 位乘法:

q1μ + q2 = a2 b2r1μ + r2 = a1b2s1μ + s2 = a2b em>1t1μ + t2 = a1b1

然后将它们累积成 256 位结果(在进行加法时注意溢出!):

c = t1μ3 + (t 2 + s1 + r1) μ2 + (s2 + r2 + q1) μ + q2

【讨论】:

只是一个观察:如果你使用他的xyz,OP 可能更容易更好地联系起来 如果只需要结果的低128,则不需要某些项,也不需要首先累加完整的256b结果。进位严格从右到左传播,因此在您想要的 128 以上的位上发生的任何事情都不会影响结果。 (我相信你知道这一点,但自 not everyone does 以来值得指出)。因此,OP 甚至不需要计算高半部分的 a1b1 乘法。【参考方案2】:

像往常一样,询问编译器如何高效地做某事:64 位平台上的 GNU C 支持 __int128_t__uint128_t

__uint128_t mul128(__uint128_t a, __uint128_t b)  return a*b; 

编译为 (gcc6.2 -O3 on Godbolt)

    imul    rsi, rdx        # tmp94, b
    mov     rax, rdi  # tmp93, a
    imul    rcx, rdi        # tmp95, a
    mul     rdx       # b
    add     rcx, rsi  # tmp96, tmp94
    add     rdx, rcx  #, tmp96
    ret

由于这是针对 x86-64 System V 调用约定,a 在 RSI:RDI 中,而 b 在 RCX:RDX 中。 结果在 RDX:RAX 中返回

非常漂亮,它只需要一条 MOV 指令,因为 gcc 不需要 a_upper * b_lower 的高半结果,反之亦然。它可以使用更快的 2 操作数形式的 IMUL 破坏输入的高半部分,因为它们只使用一次。

使用-march=haswell 启用BMI2,gcc 使用MULX 来避免甚至是一个MOV。


有时编译器输出并不完美,但通常通用策略是手动优化的良好起点。


当然,如果您真正首先想要的是 C 语言中的 128 位乘法,只需使用编译器的内置支持即可。这让优化器完成了它的工作,通常比在 inline-asm 中编写几个部分时提供更好的结果。 (https://gcc.gnu.org/wiki/DontUseInlineAsm)。

【讨论】:

在这个例子中,rdx 包含一半的 128 位数字,而 rdi 包含另一半? @Davide:为您突出显示返回结果位置。 128b 结果在 RDX 中有高半部分,在 RAX 中有低半部分。请注意,结果的低 64 位仅取决于两个输入的低 64 位。 (由 MUL 制作)。 对我不起作用。我想乘以 596a*18c2。然后在 rsi 我移动 59h 和 rdi 6ah。在 rcx 中我移动 18h 和 rdx 0c2h。结果在 rdx 0x4d62 和 rax 0x5054 中,与实际乘法不对应,结果必须为 0x8a5b254 @Davide:您没有将输入分成两半,而是在 64 位边界上拆分它们。您乘以 0x000000590000006a * 0x00000018000000c2,然后显然查看了结果的低 64 位。结果是0x00004d6200005054,此代码返回为RDX = 00004d62,以及RAX = 0x00005054,这就是你得到的。 @Davide:显然你不明白。您需要设置 rsi=0:rdi=0x596arcx=0:rdx=0x18c2 来传递您描述的输入,并获得您想要的结果。此外,如果您知道高半部分为零,只需使用一条 MUL 指令即可获得 128b 的结果。

以上是关于如何在汇编中将两个十六进制 128 位数字相乘的主要内容,如果未能解决你的问题,请参考以下文章

如何在 MIPS 中将两个数字相乘,得到大于 32 位的乘积?

Java/scala BigDecimal 解析 128 位十六进制值

如何在 C++ 中将 RGB 颜色值转换为十六进制值?

如何在Java中将字符从字母字符转换为十六进制数字?

如何在 C++ 中将十六进制数字转换为二进制?

ATmega128:加减 16 位数字(汇编)