1

我正在为处理器架构使用yasm汇编程序。x86_64假设我已经在该部分中定义了三个数字.data

section .data
;CONSTANTS:
SYSTEM_EXIT    equ 60
SUCCESS_EXIT   equ 0

;VARIABLES:
dVar1    dd  40400
wVar2    dw -234
bVar3    db -23
dRes     dd  0    ;quotient
dRem     dd  0    ;reminder

我想要做的是将有符号双字dVar1乘以有符号字,dVar2然后再除以有符号字节bVar3

下面我将参考本书介绍我的“解决方案”,以了解我为什么要执行每个步骤。问题在文末。

dVar1 * wVar2(有符号)

我没有看到任何明确的规则,即乘法仅适用于相同大小的数字。但是看到一些隐含的。这就是为什么我使用转换wVar2

movsx    eax, word [wVar2]    ;[wVar2] now in eax

现在“它们”的大小相同,所以我只是将它们相乘:

imul    dword [dVar1]    ;edx:eax = eax * [dVar1]

...例如,将ax(16 位)乘以字操作数(也是 16 位)的结果提供双字(32 位)结果。然而,结果并没有放在eax中(这可能更容易),它放在两个寄存器中,dx用于高阶结果(16 位)和ax用于低阶结果(16 位),通常写为dx:ax(按照惯例)。

正如我正确理解的那样,结果现在在edx:eax.

edx:eax / bVar3(签名)

...被除数需要D寄存器(用于高阶部分)和A(用于低阶部分)...如果执行了先前的乘法,则DA寄存器可能已经正确设置(这是我的案例[OP的注释])。

