【问题标题】:Copy a byte to another register in GNU C inline asm, where the compiler chooses registers for both operands将一个字节复制到 GNU C 内联汇编中的另一个寄存器,编译器在其中为两个操作数选择寄存器
【发布时间】:2020-06-02 01:36:58
【问题描述】:

我正在尝试处理 c 的内联 asm 中的字符串。我能够理解 strcpy 的工作原理(如下所示):

static inline char *strcpy(char *dest, char *src)
{
  int d0, d1, d2;
  char temp;

  asm volatile(
    "loop:  lodsb;"   /* load value pointed to by %si into %al, increment %si */
    "       stosb;"   /* move %al to address pointed to by %di, increment %di */
    "       testb %%al, %%al;"
    "       jne loop;"
    : "=&S" (d0), "=&D" (d1), "=&a" (d2)
    : "0" (src), "1" (dest)
    : "memory"
  );
}

我正在尝试使用此结构来制作它,以便在返回字符串之前修改字符串的各个字符。结果,我正在尝试看起来像这样的东西:

static inline char *strcpy(char *dest, char *src)
{
  int d0, d1, d2;
  char temp;

  asm volatile(
    "loop:  lodsb;"   /* load value pointed to by %si into %al, increment %si */
    "       mov %2, %3;" /* move al into temp */
    /*
     *
     * Do and comparisons and jumps based off how I want to change the characters
     *
     */
    "       stosb;"   /* move %al to address pointed to by %di, increment %di */
    "       testb %%al, %%al;"
    "       jne loop;"
    : "=&S" (d0), "=&D" (d1), "=&a" (d2), "+r" (temp)
    : "0" (src), "1" (dest)
    : "memory"
  );
}

我基本上是将lodsb 指令放入%al 的字节移动到一个临时变量中,之后我将在其中进行任何处理。但是,由于某种我无法弄清楚的原因,该字符似乎从未实际存储在 temp 中。

【问题讨论】:

  • 顺便说一句,strcpy 的第一个版本看起来安全且正确,很好。您可以使用 "+S"(src) 进行简化,因为您位于包装函数内部(因此可以修改此函数的本地 src),但具有匹配约束的虚拟输出是在您想要销毁的寄存器中获取输入的规范方式。当然,在循环中使用 lodsb/stosb 并不是特别有效,尤其是在不将 AL 与 RAX 分开重命名的 CPU 上,因此每个负载也需要一个 ALU uop 来合并到 RAX。但是一次一个字节比你用 SSE2 做的要糟糕得多,所以让我们忽略性能。

标签: c gcc assembly x86 inline-assembly


【解决方案1】:

您的第二个版本甚至无法组装,因为 tempd2 的大小不同,因此您最终会得到来自 GCC 的 mov %eax, %dlhttps://godbolt.org/z/tng4g4当内联 asm 不能满足您的要求时,请始终查看编译器生成的 asm,以了解编译器实际替换到您的模板中的内容(以及它为哪个操作数选择了哪些寄存器)。

这与您所描述的不符(运行但不起作用),因此它不是您所做的 MCVE。但真正的问题仍然可以回答。

一种简单的方法是将两个 C 临时文件声明为相同大小,以便 GCC 选择相同宽度的寄存器。

或者您可以使用mov %k2, %k3 之类的大小覆盖来获取movlmov %b2, %b3 以获取movb(8 位操作数大小)。

奇怪的是,您为 "=a" 临时选择了 int,因此编译器选择了 EAX,即使您只加载了 char

我实际上建议movzbl %2b, %3k 使用与您声明变量的方式相反的大小;这比将一个字节合并到目标的低字节更有效,并且避免在 P6 系列、早期 Sandybridge 系列和 CPU 上引入(或添加更多)部分寄存器问题进行任何部分寄存器重命名。另外,Intel 因为 Ivybridge 可以对其进行 mov-elimination。


顺便说一句,您的第一个版本的 strcpy 看起来安全且正确,很好。是的,"memory" clobber 是必要的。

Err,至少内联汇编是正确的。 但是,如果没有 return 语句,您会从非 void 函数的末尾掉出 C 未定义的行为。

您可以使用"+&S"(src) 读/写操作数而不是虚拟输出来简化 asm 操作数,因为您位于包装函数中(因此可以修改此函数的本地 src)。不过,具有匹配约束的虚拟输出是在要销毁的寄存器中获取输入的规范方式。

(如果你想像ISO C's poorly-designed strcpy 一样工作,你需要char *retval = dst 在asm 语句之前,如果你要使用"+S""+D" 操作数的上述建议。一个更好的主意将其称为stpcpy 并返回一个指向目的地end 的指针。另外,您的src 应该是const char*。)

当然,在循环中使用 lodsb/stosb 并不是特别有效,尤其是在不将 AL 与 RAX 分开重命名的 CPU 上,因此每个负载还需要一个 ALU uop 来合并到 RAX。但是一次一个字节比你用 SSE2 做的要糟糕得多,所以用movzx 负载优化它,也许索引寻址模式可能不值得麻烦。请参阅https://agner.org/optimize/https://stackoverflow.com/tags/x86/info 中的其他优化链接,尤其是https://uops.info/ 的指令延迟/吞吐量/微指令计数。 (stosb 是 3 微秒,而 mov 商店 + inc edi 总共是 2 微秒。)

如果您实际上是在优化代码大小而不是速度,只需使用 8 位或 32 位 mov 复制寄存器,而不是 movzbl


顺便说一句,对于这么多操作数,您可能希望在约束中使用命名操作数,例如 [src] "+&S"(src),然后在模板中使用 %[src]

【讨论】:

  • 嘿,所以我尝试使用大小覆盖来调整我的寄存器,使它们都具有与 mov %b2, %3; 相同的值,并且我还将 %eax 的输出类型更改为 char 而不是 int "=&a" (ret) 并且它似乎已经正确修改了寄存器大小(编译器生成的 asm 中的指令看起来像 mov %al, %cl)。因此,根据我对您的解释的理解,数据和寄存器大小应该没有问题。但是,我仍然无法使用任意十六进制值 (cmp 0x70, %3) 对该寄存器执行比较指令。
  • @Tom: 0x70 是一个绝对地址为0x70 的内存操作数。你想要像cmp $0x70, %%al 这样的东西。 (在分支之前不需要复制,您可能只需要阅读它;如果您需要第二份副本,只需在 if 正文中打扰它。您知道 lodsb 写了 AL,并且您知道您使用了一个虚拟输出来告诉专门关于 EAX 的编译器,因此您不妨编写外观正常的 asm,它显式使用该寄存器名称而不是操作数编号。顺便说一句,如果您不关心读取值,%eax 上的 clobber 也可以工作在 asm 语句之外。)
猜你喜欢
  • 1970-01-01
  • 2015-09-25
  • 1970-01-01
  • 2014-04-06
  • 2022-01-02
  • 2010-12-02
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多