【问题标题】:Dividing in Assembly装配中的划分
【发布时间】:2018-09-10 20:04:48
【问题描述】:

我正在尝试基于 Linux 命令dc 用 C 语言定义一个计算器,程序的结构并不是那么重要,你只需要知道我得到两个数字,我想在输入 @987654322 时将它们相除@。因此,我将这两个数字发送到进行除法的汇编函数(参见下面的代码)。但这仅适用于正数。

当输入 999 3 / 时,它返回 333,这是正确的,但是当输入 -999 3 / 时,我得到一个奇怪的数字 1431655432,并且当输入两个负数(如 -999 -3 /)时,对于任何两个负数,我每次都得到 0。

汇编代码为:

section .text 
global _div 

_div: 
  push rbp            ; Save caller state 
  mov rbp, rsp 

  mov rax, rdi        ; Copy function args to registers: leftmost... 
  mov rbx, rsi        ; Next argument... 

  cqo
  idiv rbx            ; divide 2 arguments 
  mov [rbp-8], rax 

  pop rbp             ; Restore caller state 

【问题讨论】:

  • 你怎么打电话给_div
  • 这就是全部代码吗?我不太明白为什么你有mov [rbp-8], rax 而没有ret
  • 问题是您将 128 位值 RDX:RAX 除以 64 位值,但传递给函数的是 32 位值。例如 -999 作为 FFFFFC19 的 32 位值,但如果您进行 64 位除法,则 RAX 的高 64 位可能为零(并且您将 RAX 扩展为 RDX,因此 RDX 将为零)给您值 00000000FFFFFC19 。 00000000FFFFFC19 / 3 = 0x55555408 即 1431655432(十进制)
  • 您可以使用cdq 代替cqo 并使用idiv ebx 吗?
  • RBX 是 Linux 64 位调用约定中的非易失性寄存器,如 RBP,因此需要由您的函数保留。更好的选择是使用其他易失性(调用者保存的寄存器)之一,如 RCX 而不是 RBX。 RBX、RBP、R12 到 R15 都需要由你修改它们的函数来保存。

标签: c assembly x86-64 nasm division


【解决方案1】:

您的问题与如何将参数传递给_div 有关。

假设您的_div 的原型是:

int64_t _div(int32_t, int32_t);

然后,参数传入ediesi(即32位有符号整数),寄存器rdirsi的上半部分未定义。

符号扩展 在将 ediesi 分配给 raxrbx 以执行 64 位 signed 除法时需要(用于执行将需要 64 位 无符号 除法零扩展)。

也就是说,而不是:

mov   rax, rdi       
mov   rbx, rsi 

ediesi 上使用指令movsx扩展源代码:

movsx rax, edi
movsx rbx, esi

使用真正的 64 位操作数进行 64 位除法

之前的方法包括对“假”64 位操作数(即 符号扩展 32 位操作数)执行 64 位除法。将 64 位指令与“32 位操作数”混合通常不是一个好主意,因为它可能会导致 worse performance and larger code size

更好的方法是简单地更改 _div 函数的 C 原型以接受实际的 64 位参数,即:

int64_t _div(int64_t, int64_t);

这样,参数将被传递给 rdirsi(即已经是 64 位有符号整数),并对真正的 64 位整数执行 64 位除法。

改用 32 位除法

如果适合您的需要,您可能还想考虑使用 32 位 idiv,因为它比 64 位除法执行得更快,并且生成的代码大小更小(没有 REX 前缀):

...   
mov eax, edi      
mov ebx, esi      
cdq
idiv ebx
...

_div 的原型是:

int32_t _div(int32_t, int32_t);

【讨论】:

  • idiv 的 32 位整数扩展为假的 64 位整数是 IMO 有点误导的建议,因为 128/64 位除法比 64/32 位除法慢得多。特别是在这种情况下,将 idiv 参数简单更改为 32 位大小将完全修复它。 (将整个函数原型更改为支持 64 位整数是有效的建议,那没问题,我说的是假的 32->64 位推广)
  • 那么第二种方法呢?那将是真正的 64 位操作数,不是吗?
  • 是的,第二种方法使函数完全 64 位整数有效,这使得性能价格可以接受。即使您的答案的第一部分也可能很有趣,并且可以帮助某些人意识到在必须将 32 位整数提升为 64b 的情况下该怎么做,但就在 int div(int a, int b) 的这种特殊情况下,它有点.. 错误。 :)
  • 你忘了说破坏rbx 不安全,而且没用。在最后一个示例中,像普通人一样使用idiv esi。在您的第一个示例中使用movsx rcx, esimovsx rsi, esi。 (与 zero-extension with zero latency (mov / movzx elimination) 的某些情况不同,在同一个寄存器内而不是在不同的寄存器内进行符号扩展没有缺点。)
【解决方案2】:

您的 cmets 说您将整数传递给 _idiv。如果您使用的是 int,这些是 32 位值:

extern int _div (int a, int b);

当传递给函数时,a 将位于 RDI 的底部 32 位,b 将位于 RSI 的底部 32 位。参数的高 32 位可能是垃圾,但通常为零,but doesn't have to be the case

如果您使用 64 位寄存器作为 IDIV 的除数,则除数为 RDX:RAX / 64 位除数(在您的情况下为 RBX)。这里的问题是您使用完整的 64 位寄存器进行 32 位除法。如果我们为了论证而假设 RDIRSI 的高位最初为 0,那么 RSI 将是 0x00000000FFFFFC19 (RAX) 而 RDI 将是0x0000000000000003 (RBX)。 CQO 会将 RAX 扩展到 RDXRAX 的最高位为零,因此 RDX 将为零。划分如下:

0x0000000000000000000000000FFFFFC19 / 0x0000000000000003 = 0x55555408

0x55555408 恰好是 1431655432(十进制),这是您看到的结果。对此的一种解决方法是使用 32 位寄存器进行除法。要将 EAXRAX 的低 32 位)签名扩展为 EDX,您可以使用 CDQ 而不是 CQO.然后您可以将 EDX:EAX 除以 EBX。这应该为您提供您正在寻找的 32 位签名除法。代码如下所示:

cdq
idiv ebx                 ; divide 2 arguments EDX:EAX by EBX

请注意,RBXRBPR12R15 都需要由您的您修改它们(它们是AMD 64-bit ABI 中的易失性寄存器)。如果您修改 RBX,您需要确保像使用 RBP 一样保存和恢复它。更好的选择是使用一个易失性寄存器,如 RCX 而不是 RBX


您不需要中间寄存器来放置除数。您可以直接使用 RSI(或固定版本中的 ESI),而不是将其移动到像 RBX 这样的寄存器中。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-17
    • 1970-01-01
    • 2022-12-11
    相关资源
    最近更新 更多