...此外,必须将A以及可能的D寄存器组合用于被除数。

  • Byte Divide: ax for 16-bits
  • 字除法:dx:ax用于 32 位
  • 双字除法:edx:eax用于 64 位(这是我的情况 [OP's note])
  • 四字除法:rdx:rax for 128-bits

因此,最初我转换bVar3为双字,然后将其分开:

movsx    ebx, byte [bVar3]    ;ebx = [bVar3]
idiv     ebx,    ;eax = edx:eax / [bVar3]

整个代码然后

section .data
;CONSTANTS:
SYSTEM_EXIT    equ 60
SUCCESS_EXIT   equ 0

;VARIABLES:
dVar1    dd  40400
wVar2    dw -234
bVar3    db -23
dRes     dd  0    ;quotient
dRem     dd  0    ;reminder

section .text

global _start
_start:
    movsx   ebx, byte [bVar3]    ;conversion to double-word
    movsx   eax, word [wVar2]    ;conversion to double-word
    imul    dword [dVar1]        ;edx:eax = eax * [dVar1]
    idiv    ebx                  ;eax = edx:eax / [bVar3], edx = edx:eax % [bVar3]
    mov     dword [dRes], eax
    mov     dword [dRem], edx
last:
    mov     rax, SYSTEM_EXIT
    mov     rdi, SUCCESS_EXIT
    syscall

我使用调试器并查看正确答案:

(gdb) x/dw &dRes
0x600159:   411026
(gdb) x/dw &dRem
0x60015d:   -2

但我不确定以下几点。

  1. 真的有必要做我做过的那些步骤吗?它是“尽可能少的行数”解决方案吗?
  2. 这是正确的解决方案吗?我的意思是我可能在这里犯了一个错误或错过了一些重要的事情。

PS 也许这个问题更可能是 CodeReview SE 问题。如果你也这么认为,请告诉我。

4

1 回答 1

1

它是“尽可能少的行数”解决方案吗?

您的代码看起来不错,并且没有任何浪费的指令或明显的效率(除了在您的系统调用中,mov64 位寄存器在哪里浪费代码大小)。

movsx但是在其他两个负载之后做第二个。乱序执行不会分析依赖链并首先在关键路径上进行加载。在结果准备好movsx之前不需要第二次加载,所以把它放在后面,这样前两次加载(和内存操作数)可以尽早执行并开始。imulimulmovsximulimul


为最少数量的指令(源代码行)优化 asm 通常没有用/不重要。选择代码大小(最少的机器代码字节)或性能(最少的微指令、最低延迟等。请参阅Agner Fog 的优化指南以及x86 标签 wiki中的其他链接)。例如,在 Intel CPU 上idiv是微编码的,并且在所有 CPU 上都比您使用的任何其他指令慢得多。

在具有固定宽度指令的体系结构上,指令数量是代码大小的代理,但在具有可变长度指令的 x86 上就是这种情况。

idiv无论如何,除非除数是编译时常量,否则没有避免的好方法:为什么 GCC 在实现整数除法时使用乘以一个奇怪的数字?,并且 32 位操作数大小(带有 64 位被除数)是您可以使用的最小/最快版本。(与大多数指令不同,div使用更窄的操作数更快)。

对于代码大小,您可能希望使用一个 RIP-relative lea rdi, [rel dVar1],然后访问其他变量,例如[rdi + 4],它需要 2 个字节(modr/m + disp8)而不是 5 个字节(modr/m + rel32)。即每个内存操作数增加 1 个字节(与寄存器源相比)。

在字和字节位置之前分配双字结果位置是有意义的,因此所有双字自然对齐,您不必担心它们在缓存行中拆分会导致性能损失。(或align 4db标签之前、标签和 之前使用dd)。


这里的一个危险是,#DE如果 的商不适合 32 位寄存器,则 64/32 => 32 位除法可能会溢出和出错(在 Linux上导致 SIGFPE )。(dVar1*wVar2) / bVar3您可以通过使用 64 位乘法和除法来避免这种情况,如果这是一个问题,编译器会采用这种方式。但请注意,在 Haswell/Skylake 上,64 位idiv比 32 位慢约 3 倍。idiv( http://agner.org/optimize/ )

; fully safe version for full range of all inputs (other than divide by 0)
movsx   rcx, byte [bVar3]
movsxd  rax, dword [dVar1]   ; new mnemonic for x86-64 dword -> qword sign extension
imul    rax, rcx             ; rax *= rcx; rdx untouched.

cqo                          ; sign extend rax into rdx:rax
movsx   rcx, word [wVar2]
idiv    rcx

mov     qword [qRes], rax    ; quotient could be up to 32+16 bits
mov     dword [dRem], edx    ; we know the remainder is small, because the divisor was a sign-extended byte

这显然是更大的代码大小(更多的指令,并且更多的指令具有使用 64 位操作数大小的 REX 前缀),但不太明显它慢得多,因为 64 位idiv很慢,正如我之前所说的。

在大多数 CPU 上使用带有 2 个显式操作数movsxd的 64 位之前imul更好,但在一些 64 位imul速度较慢的 CPU(AMD Bulldozer 系列或 Intel Atom)上,您可以使用

movsx   eax, byte [bVar3]
imul    dword [dVar1]       ; result in edx:eax

shl     rdx, 32
or      rax, rdx            ; result in rax

但是,在现代主流 CPU 上,2 操作数imul更快,因为它只需要写入一个寄存器。


除了指令选择:

您将代码放在该.data部分中! 使用section .textbefore _start:,或将您的数据放在最后。(与 C 不同,您可以在声明它们之前在源代码中引用符号,包括标签和常量。仅按顺序应用equ汇编程序宏 ( ))。%define foo bar

此外,您的源数据可以进入section .rodata,您的输出可以进入 BSS。(或者将它们留在寄存器中,除非您的分配需要内存;没有任何东西在使用它们。)

使用 RIP 相对寻址而不是 32 位绝对寻址:该default rel指令不是默认值,但 RIP 相对编码比[abs dVar1]. (32 位绝对适用于 64 位可执行文件,但不适用于 64 位与位置无关的可执行文件)。

如果div商不适合 32 位(如您现有的代码),则可以出错,此版本包含我建议的所有修复:

default rel      ; RIP-relative addressing is more efficient, but not the default

;; section .text  ; already the default section
global _start
_start:
    movsx   eax, word [wVar2]
    imul    dword [dVar1]        ;edx:eax = eax * [dVar1]

    movsx   ecx, byte [bVar3]
    idiv    ecx                  ;eax = edx:eax / [bVar3], edx = edx:eax % [bVar3]

 ; leaving the result in registers is as good as memory, IMO
 ; but presumably your assignment said to store to memory.
    mov     dword [dRes], eax
    mov     dword [dRem], edx

.last:                           ; local label, or don't use a label at all
    mov     eax, SYS_exit
    xor     edi, edi             ; rdi = SUCCESS_EXIT.  don't use mov reg,0
    syscall                      ; sys_exit(0), 64-bit ABI.


section .bss
dRes:     resd 1
dRem:     resd 1

section .rodata
dVar1:    dd  40400
wVar2:    dw -234
bVar3:    db -23

; doesn't matter what part of the file you put these in.
; use the same names as asm/unistd.h, SYS_xxx
SYS_exit       equ 60
SUCCESS_EXIT   equ 0

样式::在标签之后使用,即使它是可选的。 dVar1: dd 40400而不是dVar1 dd 40400. 这是一个好习惯,以防您不小心使用了与指令助记符匹配的标签名称。Likeenter dd 40400可能会给出一个令人困惑的错误消息,但enter: dd 40400会起作用。

不要使用mov将寄存器设置为零,使用xor same,same它是因为它更小更快。当你知道你的常数很小时,不要mov使用 64 位寄存器,让隐式零扩展来完成它的工作。mov rax,60(虽然 NASM 可以,但 YASM 不会为您优化mov eax,60)。 为什么 32 位寄存器上的 x86-64 指令会将完整 64 位寄存器的上部归零?.


我没有看到任何明确的规则,即乘法仅适用于相同大小的数字。但是看到一些隐含的。

(几乎)所有 x86 指令的所有操作数都需要相同的大小。例外情况包括movzx/ movsx, (和其他变量计数移位/旋转),以及类似在不同寄存器集之间复制数据的shr reg, cl东西。movd xmm0, eax还有 imm8 控制操作数,如pshufd xmm0, xmm1, 0xFF.

通常与相同大小的输入/输出一起工作的指令没有动态加宽输入之一的版本。

您可以在英特尔的指令集参考手册条目中看到imul它明确记录了它使用的大小。32x32 => 64 位结果的唯一形式是IMUL r/m32,即显式操作数必须是 32 位寄存器或内存,才能与隐式eax源操作数一起使用。

所以是的,movsx从内存中加载是迄今为止实现这一点的最佳方式。(但不是唯一的;cbw/cwde也可以在 EAX 中进行符号扩展,并且总是shl eax, 16/sar eax,16符号从 16 扩展到 32。这些比movsx-load 差得多。)

于 2018-03-13T02:39:31.133 回答