【问题标题】:imulq and unsigned long long overflow detection in C and asmC 和 asm 中的 imulq 和 unsigned long long 溢出检测
【发布时间】:2016-11-10 05:57:47
【问题描述】:

作为组装新手,我使用 gcc 进行逆向工程。但现在我遇到了一个奇怪的问题:我尝试将两个 64 位整数相乘以得到 x86-64。 C - 代码如下所示:

unsigned long long 
val(unsigned long long a, unsigned long long b){
    return a*b;
}

并用 gcc 编译:

val:
    movq    %rdi, %rax
    imulq   %rsi, %rax
    ret

对无符号整数使用有符号乘法可能违反直觉,但它适用于 C。

但是,我想检查乘法是否溢出。现在,如果结果大于2^63-1,则设置溢出标志(我猜是因为它毕竟是有符号乘法)。但对于无符号 64 位,只要结果不大于 2^64-1,这仍然可以。

在这种情况下,做乘法(在汇编中)的正确方法是什么?

【问题讨论】:

  • 您能否事后进行测试,看看结果是否比以前“(unsigned ...) 小”?只是猜测......
  • 或者,您可以简单地使用mul,如果无符号乘法的结果溢出操作数大小,它将设置 CF 和 OF。
  • @ead:因为imuln*n -> n 位乘法执行相同的操作。此外,mul 只有一个操作数形式(它隐式使用[r/e]ax)并且总是破坏[r/e]ax[r/e]dx。它只是不太灵活。
  • 在 C 中,无符号乘法不会溢出。如果结果超出无符号类型的范围,则通过实际上丢弃高位进行调整。如果您想检查是否有必要进行任何此类调整,那么您正在做的不是 C 无符号乘法。 (我绝对不是暗示这有什么问题。)
  • @KeithThompson:“溢出”在这里被用来表示“环绕”,例如丢弃的高半部分是否非零。它是至少具有一种技术含义的词之一,但用于具有不同技术名称的其他相关事物。 (例如执行无符号加法)

标签: c assembly gcc x86-64 integer-overflow


【解决方案1】:

看起来你不能在没有一堆额外代码的情况下使用imul,因为 CF 和 OF 的设置方式相同。正如the "operation" section of the manual 所述,如果完整的 128b 结果与sign_extend(low_half_result) 不匹配,则会设置它们。所以你是对的,即使imul 的多操作数形式仍然有一些签名行为。如果它们像 add/sub 并独立设置 OF 和 CF,那就太好了,这样您就可以查看 CF 来获取未签名数据或 OF 来获取签名数据。

为某事找到一个好的 asm 序列的最佳方法之一是询问编译器。 C 没有方便的整数溢出检测,but Rust does

