【问题标题】:How do I shift left in assembly with ADD, not using SHL?如何使用 ADD 在汇编中左移,而不使用 SHL?
【发布时间】:2019-09-27 19:57:25
【问题描述】:

我们班上的老师给了我们一个作业。问题是我们没有在课堂上介绍位移,所以我对如何做到这一点有点迷茫。在作业说明中,他告诉我们以下内容:

  • 我们只能使用 mov, add , sub 指令来完成赋值。
  • 他给了我们以下提示:想想如何使用 add 指令来实现移位。例如,假设我们有以下 8 位二进制数:00000011,将这个二进制数左移后我们就有了。 00001100。

我们从 4 个变量开始:

  • var1 BYTE 41h
  • var2 BYTE 42h
  • var3 BYTE 43h
  • var4 BYTE 44h

任务的第一部分是移动它们,所以我们最终得到以下顺序:

  • var1 = 44 小时
  • var2 = 41h
  • var3 = 42 小时
  • var4 = 43h

我成功完成了这部分,我只是提供了这个作为上下文。下一部分是我遇到问题的地方。在第二部分中,我们需要将这些变量移动到寄存器 eax 中。 var1 必须存储在 eax 的最高字节,var2 存储在第二高字节,war3 存储在第二低字节,var4 存储在最低字节。

最终结果应该是 eax = 444144243

以下是我对这个问题的了解:

  • 我知道我不能直接引用 eax 的最高 16 位。
  • 我可以参考 ax 和 al.
  • 我知道我需要使用这些十六进制值的二进制值通过加 1 或类似的值将它们向左移动。

如何移动 var1 以使其与其他 var 一起位于 eax 的高 16 位,依此类推?

注意:我没有代码,因为我什至不知道从哪里开始这部分作业。我不想解决它,我只想帮助找到解决问题的正确路径。

【问题讨论】:

  • 想想二的幂。
  • 还要考虑以 10 为底的...如果将 1234 左移 1 位,那对应的是什么?
  • 我的问题是我什至不知道如何在装配中转移。课堂上没有讲到。 @小丑2340?
  • @Aeryes 正如 Jester 所说,尝试计算左移 1 的数字 1234 是什么以及它与原始数字的关系。然后尝试弄清楚如何使用允许使用的指令来实现位移。
  • 回答你的评论:通常你用shl左移,用shr右移逻辑右移或sar算术右移。但是,您不得使用任何这些说明。有一种方法可以只使用您被允许使用的说明。

标签: assembly x86 bit-shift


【解决方案1】:

要将值向左移动,您需要将相同的值添加到自身

例如,如果你有 0011

0011 + 0011 = 0110 (shift 1 left)
0110 + 0110 = 1100 (shift 1 left again)

为了解决您的问题,我将采用以下方法(快速方法)

MOV ah, var1  (move 44h to 0000h -> 4400h)
MOV al, var2  (move 41h to 4400h -> 4441h)
ADD eax, eax    (4441h + 4441h = 8882h)
ADD eax, eax    (8882h + 8882h = 11104h)
ADD eax, eax    (11104h + 11104h = 22208h)
ADD eax, eax    (22208h + 22208h = 44410h)
ADD eax, eax    
ADD eax, eax    
ADD eax, eax   
ADD eax, eax    (444100h)
ADD eax, eax  
ADD eax, eax  
ADD eax, eax 
ADD eax, eax    (4441000h)
ADD eax, eax   
ADD eax, eax   
ADD eax, eax   
ADD eax, eax    (44410000h)

现在到另一部分

MOV ah, var3  (move 42h to 44410000h -> 44414200h)
MOV al, var4  (move 43h to 44414200h -> 44414243h)

现在你的 eax 寄存器如下:4441 4243h

【讨论】:

  • 我收到错误消息,提示指令操作数必须与 mov ax, var1 和 mov ax, var2 的大小相同
  • @Aeryes:然后使用movzx eax, var1var1 的任何宽度进行零扩展加载。或者使用部分注册技巧,例如将完整注册归零然后加载到 AL 中。
  • 我只允许在这个作业中使用 mov、add 和 sub。我对你所说的几乎所有内容都是新手。什么是部分注册技巧?什么是清零完整的 reg?
  • 已修复,重试。我有点生疏,但我想我现在明白了(供参考:codeguru.com/images/article/9415/image1.jpg
  • 我认为(作为一名教师)这个练习的重点不是要进行轮班,而是要了解注册表的工作方式。我建议您查看我在之前的评论中附上的图片,并尝试理解代码以及为什么它不起作用,现在它起作用了。很高兴它成功了,干杯
【解决方案2】:

我知道我需要使用这些十六进制值的二进制值通过加 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,0sub 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 中。

【讨论】:

  • "(这就是我在mov al, [var4] 中不需要额外语法的原因)。"寄存器al 不是也暗示了字节大小吗?
  • @ecm:是的。这不是操作数大小不明确的问题(如 mov [edi], 123),而是 MASM 将大小与数据标签相关联,并且它必须匹配。这就是它接受mov al, [var4] 但不接受mov ax, [var4] 的原因。比较 NASM,它 毫无怨言地接受,寄存器暗示字节或字操作数大小,而内存操作数不暗示任何内容。
  • 后来我回答的重复,我不记得有重复:How to mov values in eax register, ah and al left by 2 bytes? x86 Assembly
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-17
  • 1970-01-01
  • 2011-10-14
相关资源
最近更新 更多