理解内联asm的关键是要明白每个asm语句有两部分:
-
实际汇编程序的文本,编译器将在其中进行文本替换,但不理解。
这是文档中的AssemblerTemplate(直到__asm__() 中的第一个:)。
-
用编译器确实理解的术语来描述汇编器的作用。
这是the documentation中的: OutputOperands : InputOperands : Clobbers。
这必须告诉编译器汇编器如何适应编译器围绕它生成的所有代码。代码生成忙于分配寄存器来保存值、决定执行顺序、将内容移出循环、消除未使用的代码片段、丢弃不再需要的值等等。
实际的汇编器是一个黑匣子,它接受此处描述的输入,产生所描述的输出,并且作为副作用可能会“破坏”一些寄存器和/或内存。这必须是对汇编器所做工作的完整描述...否则编译器围绕您的模板生成的 asm 将与其发生冲突并依赖错误的假设。
有了这些信息,编译器可以决定汇编器可以使用哪些寄存器,你应该让它这样做。
那么,你的片段:
asm volatile(
"movq %0 , %%rax\n"
"rol %%rax\n"
"rol %%rax\n"
:"=r"(X)
:"r"(X)
);
有一些“问题”:
除此之外,一切都很棒。以下是安全的,并且避免了mov 指令:
asm("rol %0\n"
"rol %0\n" : "+r"(X));
其中"+r"(X) 表示需要一个组合输入和输出寄存器,取X 的旧值并返回一个新值。
现在,如果您不想替换X,那么假设Y 是结果,您可以:
asm("mov %1, %0\n"
"rol %0\n"
"rol %0\n" : "=r"(Y) : "r"(X));
但是最好让编译器来决定它是否需要mov 或者它是否可以让输入被销毁。
关于InputOperands有几条规则值得一提:
汇编器不得覆盖任何 InputOperands -- 编译器正在跟踪它在哪些寄存器中具有哪些值,并且期待 InputOperands 被保留。
编译器希望在写入 any OutputOperand 之前读取所有 InputOperands。当编译器知道给定的 InputOperand 在asm() 之后不再使用时,这一点很重要,因此它可以将 InputOperand 的寄存器分配给 输出操作数。有一种叫做earlyclobber (=&r(foo)) 的东西来处理这个小皱纹。
在上面,如果您实际上没有再次使用X,编译器可以将%0 和%1 分配到同一个寄存器!但是(冗余的)mov 仍然会被汇编——记住编译器真的不理解 AssemblerTemplate。所以,一般来说,你最好在 C 中改组值,而不是 asm()。见https://gcc.gnu.org/wiki/DontUseInlineAsm和Best practices for circular shift (rotate) operations in C++
以下是一个主题的四种变体,以及生成的代码 (gcc -O2):
// (1) uses both X and Y in the printf() -- does mov %1, %0 in asm()
void Never_Inline footle(void) Dump of assembler code for function footle:
{ mov $0x492782,%edi # address of format string
unsigned long X, Y ; xor %eax,%eax
mov $0x63,%esi # X = 99
X = 99 ; rol %rsi # 1st asm
__asm__("\t rol %0\n" rol %rsi
"\t rol %0\n" : "+r"(X) mov %rsi,%rdx # 2nd asm, compiler using it as a copy-and-rotate
) ; rol %rdx
rol %rdx
__asm__("\t mov %1, %0\n" jmpq 0x4010a0 <printf@plt> # tailcall printf
"\t rol %0\n"
"\t rol %0\n" : "=r"(Y) : "r"(X)
) ;
printf("%lx %lx\n", X, Y) ;
}
// (2) uses both X and Y in the printf() -- does Y = X in 'C'
void Never_Inline footle(void) Dump of assembler code for function footle:
{ mov $0x492782,%edi
unsigned long X, Y ; xor %eax,%eax
mov $0x63,%esi
X = 99 ; rol %rsi # 1st asm
__asm__("\t rol %0\n" rol %rsi
"\t rol %0\n" : "+r"(X) mov %rsi,%rdx # compiler-generated mov
) ; rol %rdx # 2nd asm
rol %rdx
Y = X ; jmpq 0x4010a0 <printf@plt>
__asm__("\t rol %0\n"
"\t rol %0\n" : "+r"(Y)
) ;
printf("%lx %lx\n", X, Y) ;
}
// (3) uses only Y in the printf() -- does mov %1, %0 in asm()
void Never_Inline footle(void) Dump of assembler code for function footle:
{ mov $0x492782,%edi
unsigned long X, Y ; xor %eax,%eax
mov $0x63,%esi
X = 99 ; rol %rsi
__asm__("\t rol %0\n" rol %rsi
"\t rol %0\n" : "+r"(X) mov %rsi,%rsi # redundant instruction because of mov in the asm template
) ; rol %rsi
rol %rsi
__asm__("\t mov %1, %0\n" jmpq 0x4010a0 <printf@plt>
"\t rol %0\n"
"\t rol %0\n" : "=r"(Y) : "r"(X)
) ;
printf("%lx\n", Y) ;
}
// (4) uses only Y in the printf() -- does Y = X in 'C'
void Never_Inline footle(void) Dump of assembler code for function footle:
{ mov $0x492782,%edi
unsigned long X, Y ; xor %eax,%eax
mov $0x63,%esi
X = 99 ; rol %rsi
__asm__("\t rol %0\n" rol %rsi
"\t rol %0\n" : "+r"(X) rol %rsi # no wasted mov, compiler picked %0=%1=%rsi
) ; rol %rsi
jmpq 0x4010a0 <printf@plt>
Y = X ;
__asm__("\t rol %0\n"
"\t rol %0\n" : "+r"(Y)
) ;
printf("%lx\n", Y) ;
}
希望它展示了编译器忙于向寄存器分配值、跟踪它需要保留哪些值、最小化寄存器/寄存器移动,并且通常很聪明。
所以诀窍是使用编译器,了解 :OutputOperands:InputOperands:Clobbers 是你在哪里描述汇编器在做什么。