我编译了这个函数来返回值和无符号环绕检测布尔值。显然,Rust 的 ABI 将它们作为隐藏的第一个 arg 传递指针返回,而不是像我认为 C ABI 对这么小的结构那样在 rdx:rax 中。 :(

pub fn overflowing_mul(a: u64, b: u64) -> (u64, bool) {
  a.overflowing_mul(b)
}
    # frame-pointer boilerplate elided
    mov     rax, rsi
    mul     rdx
    mov     qword ptr [rdi], rax
    seto    byte ptr [rdi + 8]

    mov     rax, rdi                # return the pointer to the return-value
    ret

Godbolt compiler explorer (Rust 1.7.0) 的 Asm 输出。这或多或少证实了 mov 指令和单操作数全乘法的额外微指令比我们在双操作数 imul 后进行额外检查所能做的任何事情都更有效。

documentation for mul

"如果结果的上半部分为 0,则 OF 和 CF 标志设置为 0;否则,它们设置为 1。"

总而言之,使用mul 并检查OFCF 以查看高半部分是否非零。


mulimul 琐事:

imulmul 之间只有全乘 (N x N => 2N) 结果的上半部分不同。我认为英特尔选择 imul 作为具有多个显式操作数的那个,这样
imul r32, r32, sign-extended-imm8 会更有意义,因为符号扩展可能比零扩展更有用。

我只是意识到imul 的标志结果是仅签名的。有趣的一点。


为什么 gcc 不使用mul 进行无符号乘法?

因为单操作数mul/imul 较慢(根据Agner Fog's insn tables,Intel CPU 上是 2 uop 而不是 1。另请参阅 标签 wiki)。他们还使用更多的寄存器:他们需要rax 中的一个输入,并在rdx:rax 中产生他们的输出,因此通常需要额外的mov 指令来将数据移入/移出这些寄存器。

因此,如果您不关心标志结果,imul r64, r64 是比 mul r64 更好的选择。

在 Intel CPU 上imul r64,r64 实际上比mul r32 快。在其他一些 CPU 上情况并非如此,包括 AMD Bulldozer 系列,其中 64 位乘法速度稍慢。但由于mul r32 将其结果放入edx:eax 而不仅仅是一个目标寄存器,因此在大多数情况下它们并不是直接相互替换的。

【讨论】:

  • 你的最后一段让我有点困惑。如果在 Atom 和 AMD 上速度较慢,imul r64, 64 在大多数 CPU 上如何比 mul r32 更好?我读错了吗?
  • @CodyGray: mul r32 有额外的开销来将数据移动到正确的寄存器中。这弥补了其他 CPU 上的一些速度差异。此外,imul r64, r64 在现代 Intel 上更快,即人们正在调整的“大多数 CPU”。我的意思是“大多数 CPU”,平均按受欢迎程度加权,而不是“大多数类型的 CPU”。 IIRC,即使您将两个 32 位变量转换为 64 位,clang 也会生成 imul r64,r64
  • @CodyGray:虽然说实话,编译器的行为显然是因为它们很笨,而不一定是因为它更好。 Even with -march=bdver2 (i.e. AMD piledriver), clang and gcc use imul r64, r64。实际上,即使使用-march=atom,这也太可怕了。哦,我刚刚意识到mul r32 会将结果拆分为两个寄存器。嗯,我不确定是否还有有用的观点。也许我应该把最后一段删掉。
  • 我怀疑编译器更喜欢imul r64, r64,因为它使寄存器分配变得更加容易。但这也是将结果分配在两个寄存器之间的一个好点。合并这两个结果无疑会受到惩罚,当有 64 位寄存器可用时,您不妨从一开始就使用它们。但这在您将两个 32 位值相乘的情况下无关紧要,因为 C 和 C++ 语言标准允许编译器只丢弃结果的高 32 位。
  • @CodyGray: imul r32,r32 在所有 CPU 上总是至少与 mul r32 一样快。仅当编译器知道被乘数在其高 32 位中全为零时(例如,在强制转换之后),编译器才会出现这种情况。是的,合并需要 SHL + OR。我想我记得测试过编译器做了什么,其中任何一个都是有效的选项。也许将结果存储到一个结构中,它只是一个 64b 存储与两个 32b 存储?或者使用 ORing 上半部分和下半部分来强制编译器以任何一种方式组合它们。
【解决方案2】:

当两个值相乘时,结果的最低有效位完全相同,无论是无符号乘法还是有符号乘法。因此,如果您将两个 32 位值相乘,您将得到一个 64 位结果,其低 32 位是相同的,无论乘法是有符号还是无符号。对于 64 位乘法也是如此,它产生 128 位结果,两种情况下的低 64 位是相同的。

因此,编译器通常将IMUL 指令(其助记符建议有符号乘法)用于两种类型的乘法,因为它比MUL 更灵活,而且通常更快。 MUL 只有一种形式(允许任意通用寄存器或内存位置乘以隐含的目标寄存器 AL/AX/EAX/RAX),IMUL 有多种形式,包括单操作数形式(与MUL 相同)、双操作数形式(寄存器或内存 × 寄存器或内存或立即数)和三操作数形式(寄存器或内存 × 立即数,将结果存储在第三个目标寄存器中)。英特尔文档中提供了更多详细信息(有关链接,请参阅 标签 wiki)或MULIMUL 的快速参考。

编译器可以一直使用IMUL 的原因是你丢弃了结果的高位。当您执行 32 位 × 32 位乘法并将结果存储在 32 位变量中时,整个 64 位结果的高 32 位将被丢弃。同样,对于 64 位 × 64 位乘法也是如此,它会丢弃 128 位结果的高 64 位,只留下低 64 位,无论是有符号乘法还是无符号乘法,它们都是相同的。

引用英特尔手册:

[IMUL] 的二操作数和三操作数形式也可以与无符号操作数一起使用,因为无论操作数是有符号还是无符号,乘积的下半部分都是相同的。但是,CF 和 OF 标志不能用于确定结果的上半部分是否为非零。

Peter Cordes 在他的larger answer to a very general question on two's-complement arithmetic operations 的一部分中也很好地解释了这一点。

无论如何,在自己编写汇编代码时,您必须决定是要执行编译器所做的相同事情并丢弃产品的高位,还是要保留它们。如果不关心高位,假设操作不会溢出,就写和编译器一样的代码。

如果您确实关心高位,只需使用MUL 指令,如果乘积大于其操作数的类型,则设置 CF 和 OF 标志。

mov  rax, QWORD PTR [a]   ; put 64-bit operand 'a' into RAX
mov  rbx, QWORD PTR [b]   ; put 64-bit operand 'b' into RBX
mul  rbx                  ; multiply 'a' * 'b'
; 128-bit result is returned in RDX:RAX (upper-bits:lower-bits)

jo  ProductOverflowed

在这里使用MUL 几乎肯定比尝试找到一种使用IMUL 的方法并在之后测试高64 位以查看它们是否非零(这表明溢出)更有效。与使用 IMUL 保存的 1 或 2 μops 相比,简单地拥有一个不可预测的分支会使您的性能落后。

【讨论】:

  • 您不小心在此处使用了 2 操作数变体
  • @harold IMUL 的 2 操作数变体是故意的……我没有乘以立即数。
  • 但是结果不是128位
  • xD,我们都在几分钟内发布了我们的答案。但是您不需要在乘法之前将rdx 归零。它始终是只写操作数。所以mov rax, [a] / imul rax, [b] 会起作用,除了哈罗德指出的错误。 (您实际上需要单操作数形式mul [b]。)或者如果您真的想手动测试上半部分,请使用mov rdx, [a]/mulx rsi, rdi, [b]/test rsi,rsi。 (MULX is in BMI2)
  • 嗯,我认为双操作数版本的行为方式与单操作数版本相同,将结果放入 RDX:RAX。我想我读手册太快了。好吧,这使答案变得不那么有用。哦,好吧。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-01-27
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-07-29
  • 1970-01-01
  • 2011-08-06
相关资源
最近更新 更多