我知道我需要使用这些十六进制值的二进制值通过加 1 或类似的值将它们向左移动。
十六进制是一种人类可读的打印二进制值的方式。您在寄存器和内存中的字节只是由位组成,因为计算机使用二进制逻辑。
由于您不能使用移位或旋转指令(除了使用 add same,same 左移 1 之外),我会考虑使用存储/重新加载以按所需顺序临时获取字节。
请注意,所需的结果仅以字节粒度重新排序数据,即围绕整个字节移动。字节之间没有位必须移动。因此您不一定需要位移位。
您的想法没有错,但需要大量说明。我只是介绍其他方式。
练习的重点似乎是教您如何将 AX、AH 和 AL 别名到 EAX 的各个部分。 (与 ECX、EDX 和 EBX 类似)。和/或 dword 加载/存储如何影响它所触及的 4 个单独的内存字节,即 little-endian 字节顺序。
如果预期的解决方案是使用add 16 次作为班次,我真的不明白禁止shl eax,8 或16 的意义。仍然存在使用部分寄存器来修改 EAX 字节的问题。我的意思是,使用 x+x = 2*x = x<<1 身份通常很有用,例如在使用 LEA 的寻址模式下进行数学运算,例如 x*3 = lea eax, [eax + eax*2]。或者使用add same,same 作为更高效的shl eax,1。
我成功完成了这部分,我只是提供了这个作为上下文。下一部分是我遇到问题的地方。在第二部分中,我们需要将这些变量移动到寄存器 EAX 中。 var1 必须存储在 eax 的最高字节 [...]
假设变量按所示顺序存储(var1 在前,因此在最低地址:
您指定的顺序与您从覆盖所有 4 个字节的 4 字节加载中获得的顺序相反:x86 是小端序,因此最低地址字节在EAX(又名 AL)的低字节。
通常你会做一个双字加载然后bswap eax 来颠倒寄存器中4个字节的顺序。 (或者在支持它的较新的 CPU 上,movbe load: mov big-endian data 通常至少与单独的 load + bswap 一样有效。)但是这个练习迫使你变得更有趣。
跨越多个字节变量的更广泛的加载/存储
假设您的 4 个变量是连续声明的,首先是 var1(在最低地址),您可以使用一个字或双字 mov 加载一次加载多个字节。例如
mov eax, dword ptr [var1] ; AL=var1, AH=var2, higher bytes = var3, var4
MASM 将大小与数据标签相关联,因此为了让它满意,我们必须将dword ptr 大小覆盖应用于内存操作数,以使其与寄存器 EAX 的双字大小匹配。 mov 始终要求两个操作数的大小相同,但对于像 mov [esp], eax 这样的情况,寄存器源操作数 EAX 暗示内存目标的大小。如果没有标签(或 2 个寄存器),就不可能出现不匹配。当大小自行匹配时,它也是可选的,例如 mov al, [var1]
就 CPU 而言,一切都只是字节; dword ptr 覆盖纯粹是源代码级别的东西,可以让汇编程序满意。 NASM 等其他汇编程序不需要它。只是因为寻址模式包含一个数据标签,MASM 才将其视为具有相关大小的“变量”。
鉴于您已经完成第 1 部分并将其存储到内存中,因此您在内存中按此顺序有 44h, 41h, 42h, 43h(即 43'42'41'44h 作为 little-endian dword)您可能会处理第 2 部分的内容像这样:
mov ax, word ptr [var1] ; AX = 41'44h = AH:AL
mov dx, word ptr [var1 + 2] ; DX = 43'42h = DH:DL
sub esp, 4 ; reserve stack space
mov [esp+0], DH
mov [esp+1], DL
mov [esp+2], AH ; 4x byte stores
mov [esp+3], AL ; constructing a byte-reversed copy
mov eax, [esp] ; reload that
add esp, 4 ; release the stack allocation
在我的 cmets 中,在编写 EAX 值的 32 位十六进制表示形式时,我用 ' 分隔字节,以便更容易查看字节边界与 44414243h
@Pablo 的回答提出的指令要少得多。它确实具有导致存储转发停止的性能缺点。 (EAX 的双字重载是刚刚由 4 个单独的字节存储写入的数据)。但是与 16 条 add 指令相比,它可能仍然具有更好的延迟和吞吐量!存储转发停顿“仅”大约 15 个周期的总存储->重新加载延迟,但 may block other store-forwarding during that time,或至少多个存储转发失败似乎无法同时发生。请参阅下面的另一段。
执行 4x 字节加载和两个 word 存储是一种选择,但这需要部分寄存器合并,并且在 Intel 上可能会更糟。请参阅下面的第 1 部分实现,在此答案的末尾附近。
高效地完成第一部分
如果您可以使用任何您想要的指令,您只需使用左旋转将高 8 位向下移动到开头,然后将其他位向左移动以腾出空间。
; MSB(var4) LSB(var1)
; dword at var1 = 44'43'42'41h
rol dword ptr [var1], 8 ; dword at var1 = 43'42'41'44h
或者您可以使用左移 1 个字节(8 位):
mov eax, dword ptr [var1] ; EAX = 44'43'42'41h
add eax, eax ; shift left by 8 bits, 1 at a time
add eax, eax
add eax, eax
add eax, eax
add eax, eax
add eax, eax
add eax, eax
add eax, eax ; EAX = 43'42'41'00h ; after shifting
mov al, [var4] ; EAX = 43'42'41'44h ; replace the low byte
mov dword ptr [var1], eax ; overwrite var1..4 with the bytes of EAX
写入 AL 然后读取 EAX 可能会导致某些旧 CPU 上的部分寄存器停顿,但我们不需要单独写入 AH,因此现代 CPU 不会出现任何性能问题。
如果我们可以做一个写在 4 个字节之外的 DWORD 存储,那就真的很方便了,即踩到一个假设的 var5。那我们就可以了
;; writes 1 byte past var4
mov eax, dword ptr [var1]
mov dl, [var4]
mov dword ptr [var2], eax ; overwrite var2..4 and 1 extra byte past the end
mov [var1], dl
我们可以使用堆栈上的一些临时存储来完成这项工作,然后复制回var1,但这有点笨拙:
sub esp, 8 ; reserve some stack space
mov eax, dword ptr [var1] ; EAX = 44'43'42'41h
mov [esp], eax
mov [esp+4], eax ; 2 copies of EAX so we can take any 4-byte window
mov eax, [esp+3] ; EAX = 43'42'41'44h = rotated
mov dword ptr [var1], eax ; and store that over var1..4
add esp, 8 ; and dealloc it.
这通过连接内存中字节序列的 2 个副本(在两个 dword 中)来模拟旋转。然后我们可以加载任何 4 字节的窗口。我们选择低字节来自原始值的高字节的那个,1字节(8位)左旋转= 3字节(24位)右旋转。
很遗憾,这将导致存储转发停止。 (两个字存储被重叠的负载读取)。所以大约 15 个周期的延迟,而不是现代 x86 上通常的 5 个。 (https://agner.org/optimize)。
它的吞吐量可能比 8x add 的版本更好,但延迟更差。或者对于吞吐量来说也可能更糟,因为add 的独立链可以重叠它们的执行。多个存储转发停顿can't be happening at once on Intel CPUs at least,因此如果您正在做很多此,这会限制吞吐量,而不仅仅是在很多其他工作之间。
仅加载字节:
在进行任何存储之前加载所有内容意味着我们不必担心破坏我们尚未读取的数据。
mov ah, [var1] ; EAX = ??'??'41'??h
mov al, [var4] ; EAX = ??'??'41'44h
mov ch, [var3] ; ECX = ??'??'43'??h
mov cl, [var2] ; ECX = ??'??'43'42h
mov word ptr [var3], cx ; var3 = CL = 42h var4 = CH = 43h
mov word ptr [var1], ax ; var1 = AL = 44h var2 = AH = 41h
另一个明显的选择是两个word 加载和4 个byte 存储。就性能而言,这将避免 Intel CPU 上的部分寄存器合并停顿(从写入 AL/AH 然后读取 AX),但负载吞吐量通常优于存储吞吐量。虽然 AH 合并惩罚可能比 Haswell/Skylake 上的惩罚更糟
但由于我们希望保持 2 个源字节有序,我们可以将 CL 和 CH 负载合并为一个 CX 负载:
也许是第 1 部分最有效的版本
mov ah, [var1] ; EAX = ??'??'41'??h
mov al, [var4] ; EAX = ??'??'41'44h
mov cx, word ptr [var2] ; ECX = ??'??'43'42h
mov word ptr [var3], cx ; var3 = CL = 42h var4 = CH = 43h
mov word ptr [var1], ax ; var1 = AL = 44h var2 = AH = 41h
请注意,我们正在从var2 进行字加载,然后将字存储到var3,将这 2 个字节向左移动 8 位/1 个字节。
这里唯一的停顿是在写入 AL+AH 之后读取 AX,这在 Intel Sandybridge 系列上最多是前端的一个循环(在此期间,合并 uop 自行问题)。
如果稍后的代码要对我们重新排列的 4 个字节进行 dword 加载,那么将其写入 2 个半字会导致存储转发停止。但是我上面的 bswap 仿真只重新加载了两个 word 一半,所以没关系。
这也对 EAX 和 ECX 的旧值具有错误的依赖性(对于 ECX,您可以通过执行 dword 加载来避免这种依赖性,但仍然是字存储)。或者对于 EAX,mov eax,0 或 sub eax,eax 会将其归零并(在大多数 CPU 上)打破对旧值的依赖关系,允许无序执行此指令序列,即使最后一个块使用 ECX 用于不同价值仍然停滞不前。
与 8x add 版本或在堆栈上使用暂存空间的版本相比,这更少的指令(并且每条指令仍然相当有效)。并且不会临时存储/重新加载。
结合第 1 部分和第 2 部分:
最终在 EAX 中得到所需数据并在内存中进行字节反转可以合并为一个步骤,也许可以重用堆栈上的临时数据。
当然,如果你没有受到指令选择的限制,你可以做的非常有效:
mov eax, dword ptr [var1]
ror eax, 8
mov dword ptr [var1], eax
bswap eax
仅优化第 1 部分和第 2 部分以及 mov 和 add/sub 留给读者作为练习。
其他有趣的想法:XOR 交换也适用于 SUB,因此您可以在寄存器和内存之间低效地交换字节。但是您不限于使用 tmp regs,因此最好加载到单独的字节 reg 中。