【问题标题】:How to swap 2 bytes in x86 assembly correctly?如何正确交换 x86 程序集中的 2 个字节?
【发布时间】:2013-10-09 17:34:52
【问题描述】:

我有一个缓冲区,其中包含:'bac\n',我正在尝试交换字母 'b''a'

我检查了调试器,它在 ebp 指向的地址打印了 4 个字节:'bac\n'

  1. ebp - 缓冲区的地址
  2. eax - 偏移量(当前为 0),以便 ebp + eax 指向缓冲区中的 'b'
  3. ebx - 包含'b'
  4. edi - 包含'a'

问题是当我运行应该用'a'覆盖缓冲区中的'b'的指令时:

mov [ebp + eax], edi

...然后当我打印缓冲区时,它现在包含:'ac\n''b' 去哪儿了?如果我运行下一条应该用'b' 覆盖缓冲区中的'a' 的指令,则完成交换:

mov  [ebp + eax + 1], ebx

...那么缓冲区现在包含:'abac' 而不是 'abc\n'

谁能解释这里发生了什么?

【问题讨论】:

  • 您只需要移动一个字节,而不是整个寄存器(4 个字节)。

标签: assembly syntax x86 swap


【解决方案1】:

我认为你之前复制了整个 32 位寄存器,在你没有显示的部分

; copy 4 bytes from ebp + eax to ebp + eax + 3
mov ebx, [ebp + eax]     ; ebx = 'bac\n'
mov edi, [ebp + eax + 1] ; edi = 'ac\n<garbage>', which is ebp + eax + 1 to ebp + eax + 4

因此将字符移回内存后

mov [ebp + eax], edi     ; the string now becomes 'ac\n<garbage>'
mov [ebp + eax + 1], ebx ;                        'abac\n' (5 bytes)

这就是你所看到的。您只能复制一个字节,而不是一个双字。但是DI没有对应的字节寄存器名称,因此您应该将您的寄存器使用重新排列为CL / BL等低字节寄存器,例如

mov  bl, [ebp + eax]
xchg [ebp + eax + 1], bl ; simple, but not efficient
mov  [ebp + eax], bl

如果您没有可用的寄存器,您将不得不使用按位操作

如果你使用 x86_64,那么 DI 的低部分可以评估为DIL,但它会长一个字节

【讨论】:

  • 不要推荐带内存的 xchg 除非你提出警告,指出它比单独的加载+存储慢 很多 倍,因为隐含的 lock 前缀。请参阅this answer,了解带有/不带有暂存寄存器的一些与内存交换的可能性。 (另外,我认为您将 DI 与 SI 混为一谈)
  • @PeterCordes 我本来打算这么说的,但忘了。感谢 SI
  • 如果你想要简单,请使用rol word [mem], 8。此外,OP 描述了寄存器中已经有这两个值。只需更改您的寄存器分配,以便您在 regs 中都可以访问低字节。我添加了一个答案。
【解决方案2】:

OP 的关键问题是 EDI 只能作为 32 位值移动到内存中。正如 Luu 建议的那样,他需要一个可以作为 8 位值移动的寄存器;这将是 AH、AL、BH、...。 AL 最容易使用,它是 EAX 的“一部分”。

如果 OP 修改他的代码以便 EDI 包含偏移量,而 EAX 包含字符,那么按照 Luu 的建议会很容易。那么存储一个字节需要的指令是

mov byte ptr [ebp+edi],al

“byte ptr”告诉汇编器您希望移动 8 位而不是 32 位;这在技术上是不必要的,因为使用 AL 清楚地表明只使用 8 位,但它对读者有帮助。

【讨论】:

    【解决方案3】:

    您可以使用 16 位 8 位循环交换内存中的两个字节:

    rol  word ptr [ebp], 8     ;  byte [ebp] becomes byte [ebp+1], and vice verse
    

    但是如果您已经在寄存器中保存了字节(例如,因为您加载了它们以便可以比较它们),那么最好从寄存器中存储它们。

    由于您只需要存储寄存器的低字节,因此您需要使用albl 的字节存储,而不是edi 的双字存储! 更改寄存器分配,使字节位于 AL、BL、CL 或 DL 之一中,而不是 EDI 的低字节。只有 x86-64 可以访问 EDI 的低字节(作为 DIL)。使用 EDI 作为索引。 (然后名称 EDI 来自目标索引)。或者使用指针增量代替 base+index,以便 EDI 指向您可能想要交换的当前字节或字节对。

    因此:

         movzx    eax, byte ptr [edi + ebp]      ; load the 1st byte
         movzx    edx, byte ptr [edi + ebp + 1]  ; load the 2nd byte
         cmp      al, dl
         jae   noswap
    
         mov      [edi+ebp],   dl        ; opposite of how you loaded them
         mov      [edi+ebp+1], al
    noswap:
         inc      edi
    
         ... loop logic
    

    movzx 避免了对不将 AL 与整个 EAX 分开重命名的 CPU 上的 EAX 旧值的错误依赖。如果你完成了mov al, [edi + ebp],一些 CPU 会将 EAX 的旧值作为该指令的另一个输入依赖项。

    请注意,如果您实际上是在实施冒泡排序(eww,yuck),则每次迭代只需执行一次加载。您总是有两个值之一要在寄存器中进行比较。您可以为第一次迭代设置循环外的负载。


    如果您进行 16 位加载,然后只比较低字节(例如,作为排序的一部分),那么您完全可以交换 ebx 的低 2 字节并将其存储回来:

        movzx   eax,  word ptr [edi+ebp]
        cmp     ah, al       ; compare the low 2 bytes of EAX with each other
        jae    noswap
    
        rol     ax, 8        ; swap AL with AH.  This is more efficient than xchg al,ah or two MOV stores.
        mov     [edi+ebp], ax
    noswap:
    

    这本身很好,但是与下一个 16 位加载部分重叠的 16 位存储有点糟糕。 (商店转发摊位)。仅将新字节加载到 AH 中(将旧字节保留在 AL 中)也不是很好。在现代 Intel CPU 上读取 AH 时,这将暂停一个循环以合并,并创建一个依赖链。

    为什么我使用[edi+ebp] 而不是[ebp+edi]?它节省了一个字节以使 EBP 成为索引寄存器,因为没有位移的 [EBP + EDI*1] 是不可编码的。 NASM 和 YASM 不会为您交换,因为 base=EBP 意味着 SS 段。但假设是平坦的内存模型,没关系,所以我们可以手动进行优化。


    相关:

    对 32 位 int 元素的冒泡排序。 Bubble sort in x86 (masm32), the sort I wrote doesn't work

    8 位元素的 BubbleSort,19 个字节的 16 位 x86 机器代码:https://codegolf.stackexchange.com/questions/77836/sort-an-integer-list/149038#149038,以及 32 位元素的 JumpDown 排序。

    Assembly - bubble sort for sorting string(对字符串中的字符进行排序)

    Assembly bubble sort swap

    在了解 x86 部分寄存器的工作原理后,在 8 位和 32 位元素之间进行更改只需更改寄存器名称并将增量从 1 更改为 4。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-11-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-12-13
      相关资源
      最近更新 更多