C 和 asm 中的 imulq 和 unsigned long long 溢出检测
imulq and unsigned long long overflow detection in C and asm
作为汇编新手,我使用 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
.
就可以了
在这种情况下(在汇编中)进行乘法运算的正确方法是什么?
当两个值相乘时,无论是无符号乘法还是有符号乘法,结果的最低有效位完全相同。因此,如果将两个 32 位值相乘,将得到一个 64 位结果,无论乘法是有符号还是无符号,其低 32 位都是相同的。 64 位乘法也是如此,它产生 128 位结果,两种情况下的低 64 位相同。
因此,编译器通常对两种类型的乘法都使用 IMUL
指令(其助记符表示带符号的乘法),因为它比 MUL
更灵活,而且通常速度更快。 MUL
只有一种形式(允许任意通用寄存器或内存位置乘以隐含的目标寄存器 AL/AX/EAX/RAX),IMUL
有多种形式,包括一个 -操作数形式(同MUL
),双操作数形式(寄存器或内存×寄存器或内存或立即数),三操作数形式(寄存器或内存×立即数,将结果存入第三个目的寄存器).英特尔文档中提供了更多详细信息(请参阅 x86 tag wiki for links), or quick reference for MUL and IMUL.
编译器可以一直使用 IMUL
的原因是你丢弃了结果的高位。当您执行 32 位 × 32 位乘法并将结果存储在 32 位变量中时,整个 64 位结果的高 32 位将被丢弃。同样,对于 64 位 × 64 位乘法,它会丢弃 128 位结果的高 64 位,只留下低 64 位,无论是有符号乘法还是无符号乘法,它们都是相同的。
引用英特尔手册:
The two- and three-operand forms [of IMUL] may also be used with unsigned operands because the lower half of the product is the same regardless if the operands are signed or unsigned. The CF and OF flags, however, cannot be used to determine if the upper half of the result is non-zero.
Peter Cordes 在他的 .
的一部分中也很好地解释了这一点
无论如何,当你自己写汇编代码的时候,你必须决定你是想和编译器做同样的事情,扔掉乘积的高位,还是要保留它们。如果你不关心高位,假设运算不会溢出,就写和编译器一样的代码。
如果您确实关心高位,只需使用 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 相比,简单地拥有一个不可预测的分支会使您的性能落后
如果没有一堆额外的代码,您似乎无法使用 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.
我将此函数编译为 return 值和无符号环绕检测布尔值。显然 Rust 的 ABI return 将指针作为隐藏的第一个参数传递,而不是像我认为 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
指令和单操作数完全乘法的额外 uop 比我们在双操作数 imul
.[=49= 之后进行额外检查所能做的任何事情都更有效。 ]
"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
和 mul
只有全乘法 (N x N => 2N) 结果的上半部分不同。我认为英特尔选择 imul
作为具有多个显式操作数的那个
imul r32, r32, sign-extended-imm8
会更有意义,因为符号扩展可能比零扩展更有用。
不过,我才刚刚意识到 imul
的标志结果是仅签名的。有趣的一点。
why does gcc not use mul
for unsigned multiplication?
因为单操作数 mul
/imul
速度较慢(根据 Agner Fog's insn tables. See also the x86 tag wiki,在 Intel CPU 上是 2 微指令而不是 1 微指令)。他们还使用更多的寄存器:他们需要在 rax
中输入一个,并在 rdx:rax
中产生输出,因此通常需要额外的 mov
指令来移动数据 in/out那些 regs.
因此,如果您不关心标志结果,imul r64, r64
是比 mul r64
更好的选择。
在 Intel CPU 上 imul r64,r64
实际上比 mul r32
快。在其他一些 CPU 上情况并非如此,包括 AMD Bulldozer 系列,其中 64 位乘法运算速度稍慢。但是由于 mul r32
将它的结果放入 edx:eax
而不是只有一个目标寄存器,所以在大多数情况下它们不是彼此的直接替换。
作为汇编新手,我使用 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
.
在这种情况下(在汇编中)进行乘法运算的正确方法是什么?
当两个值相乘时,无论是无符号乘法还是有符号乘法,结果的最低有效位完全相同。因此,如果将两个 32 位值相乘,将得到一个 64 位结果,无论乘法是有符号还是无符号,其低 32 位都是相同的。 64 位乘法也是如此,它产生 128 位结果,两种情况下的低 64 位相同。
因此,编译器通常对两种类型的乘法都使用 IMUL
指令(其助记符表示带符号的乘法),因为它比 MUL
更灵活,而且通常速度更快。 MUL
只有一种形式(允许任意通用寄存器或内存位置乘以隐含的目标寄存器 AL/AX/EAX/RAX),IMUL
有多种形式,包括一个 -操作数形式(同MUL
),双操作数形式(寄存器或内存×寄存器或内存或立即数),三操作数形式(寄存器或内存×立即数,将结果存入第三个目的寄存器).英特尔文档中提供了更多详细信息(请参阅 x86 tag wiki for links), or quick reference for MUL and IMUL.
编译器可以一直使用 IMUL
的原因是你丢弃了结果的高位。当您执行 32 位 × 32 位乘法并将结果存储在 32 位变量中时,整个 64 位结果的高 32 位将被丢弃。同样,对于 64 位 × 64 位乘法,它会丢弃 128 位结果的高 64 位,只留下低 64 位,无论是有符号乘法还是无符号乘法,它们都是相同的。
引用英特尔手册:
The two- and three-operand forms [of IMUL] may also be used with unsigned operands because the lower half of the product is the same regardless if the operands are signed or unsigned. The CF and OF flags, however, cannot be used to determine if the upper half of the result is non-zero.
Peter Cordes 在他的
无论如何,当你自己写汇编代码的时候,你必须决定你是想和编译器做同样的事情,扔掉乘积的高位,还是要保留它们。如果你不关心高位,假设运算不会溢出,就写和编译器一样的代码。
如果您确实关心高位,只需使用 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
.
如果没有一堆额外的代码,您似乎无法使用 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.
我将此函数编译为 return 值和无符号环绕检测布尔值。显然 Rust 的 ABI return 将指针作为隐藏的第一个参数传递,而不是像我认为 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
指令和单操作数完全乘法的额外 uop 比我们在双操作数 imul
.[=49= 之后进行额外检查所能做的任何事情都更有效。 ]
"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
和 mul
只有全乘法 (N x N => 2N) 结果的上半部分不同。我认为英特尔选择 imul
作为具有多个显式操作数的那个
imul r32, r32, sign-extended-imm8
会更有意义,因为符号扩展可能比零扩展更有用。
不过,我才刚刚意识到 imul
的标志结果是仅签名的。有趣的一点。
why does gcc not use
mul
for unsigned multiplication?
因为单操作数 mul
/imul
速度较慢(根据 Agner Fog's insn tables. See also the x86 tag wiki,在 Intel CPU 上是 2 微指令而不是 1 微指令)。他们还使用更多的寄存器:他们需要在 rax
中输入一个,并在 rdx:rax
中产生输出,因此通常需要额外的 mov
指令来移动数据 in/out那些 regs.
因此,如果您不关心标志结果,imul r64, r64
是比 mul r64
更好的选择。
在 Intel CPU 上 imul r64,r64
实际上比 mul r32
快。在其他一些 CPU 上情况并非如此,包括 AMD Bulldozer 系列,其中 64 位乘法运算速度稍慢。但是由于 mul r32
将它的结果放入 edx:eax
而不是只有一个目标寄存器,所以在大多数情况下它们不是彼此的直接替换。