签名乘法,然后在YASM中签名除法(x86_64架构)

时间:2018-03-12 12:41:15

标签: assembly x86-64 integer-arithmetic yasm

我正在为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

下面我介绍我的解决方案"引用this一书,说明我为什么要做每一步。问题在文本末尾。

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位)和< strong> ax 用于低阶结果(16位),通常写为 dx:ax (按惯例)。

据我所知,结果现在在edx:eax

edx:eax / bVar3(签名)

  

...红利需要 D 寄存器(对于高位部分)和 A (对于低位部分)...如果是先前的乘法执行后, D A 寄存器可能已经设置正确(这是我的情况[OP注意事项])。

  

...此外, A ,可能还有 D 寄存器,必须结合使用。

     
      
  • 字节除以: ax 表示16位
  •   
  • 单词划分: dx:ax 表示32位
  •   
  • 双字划分: edx:eax 64位(这是我的情况[OP&#39; s])
  •   
  • Quadword Divide: rdx:rax for 128-bit
  •   

所以最初我将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. 我真的有必要做我已经做过的那些步骤吗?它是&#34;尽可能少的行数&#34;溶液
  2. 这是完全正确的解决方案吗?我的意思是我可能犯了错误或错过了重要的事情。
  3. P.S。也许这个问题更可能是CodeReview SE的问题。如果你也这么想,请告诉我。

1 个答案:

答案 0 :(得分:1)

  

它是&#34;尽可能少的行数&#34;溶液

您的代码看起来很好,并且没有任何浪费的指令或明显的效率(除非在系统调用中,mov到64位寄存器是浪费代码大小)。< / p>

但是在其他两次加载之后再做第二次movsx。乱序执行不会分析依赖关系链并首先在关键路径上执行加载。在movsx结果准备就绪之前,不需要第二次imul加载,因此请将其放在imul之后,以便前两次加载(movsx和{{{} 1}}内存操作数)可以尽早执行,让imul开始。

针对最少数量的指令(源代码行)优化asm通常没有用/重要。要么是代码大小(最少的机器代码字节),要么是性能(最小的uops,最低的延迟等,请参阅Agner Fog's optimization guide,以及the x86 tag wiki中的其他链接)。例如,imulmicrocoded on Intel CPUs,并且在所有CPU上都比您使用的任何其他指令慢得多。

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

无论如何,除非除数是编译时常量:Why does GCC use multiplication by a strange number in implementing integer division?和32位操作数大小(64位),否则没有好办法避免idiv。 bit dividend)是您可以使用的最小/最快版本。 (与大多数指令不同,idiv使用更窄的操作数会更快。)

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

在单词和字节位置之前分配您的双字结果位置是有意义的,因此所有双字都是自然对齐的,您不必担心它们被分割在缓存行中的性能损失。 (或者在[rdi + 4]之后,在标签和align 4之前使用db

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

idiv

这显然是更大的代码大小(更多的指令,更多的指令使用64位操作数大小的REX前缀),但不太明显它的慢,因为64正如我之前所说,比特; 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 很慢。

在具有2个显式操作数的64位idiv之前使用movsxd在大多数CPU上更好,但在64位imul缓慢的几个CPU上(AMD Bulldozer-family)或者Intel Atom),你可以使用

imul

在现代主流CPU上,2操作数movsx eax, byte [bVar3] imul dword [dVar1] ; result in edx:eax shl rdx, 32 or rax, rdx ; result in rax 更快,因为它只需写一个寄存器。

除指令选择外:

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

此外,您的源数据可能会进入%define foo bar ,您的输出可能会进入BSS。 (或者将它们留在寄存器中,除非你的作业需要记忆;没有任何东西在使用它们。)

使用RIP相对寻址而不是32位绝对值: section .rodata 指令不是默认值,但RIP相对编码的时间比{{短1个字节1}}。 (32位绝对值适用于64位可执行文件,但not in 64-bit position-independent executables)。

如果商数不适合32位(如现有代码),default rel可以解决问题,这个版本有我建议的所有修复:

[abs dVar1]

样式:在标签后使用div,即使它是可选的。 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只会有效。

请勿使用enter dd 40400将寄存器设置为零,use xor same,same because it's smaller and faster。当你知道你的常数很小时,不要enter: dd 40400到64位寄存器,让隐式零扩展完成它的工作。 (YASM并没有为您优化movmov,尽管NASM确实如此)。 Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?

  

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

(差不多)所有x86指令对所有操作数都需要相同的大小。例外情况包括mov rax,60 / mov eax,60movzx(和其他变量计数移位/旋转),以及movsx之类的内容,可以在不同的寄存器集之间复制数据。还有imm8控制操作数,如shr reg, cl

通常与相同尺寸的输入/输出一起使用的说明不具备可以动态扩展其中一个输入的版本。

您可以看到movd xmm0, eax明确记录了Intel's instruction-set reference manual entry for it 中适用的尺寸。 32x32 =&gt;的唯一形式64位结果为pshufd xmm0, xmm1, 0xFF,即显式操作数必须是32位寄存器或内存,以与隐式imul源操作数一起使用。

所以是的,来自内存的IMUL r/m32加载是迄今为止实现此目的的最佳方式。 (但不是唯一的; cbw / cwde also work to sign extend within EAX,并且始终eax / movsx的签名从16延伸到32.这些比shl eax, 16更糟糕 - 负载。)