扩展 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 和暂存寄存器的使用