作为汇编新手,我使用 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
,这仍然可以。
在这种情况下(在汇编中)进行乘法的正确方法是什么?
最佳答案
看起来你不能使用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 .
我编译此函数以返回值和无符号环绕检测 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
之后进行额外检查所能做的任何事情都更有效。 .
"The OF and CF flags are set to 0 if the upper half of the result is 0; otherwise, they are set to 1."
总而言之,使用mul
并检查 OF
或 CF
查看高半部分是否非零。
mul
与 imul
琐事:
在 imul
之间只有全乘法 (N x N => 2N) 结果的上半部分不同和 mul
.我认为英特尔选择了 imul
作为具有多个显式操作数的那个
imul r32, r32, sign-extended-imm8
会更有意义,因为符号扩展可能比零扩展更有用。
我才刚刚意识到标志来自 imul
但是,仅签名。有趣的一点。
why does gcc not use
mul
for unsigned multiplication?
因为单操作数 mul
/imul
较慢(根据 Agner Fog's insn tables,在 Intel CPU 上是 2 微指令而不是 1 微指令。另请参见 x86 标签 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
而不是只有一个目标寄存器,它们在大多数情况下都不是彼此的直接替代品。
关于c - C 和 asm 中的 imulq 和 unsigned long long 溢出检测,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38253541/