我必须编写汇编代码,循环复制内存中的 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
我不确定复制部分。我将地址中的值读取到寄存器中,然后将其放入另一个寄存器中。这对我来说看起来不错,但我不确定是否要增加 rsi 和 rdi。
真的够了吗?
我是 NASM 和汇编的新手,所以请帮助:-)
最佳答案
I know about rep movsb but task has been to make it in loop byte after byte, I don't know if it could be done better way.
如果您必须一次循环 1 个字节,以下是如何有效地执行此操作的方法。值得一提的是,因为有效循环对于 memcpy
以外的情况很有用。还有!
首先,您知道循环体应该至少运行一次,因此您可以使用底部有条件分支的普通循环结构。 (Why are loops always compiled into "do...while" style (tail jump)?)
其次,如果您根本不打算展开,那么您应该使用索引寻址模式以避免增加两个指针。 (但实际上最好展开)。
如果没有必要,不要使用 16 位寄存器。首选 32 位操作数大小 (ECX);写入 32 位寄存器会隐式零扩展到 64 位,因此使用索引作为寻址模式的一部分是安全的。
您可以使用索引加载但使用非索引存储,这样您的存储地址 uops 仍然可以在端口 7 上运行,这使得它在 Haswell/Skylake 上对超线程更加友好。并避免在 Sandybridge 上分层。显然,一次复制 1 个字节对于性能来说完全是垃圾,但有时您确实想要循环并实际上对寄存器中的每个字节做一些事情,并且您可以不需要使用 SSE2 手动对其进行矢量化(一次处理 16 个字节)。
您可以通过相对于 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 就会拒绝。
事实上,我可以不使用 2 寄存器寻址模式,而是这样做 [rdi + (a-b)]
,或者简单地[rdi - ARR_SIZE]
因为我知道它们是连续的。
_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);
数组末尾指针与 C++ 中的 foo.end()
完全相同。获取指向末尾一位的指针/迭代器。
这需要 INC + CMP/JCC 作为循环开销。在 AMD CPU 上,CMP/JCC 可以宏融合到 1 uop,但 INC/JCC 不能,因此最后的额外 CMP 与索引基本上是免费的。 (代码大小除外)。
在英特尔上,这避免了索引存储。在这种情况下,负载是纯负载,因此无论如何它都是单个 uop,无需与 ALU uop 保持微融合。 Intel 可以宏熔断 inc/jcc
所以这确实会花费额外的 uop 循环开销。
如果您正在展开,如果您不需要避免加载的索引寻址模式,那么这种循环方式很好。但如果您使用 ALU 指令的内存源,如 vaddps ymm0, ymm1, [rdi]
,那么是的,您应该分别递增两个指针,以便可以对加载和存储使用非索引寻址模式,因为 Intel CPU 这样效率更高。 (端口 7 存储 AGU 仅处理非索引,一些微熔断负载使用索引寻址模式未层压。Micro fusion and addressing modes)
关于assembly - 复制到 NASM 中的阵列,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56409664/