什么确保操作数的读/写在扩展ASM的期望时间发生?

根据GCC的扩展ASM和汇编程序模板 ,为了使指令连续,它们必须位于同一个ASM块中。 我无法理解是什么提供了对具有多个语句的块中的操作数进行读写的调度或时序。

例如,在使用CPUID时需要保留EBXRBX ,因为根据ABI,调用者拥有它。 关于EBXRBX的使用存在一些悬而未决的问题,因此我们希望无条件地保留它(这是一项要求)。 因此需要将三个指令编码到单个ASM块中以确保指令的连续性(re:第一段中讨论的汇编器模板):

 unsigned int __FUNC = 1, __SUBFUNC = 0; unsigned int __EAX, __EBX, __ECX, __EDX; __asm__ __volatile__ ( "push %ebx;" "cpuid;" "pop %ebx" : "=a"(__EAX), "=b"(__EBX), "=c"(__ECX), "=d"(__EDX) : "a"(__FUNC), "c"(__SUBFUNC) ); 

如果表示操作数的表达式在错误的时间点被解释,则__EBX将是保存的EBX (而不是CPUIDEBX ),如果启用了PIC,它可能是指向全局偏移表(GOT)的指针。

确切地说,表达式指定CPUID%EBX__EBX应该在PUSH %EBX之后发生(1); (2) CPUID ; 但(3) POP %EBX之前?

在你的问题中,你提出了一些pushpop ebx 。 使用-fPIC (位置无关代码)使用gcc编译时保存ebx的想法是正确的。 在这种情况下返回时,我们的function是不要破坏ebx 。 不幸的是,你定义了明确使用ebx的约束的方式。 通常,如果您使用PIC代码并且指定=b作为输出约束,编译器将警告您( 错误:’asm’中的操作数约束不一致 )。 为什么它不会给你发出警告是不寻常的。

要解决此问题,您可以让汇编程序模板为您选择一个寄存器。 我们只需将%ebx与编译器选择的未使用的寄存器交换,而不是推送和弹出,然后通过交换后将其恢复。 由于我们不希望编译器在交换期间破坏我们的输入寄存器,因此我们指定了早期的clobber修饰符,因此最终得到了约=&r (而不是OP代码中的=b )。 有关修饰符的更多信息,请点击此处 。 你的代码(32位)看起来像:

 unsigned int __FUNC = 1, __SUBFUNC = 0; unsigned int __EAX, __EBX, __ECX, __EDX; __asm__ __volatile__ ( "xchgl\t%%ebx, %k1\n\t" \ "cpuid\n\t" \ "xchgl\t%%ebx, %k1\n\t" : "=a"(__EAX), "=&r"(__EBX), "=c"(__ECX), "=d"(__EDX) : "a"(__FUNC), "c"(__SUBFUNC)); 

如果您打算编译X86_64(64位),则需要保存%rbx的全部内容。 上面的代码不太适用。 你必须使用类似的东西:

 uint32_t __FUNC = 1, __SUBFUNC = 0; uint32_t __EAX, __ECX, __EDX; uint64_t __BX; /* Big enough to hold a 64 bit value */ __asm__ __volatile__ ( "xchgq\t%%rbx, %q1\n\t" \ "cpuid\n\t" \ "xchgq\t%%rbx, %q1\n\t" : "=a"(__EAX), "=&r"(__BX), "=c"(__ECX), "=d"(__EDX) : "a"(__FUNC), "c"(__SUBFUNC)); 

您可以使用条件编译来处理X86_64和i386:

 uint32_t __FUNC = 1, __SUBFUNC = 0; uint32_t __EAX, __ECX, __EDX; uint64_t __BX; /* Big enough to hold a 64 bit value */ #if defined(__i386__) __asm__ __volatile__ ( "xchgl\t%%ebx, %k1\n\t" \ "cpuid\n\t" \ "xchgl\t%%ebx, %k1\n\t" : "=a"(__EAX), "=&r"(__BX), "=c"(__ECX), "=d"(__EDX) : "a"(__FUNC), "c"(__SUBFUNC)); #elif defined(__x86_64__) __asm__ __volatile__ ( "xchgq\t%%rbx, %q1\n\t" \ "cpuid\n\t" \ "xchgq\t%%rbx, %q1\n\t" : "=a"(__EAX), "=&r"(__BX), "=c"(__ECX), "=d"(__EDX) : "a"(__FUNC), "c"(__SUBFUNC)); #else #error "Unknown architecture." #endif 

GCC在__cpuid定义了一个__cpuid宏。 它定义了宏,以便在需要时仅保存ebxrbx寄存器。 您可以在此处找到GCC 4.8.1宏定义,以了解它们如何在cpuid.h中处理cpuid

精明的读者可能会问这个问题 – 是什么阻止编译器选择ebxrbx作为用于交换的临时寄存器。 编译器在PIC的上下文中了解ebxrbx ,并且不允许它用作临时寄存器。 这是基于我多年来的个人观察以及审查从C代码生成的汇编程序(.s)文件。 我不能肯定地说更古老版本的gcc如何处理它所以它可能是一个问题。

我想你理解,但要明确,“连续”规则意味着:

 asm ("a"); asm ("b"); asm ("c"); 

…可能会插入其他指令,所以如果不可取,​​则必须重写如下:

 asm ("a\n" "b\n" "c"); 

……现在它将作为一个整体插入。


至于cpuid片段,我们有两个问题:

  1. cpuid指令将覆盖ebx ,因此破坏了PIC代码必须保留的数据。

  2. 我们想要提取cpuidebx放置的值,而不会返回具有“错误” ebx值的已编译代码。

一种可能的解决方案是:

 unsigned int __FUNC = 1, __SUBFUNC = 0; unsigned int __EAX, __EBX, __ECX, __EDX; __asm__ __volatile__ ( "push %ebx;" "cpuid;" "mov %ebx, %ecx" "pop %ebx" : "=c"(__EBX) : "a"(__FUNC), "c"(__SUBFUNC) : "eax", "edx" ); __asm__ __volatile__ ( "push %ebx;" "cpuid;" "pop %ebx" : "=a"(__EAX), "=c"(__ECX), "=d"(__EDX) : "a"(__FUNC), "c"(__SUBFUNC) ); 

没有必要将ebx标记为破坏,因为你要将它放回原处。

(我没有做太多的英特尔编程,所以我可能会有一些汇编程序特定的细节,但这就是asm工作原理。)