扩展 GCC asm 中多个输入和输出操作数的正确用法是啥?

Posted

技术标签:

【中文标题】扩展 GCC asm 中多个输入和输出操作数的正确用法是啥?【英文标题】:What is the correct use of multiple input and output operands in extended GCC asm?扩展 GCC asm 中多个输入和输出操作数的正确用法是什么? 【发布时间】:2016-06-30 09:07:07 【问题描述】:

在寄存器约束下,扩展 GCC asm 中多个输入和输出操作数的正确用法是什么?考虑一下我的问题的这个最小版本。以下是 GCC、AT&T 语法中的简短扩展 asm 代码:​​

    int input0 = 10;
    int input1 = 15;
    int output0 = 0;
    int output1 = 1;

    asm volatile("mov %[input0], %[output0]\t\n"
                 "mov %[input1], %[output1]\t\n"
                 : [output0] "=r" (output0), [output1] "=r" (output1)
                 : [input0] "r" (input0), [input1] "r" (input1)
                 :);

    printf("output0: %d\n", output0);
    printf("output1: %d\n", output1);

根据https://gcc.gnu.org/onlinedocs/gcc/Extended-Asm.html,语法看起来是正确的但是,我一定是忽略了一些东西,或者犯了一些我由于某种原因看不到的小错误。

GCC 5.3.0 p1.0(无编译器参数)的输出是:

输出0:10 输出1:10

预期输出是:

输出0:10 输出1:15

在 GDB 中查看显示:

0x0000000000400581 : mov eax,DWORD PTR [rbp-0x10] 0x0000000000400584 : mov edx,DWORD PTR [rbp-0xc] 0x0000000000400587 : mov edx,eax 0x0000000000400589 : mov eax,edx 0x000000000040058b : mov DWORD PTR [rbp-0x8],edx 0x000000000040058e : mov DWORD PTR [rbp-0x4],eax

据我所见,它用 input0 加载 eax,用 input1 加载 edx。然后它用 eax 覆盖 edx,用 edx 覆盖 eax,使它们相等。然后它将这些写回 output0 和 output1。

如果我对输出使用内存约束 (=m) 而不是寄存器约束 (=r),它会给出预期的输出,并且程序集看起来更合理。

【问题讨论】:

你可能想看看early clobbers & modifier。特别是我觉得你需要 =&r 作为你的 output0 操作数,因为该寄存器在你的汇编器模板的最后一条指令之前被修改。 GCC 会认为它也可以重用该寄存器作为输入。 & 将防止早期的clobber分配寄存器被用作输入寄存器 您可能还想考虑在输入操作数上使用g 约束而不是r。由于输出仅定义为寄存器,并且模板中的mov 指令可以占用至少一个内存或立即值操作数,因此您可以让编译器有机会使用g 执行其他优化。 g 约束记录为允许任何寄存器、内存或立即整数操作数,除了不是通用寄存器的寄存器 特别是如果您使用g 作为输入操作数约束,编译器应该能够意识到一些输入实际上是常量(立即)值,这应该允许一些代码减少。如果您使用 GCC 使用 -O3 的优化级别进行编译,您可以更好地看到这些优化 @MichaelPetch 好吧,如果您想完全枚举允许的操作数并为编译器提供最大的灵活性,您将使用"=r,r,rm,rm", "=r,rm,r,rm" : "g,g,ri,ri", "g,ri,g,ri" 【参考方案1】:

问题在于 GCC 假设所有输出操作数都只写在指令的末尾,在所有输入操作数都被消耗完之后。这意味着它可以使用相同的操作数(例如寄存器)作为输入操作数和输出操作数,这就是这里发生的情况。解决方案是用early clobber constraint 标记[output0],以便GCC 知道它在asm 语句结束之前写入。

例如:

 asm volatile("mov %[input0], %[output0]\t\n"
              "mov %[input1], %[output1]\t\n"
              : [output0] "=&r" (output0), [output1] "=r" (output1)
              : [input0] "r" (input0), [input1] "r" (input1)
              :);

您无需将[output1] 标记为早期破坏者,因为它仅在指令末尾写入,因此它使用与[input0][input1] 相同的寄存器无关紧要。

【讨论】:

除了罗斯的回答之外,还有我最喜欢的派对技巧:asm ("" : "=r" (output0), "=r" (output1) : "0" (input0), "1" (input1));。没错,不需要汇编就可以将输入“移动”到输出。好吧,我不会被邀请参加很多派对...... @AttributedTensorField 和 Ross:这不需要是 volatile。它没有副作用,只是一个纯“函数”,其输出值仅取决于输入。 @PeterCordes 它也不需要是 asm 语句。 @RossRidge:我以为 OP 计划在他将这个实验作为构建块/垫脚石之后,用真实的东西替换 mov 指令。我喜欢@David Wohlferd 的评论,因为 SO 问题中的代码通常会错误地将 mov 指令硬编码为内联 asm,而不是让编译器为它们做这件事。 @PeterCordes 我也是。这就是为什么我认为没有必要提出改进建议,无论是删除 volatile 关键字、完全枚举约束、消除 MOV 指令,还是对它们进行最好的改进全部用纯 C 重写。

以上是关于扩展 GCC asm 中多个输入和输出操作数的正确用法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

GCC 扩展 Asm - 了解 clobbers 和暂存寄存器的使用

如何在没有扩展内联 asm 的情况下在 gcc 内联汇编中声明和初始化局部变量?

gcc asm

翻译 | “扩展asm”——用C表示操作数的汇编程序指令

ASM 约束副作用

您如何在运行时使用 GCC 和内联 asm 检测 CPU 架构类型?