【问题标题】:How to implement memmove, not just memcpy, in assembly?如何在汇编中实现 memmove,而不仅仅是 memcpy?
【发布时间】:2021-07-16 04:52:37
【问题描述】:

我试图将 memmove 从 C 实现到 x86 程序集,所以我写道:

start:
    movb source(%eax), %cl
    movb %cl, destination(%eax)
    inc %eax
    cmp num, %eax
    jne start

end:

但这是错误的,为什么?根据:http://www.cplusplus.com/reference/cstring/memmove/

将 num 个字节的值从 source 指向的位置复制到 目标指向的内存块。 复制就像 使用了中间缓冲区允许目的地和 来源重叠。

我的代码不支持。

如何在不使用堆栈的情况下解决此问题? 注意:我们可以假设在源目标进入内存之后,num(要复制的字节数)很远,不能被错误触及。

【问题讨论】:

  • 好吧,既然你特别说你可以假设目的地在源之后,你可以简单地向后复制。 PS:一开始还不清楚你的轮换是为了什么。
  • 当它们重叠时,您可以向后(从末尾开始并递减)或向前复制,具体取决于它们重叠的方式。
  • stackoverflow.com/questions/19606399/… 有一个 C 中的示例实现,您可以尝试调整它。如果您的系统有enhanced rep movsb,那么您可以使用它,使用direction flag 来控制前进与后退。
  • @NateEldredge: rep movsb 在 DF=1 时非常慢,包括在带有 ERMSB 的 CPU 上。当然,您仍然可以使用它来确保正确性,尽管它可能是这个天真的 byte-at-a-time 循环的一半速度(在 Skylake 上每个周期约 2 个字节),每次迭代都从内存中重新加载 num 并加载通过将一个新字节合并到 ECX 的底部(而不是使用 movzbl)。 (cmp num, %eax/jcc 至少在 Haswell/Skylake 上,IIRC 是否仍将微融合和宏融合到 1-uop cmp/jcc 中,因为它使用的是绝对寻址模式)。
  • @CraigEstey:对于某些硬件上的硬件预取器而言,复制转发可能会更有效,因此您通常希望在 C 规则允许时复制转发。在开始做有用的工作之前,您需要更少的条件分支。当然,如果有足够多的程序滥用memcpy 并依赖它像memmove 一样工作,那么让memcpy 做他们实际上并不想要的事情,仅仅因为语言标准允许这样做并不是一个理想的情况。对“错误”更加宽容可能会降低性能。 (不过,如果没有单独的 memcpy,好处可能是代码占用空间更小。)

标签: c assembly x86 att memmove


【解决方案1】:

这里的问题是关于destination - source < size 的潜在重叠(即sourcedestination 都指向同一个数据块)。发生这种情况时,您的情况如下:

AAAAAAAAAAAAAABBBBBBBBBBBBBB_______________________
^             ^             ^             ^
source        destination   source        destination
                             + num        + num

如果您从source 开始复制,您将覆盖您尝试复制的部分内容(在此示例中,您将用As 覆盖Bs),丢失原始值,最终得到像这样:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_________
^             ^             ^             ^
source        destination   source        destination
                             + num        + num

当你实际上想要这个时:

AAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBBBBBBBB_________
^             ^             ^             ^
source        destination   source        destination
                             + num        + num

您可以通过检查何时destination - source < num 来解决此问题,并在这种情况下反向复制(以eax = num 开头)。

相应的程序集是这样的:

    mov  $destination, %eax
    sub  $source, %eax             # dst-src distance in bytes
    cmp  num, %eax
    jb  backwards                  # if (dst-src < copy_size) goto backwards

forward:
    mov  $0, %eax
forward_loop:
    movb source(%eax), %cl
    movb %cl, destination(%eax)
    inc %eax
    cmp num, %eax
    jne forward_loop
    jmp end

backwards:
    movl  num, %eax        # start from i=length
backwards_loop:
    movb  source-1(%eax), %cl
    movb  %cl, destination-1(%eax)
    dec   %eax
    jnz   backwards_loop

end:

【讨论】:

  • backwards 中,我想你想要dec %eax / jnz,没有cmp。或者 jge 如果您需要在 EAX 变为 0 后运行循环体。 (但实际上我认为你需要dec before 复制,因为 array+size 是结束后的一个。或者更好的是,在循环之前一次,这样你就可以使用宏融合 dec / jge 作为循环分支。或source-1(%eax) / destination-1(%eax))
  • test %eax,%eax before dec 没用:jge 不读取 CF,dec 写入其他 FLAGS。我覆盖了这个更改,因为我已经在修复你的 AT&T 语法和评论事情了。 (例如,mov 0, %eax 是从绝对地址 0 加载的,不等于 xor %eax,%eax
  • @PeterCordes 感谢您的编辑和评论。我来自移动设备,因此正确格式化代码已经是一项成就。我们和我一起编辑保存以查看结果并相互冲突。不知道你能做到source-1(%eax),不是那么令人赏心悦目,但是如果它有效的话,那就好。你可能会猜到我不习惯 AT&T :')
  • @lion:不,我没有忘记,我特意从 Marco 的回答中删除了 cmp,因为 dec 已经设置了 ZF。请参阅我的第一条评论。 dec reg / jnz 是将计数器循环到零的惯用方式。 What am I comparing to? No CMP instruction in assembly code before JNE
  • @lion 我已经添加了关于为什么需要反向复制的更详尽的解释,希望对您有所帮助。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-05-04
  • 2011-05-23
  • 1970-01-01
  • 2020-05-12
  • 2017-03-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多