我将如何递增寄存器中的每个字节? (64 位,Linux,NASM)

How would I increment every byte in a register? (64 Bit, Linux, NASM)

最近开始学习汇编,给自己立了一个小项目。目标是使用循环。我想将 0x414141 移动到 RAX,然后遍历 RAX,并递增每个字节,这样 RAX 将在代码末尾包含 0x424242。

我已经尝试增加字节 rax,但在尝试编译它时我总是从 NASM 得到错误。目前,我有工作代码,最终会将 RAX 增加到等于 0x414144。我似乎找不到任何 looks/sounds 接近我想做的事情。 (但这有多难,对吧?)

global _start

section .text
_start:
    mov rax, 0x414141
    mov rcx, 3
strLoop:
    inc rax
    loop strLoop

    mov rax, 60
    mov rdi, 0
    syscall
    ; ^ exit 

当我查看 GDB 中的 RAX 时,在此代码中,我希望它是 0x414144,它确实是。但是,我想让我的代码达到 0x424242 的位置,我想这将是该项目的预期结果。

对于 asm 来说,有多种好方法可以实现您想要的。最重要的问题是字节之间的进位传播是否是一个可能的问题。


选项 1(带进位传播的简单加法)

如果您只关心 64 位 RAX 的低 4 字节,您可能应该只对 32 位操作数大小使用 EAX。 (写入 32 位寄存器零扩展到完整的 64 位寄存器,这与写入 8 位或 16 位寄存器不同。)

因此,正如评论中所提到的,这是对您的问题进行一种解释的诀窍。

 add   eax, 0x010101

如果你真的想要 RAX 的每个 字节,那就是 8 个字节。但是只有 mov 支持 64 位立即数,add 不支持。您可以在另一个寄存器中创建一个常量:

 mov   rdx, 0x0101010101010101
 add   rax, rdx

上面使用单个宽 add 的方法的缺点是 某个字节的溢出会传播到下一个更高的字节 。所以它并不是真正的 4 或 8 independent 字节添加,除非你知道每个单独的字节不会溢出并进入下一个字节。 (即 SWAR

例如:如果你有eax = 0x010101FF,加上上面的常量,你不会得到0x02020200,而是0x02020300(最低位溢出到次低位一个).


选项 2(没有进位传播的循环)

既然你指出你想使用一个循环来解决你的问题,一个可能的方法也只需要两个寄存器是这样的:

[global func]
func:
    mov rax, 0x4141414141414141

    mov rcx, 8
.func_loop:             ; NASM local .label is good style within a function
    inc al              ; modify low byte of RAX without affecting others
    rol rax, 8
    dec rcx
    jne .func_loop
    ; RAX has been rotated 8 times, back to its original layout

    ret

这将递增rax的最低有效字节(不影响rax的其他位),然后将rax向左旋转8位,并重复。

你可以旋转 16 位(4 次)然后做

inc ah           ; doing AH first happens to be better with Skylake's partial-register handling: inc al can run in parallel with this once AH is already renamed separately.
inc al
rol rax, 16

作为循环体,但是修改AH通常是 than just modifying AL, although it should reduce overhead on CPUs like Ryzen that don't rename AH separately from RAX. (Fun fact: on Skylake this breaks even for latency while inc al ; inc ah in that order is slower, because the inc ah can't start until after inc al, because 与full reg分开,只有high-8。)

请注意,loop 指令在 Intel CPU 上是 slow,并且在功能上等同于此(但不修改标志):

dec rcx
jne func_loop

另请注意,如 .

所述,在某些系统上执行 add al, 1 实际上可能比执行 inc al 稍微快一些

(编者注:rol除了1之外的计数只需要修改CF,而inc/dec只修改其他标志(SPAZO) . 因此,通过良好的部分标志重命名 inc / rol / dec 不会将 inc/rol 依赖链耦合到 dec 循环计数器依赖链中并使其变慢比它需要的。(在 Skylake 上测试,它实际上 运行 在 2 个周期/迭代吞吐量下用于大循环计数)。但是 dec 在 Silvermont 上会是一个问题,其中 inc /dec 合并到 FLAGS 中。使其中之一成为 subadd 将通过 FLAGS 打破依赖链。)


选项 3(不带进位传播的 SIMD 添加)

实现这种溢出行为的最有效方法可能是使用专用的 SSE2 SIMD 指令:

default rel        ; use RIP-relative addressing by default

section .rodata
align 16           ; without AVX, 16-byte memory operands must be aligned
vec1:  times 8 db 0x01
               dq 0

section .text
[global func]
func:
    mov    rax, 0x4141414141414141

    movq   xmm0, rax
    paddb  xmm0, [vec1]      ; packed-integer add of byte elements
    movq   rax, xmm0

    ret

这会将rax的值移动到xmm0的下半部分,对预定义常量(长128位,但高64位是与我们无关,因此为零),然后再次将结果写回 rax

输出符合预期:rax = 0x01010101010101FF 产生 0x0202020202020200(最低有效字节溢出)。

请注意,使用整数加法也可以使用内存中的常量,而不是 mov-立即数。

MMX 只允许使用一个 8 字节的内存操作数,但是在返回之前你需要 EMMS; x86-64 System V ABI 指定 FPU 应在 call/ret.

上处于 x87 模式

您可以使用一个技巧来代替从内存中加载常量,即动态生成常量。使用 pcmpeqd xmm1, xmm1 生成全一向量是有效的。但是如何使用它来添加 1? SIMD 右移仅适用于字(16 位)或更大的元素,因此需要几条指令才能将其转换为 0x0101... 的向量。 .

诀窍是加1和减-1是一样的,全1是补码-1.

    movq     xmm0, rax
    pcmpeqd  xmm1, xmm1        ; set1( -1 )
    psubb    xmm0, xmm1        ; packed-integer sub of (-1) byte elements
    movq     rax, xmm0

请注意,SSE2 也有 饱和 加和减的指令,paddsb or psubsb for signed saturation and paddusbpsubusb 用于无符号。 (对于无符号饱和度,您不能使用减法 -1 技巧;它总是会饱和到 0,而不是回到原始值之上的 1。)