复制到NASM中的阵列

时间:2019-06-01 19:08:50

标签: assembly x86-64 nasm

我必须编写汇编代码,该代码在循环中复制100字节在内存中。我是这样写的:

section .data
    a times 100 db 1 ;reserve 100 bytes and fill with 1
    b times 100 db 0 ;reserve 100 bytes and fill with 0

    section _start
    global _start

    _start:
    mov rsi, a ;get array a address
    mov rdi, b ;get arrat b address

    _for: ;początek pętli
    cmp cx, 100     ;loop
    jae _end_for        ;loop
    push cx         ;loop

    mov byte al, [rsi]  ;get one byte from array a from al
    mov byte [rdi], al  ;put one byte from al to array b
    inc rsi         ;set rsi to next byte in array a
    inc rdi         ;set rdi to next byte in array b

    pop cx          ;loop
    inc cx          ;loop
    jmp _for        ;loop

_end_for:

_end:
    mov rax, 60
    mov rdi, 0
    syscall

我不确定复制部分。我从地址读取值到寄存器,然后将其放入另一个。这对我来说看起来不错,但是我不确定要递增rsirdi

真的够吗?
我是NASM和组装的新手,请帮助:-)

2 个答案:

答案 0 :(得分:2)

  

我知道rep movsb,但是任务是逐个字节地在循环中进行,我不知道是否可以做得更好。

如果您必须一次循环1个字节,这是有效执行此操作的方法。值得一提的是,有效循环对memcpy以外的其他情况也很有用!

首先,您知道循环主体应该至少运行一次,因此可以使用底部带有条件分支的普通循环结构。 (Why are loops always compiled into "do...while" style (tail jump)?

第二,如果您根本不打算展开,那么应该使用索引寻址模式,以避免必须增加两个指针。 (但实际上最好将其展开)。

如果不需要,请不要使用16位寄存器。最好使用32位操作数大小(ECX);编写一个32位寄存器隐式零扩展到64位,因此可以安全地将索引用作寻址模式的一部分。


您可以使用索引加载,但可以使用非索引存储,因此您的存储地址uops仍可以在port7上运行,这在Haswell / Skylake上对超线程更加友好。并避免在Sandybridge上分层。显然,一次复制1个字节对于性能来说是完全浪费的,但是有时您确实想循环并实际上在寄存器中的每个字节执行某些操作

您可以通过相对于dst索引src来做到这一点。

或者另一个技巧是将负索引计数到零,这样就避免了额外的cmp。让我们先这样做:

default rel       ; use RIP-relative addressing modes by default

ARR_SIZE  equ 100
section .data
    a:  times ARR_SIZE db 1

section .bss
    b:  resb ARR_SIZE       ;reserve n bytes of space in the BSS

    ;section _start   ; do *not* use custom section names unless you have a good reason
                      ; they might get linked with unexpected read/write/exec permission

section .text
global _start
_start:
    lea     rsi, [a+ARR_SIZE]   ; pointers to one-past-the-end of the arrays
    lea     rdi, [b+ARR_SIZE]   ; RIP-relative LEA is better than mov r64, imm64

    mov     rcx, -ARR_SIZE

.copy_loop:                 ; do {
    movzx   eax, byte [rsi+rcx]  ; load without a false dependency on the old value of RAX
    mov     [rdi+rcx], al
    inc     rcx
    jnz    .copy_loop       ; }while(++idx != 0);

.end:
    mov  eax, 60
    xor  edi, edi
    syscall             ; sys_exit(0)

在类似于静态(或其他非PIE)Linux可执行文件的位置相关代码中,mov edi, b+ARR_SIZE是将静态地址放入寄存器的最有效方法。

请勿将_用于所有标签名称。 _start之所以这样命名,是因为保留以_开头的C符号名称供实现使用。这不是您应该复制的东西;事实恰恰相反。

在函数中使用.foo作为本地标签名称。例如如果您在.foo:之后使用_start.foo:,则它是_start的简写。


相对于dst的src索引:

通常,您的输入和输出都不都是在静态存储中,因此您必须在运行时sub地址。在这里,如果将它们像您最初所做的那样放在同一部分中,mov rcx, a-b实际上会组装在一起。但是,如果没有,NASM拒绝。

实际上,我可以执行[rdi + (a-b)]或简单地[rdi - ARR_SIZE]来代替2寄存器寻址模式,因为我知道它们是连续的。

_start:
    lea     rdi, [b]   ; RIP-relative LEA is better than mov r64, imm64
    mov     rcx, a-b   ; distance between arrays so  [rdi+rcx] = [a]
;;; for a-b to assemble, I had to move b back to the .data section.

    lea     rdx, [rdi+ARR_SIZE]    ; end_dst pointer

.copy_loop:                 ; do {
    movzx   eax, byte [rdi + rcx]    ; src = dst+(src-dst)
    mov     [rdi], al
    inc     rdi

    cmp     rdi, rdx
    jbe    .copy_loop       ; }while(dst < end_dst);

数组末尾指针与使用foo.end()在C ++中获得指向过去的指针/迭代器的方式完全一样。

这需要INC + CMP / JCC作为循环开销。在AMD CPU上,CMP / JCC可以宏熔合为1个uop,而INC / JCC则不能,因此从末尾开始的额外CMP vs.索引基本上是免费的。 (代码大小除外)。

在Intel上,这避免了建立索引存储。在这种情况下,负载是纯负载,因此无论如何它都是单个uop,而无需与ALU uop保持微融合。英特尔可以对inc/jcc进行宏熔接,因此这确实会增加额外的循环开销。

如果您要展开,并且不需要避免为负载分配索引的寻址方式,则这种循环方式非常有用。但是,如果您将存储源用于诸如vaddps ymm0, ymm1, [rdi]之类的ALU指令,那么是的,您应该分别增加两个指针,以便可以对装载和存储使用非索引寻址模式,因为英特尔CPU比办法。 (端口7的存储AGU仅处理未建立索引的索引,并且某些微融合的负载会以建立索引的寻址模式分层。Micro fusion and addressing modes

答案 1 :(得分:1)

  

真的够吗?

是;您显示的代码足以复制数组。

对于性能/优化,您显示的代码可能更好。但是优化是一个滑坡,它绕过“ rep movsb更适合代码大小”,经过“带循环展开的SIMD”,最后到达“可以避免复制数组”。