C unsigned long long和imulq

时间:2016-07-07 19:12:38

标签: c gcc assembly x86-64

作为一个刚接触组装的人,我使用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,这仍然可以。

在这种情况下进行乘法(在装配中)的正确方法是什么?

2 个答案:

答案 0 :(得分:8)

看起来如果没有一堆额外的代码就不能使用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

我编译了这个函数来返回值和unsigned-wraparound检测bool。显然Rust的ABI返回它们作为隐藏的第一个arg传递指针,而不是rdx:rax,就像我认为C ABI会用于这样一个小结构。 :(

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指令和单操作数完全乘法的额外uop比在双操作数imul之后使用额外检查所做的任何事情都更有效。

documentation for mul

  

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

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

mulimul琐事:

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

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

  

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

因为单操作数mul / imul较慢(根据Agner Fog's insn tables,在Intel CPU上为2 uops而不是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而不是仅仅一个目标寄存器,因此无论如何它们都不是彼此的直接替换。

答案 1 :(得分:7)

当乘以两个值时,无论是无符号乘法还是有符号乘法,结果的最低有效位都完全相同。因此,如果乘以两个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位,无论是有符号还是无符号乘法都是相同的。 / p>

引自英特尔手册:

  

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

彼得·科德斯在他的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μs。