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

Loops 如何增加寄存器中的每个字节?(64位,Linux,NASM),loops,assembly,x86-64,nasm,increment,Loops,Assembly,X86 64,Nasm,Increment,我最近开始学习组装,并为自己建立了一个小项目。目标是使用循环。我想将0x414141移动到RAX,然后在RAX上循环,并增加每个字节,以便RAX在代码末尾包含0x4242 我曾尝试递增字节rax,但在尝试编译它时,我总是从NASM中得到错误。目前,我有一个工作代码,最终将RAX增加到等于0x414144。我似乎找不到任何看起来/听起来与我想做的事情相近的东西。(但这有多难,对吧?) 当我在GDB中查看RAX时,在这段代码中,我希望它是0x414144,事实就是这样。但是,我想让我的代码达到0x4

我最近开始学习组装,并为自己建立了一个小项目。目标是使用循环。我想将0x414141移动到RAX,然后在RAX上循环,并增加每个字节,以便RAX在代码末尾包含0x4242

我曾尝试递增字节rax,但在尝试编译它时,我总是从NASM中得到错误。目前,我有一个工作代码,最终将RAX增加到等于0x414144。我似乎找不到任何看起来/听起来与我想做的事情相近的东西。(但这有多难,对吧?)


当我在GDB中查看RAX时,在这段代码中,我希望它是0x414144,事实就是这样。但是,我想让我的代码达到0x424242,我想这将是本项目的预期结果。

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


选项1(带进位传播的简单加法) 如果您只关心64位RAX的低位4字节,那么您可能只需要将EAX用于32位操作数大小。(与写入8位或16位寄存器不同,写入32位寄存器零会扩展到完整的64位寄存器。)

所以,正如在评论中提到的,这是对你的问题的一种解释

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

 mov   rdx, 0x0101010101010101
 add   rax, rdx

上面使用单个宽
add
的方法的缺点是某个字节中的溢出会传播到下一个更高的字节。所以它实际上不是4或8个独立的字节相加,除非您知道每个单独的字节不会溢出并进入下一个字节。(即)

例如:如果有
eax=0x010101FF
并从上面添加常量,则不会得到
0x020200
,而是
0x0200300
(最低有效字节溢出到第二个最低有效字节)


选项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次)并执行以下操作

作为循环体,但是修改AH通常不仅仅是修改AL,尽管它应该减少Ryzen等CPU的开销,Ryzen不会将AH与RAX分开重命名。(有趣的事实:在Skylake上,延迟是盈亏平衡的,
inc-al
inc-ah
按该顺序的速度较慢,因为
inc-ah
inc-al
之后才能启动,因为与完整的注册分开,只有高8。)

请注意,
循环
指令位于英特尔CPU上,功能等同于此(但不修改标志):

另外请注意,在某些系统上执行
addal,1
实际上可能比执行
inc al
稍快,如前所述

(编者按:
rol
的计数不是
1
只需修改CF,而
inc
/
dec
只需修改其他标志(SPAZO)因此,使用良好的部分标志重命名
inc
/
rol
/
dec
不会将inc/rol依赖链耦合到dec循环计数器依赖链中,并使其比需要的速度慢。(在Skylake上测试,事实上,对于大循环计数,它以2个周期/迭代吞吐量运行)但是在Silvermont,
dec
将是一个问题,
inc
/
dec
合并到标志中。将其中一个标志作为
子标志或
添加标志将打破依赖链。)


选项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
产生
0x02020200
(最低有效字节溢出)

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

MMX只允许使用8字节内存操作数,但在返回之前需要
emm
;x86-64 System V ABI指定FPU应在调用/重试时处于x87模式


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

诀窍在于,添加
1
与减去
-1
是一样的,所有的1都是2的补码
-1

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


请注意,SSE2还具有饱和加法和减法的指令,对于有符号饱和,使用或
psubsb
;对于无符号饱和,使用或
psubsb
。(对于无符号饱和,您不能使用减法
-1
技巧;这将始终饱和到0,而不是在原始值上返回到1。)

尝试将0x010101添加到RAX(无需循环)?@MichaelPetch-我想这取决于溢出的内容
dec rcx
jne func_loop
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
    movq     xmm0, rax
    pcmpeqd  xmm1, xmm1        ; set1( -1 )
    psubb    xmm0, xmm1        ; packed-integer sub of (-1) byte elements
    movq     rax, xmm0