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

时间:2015-08-19 15:34:34

标签: c gcc assembly

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

例如,使用EBX时需要保留RBXCPUID,因为根据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(而不是CPUID' s {{1如果启用了PIC,它可能是指向全局偏移表(GOT)的指针。

确切地说,表达式指定EBX CPUID到[{1}}的商店应该在%EBX之后发生(1); (2)__EBX之后;但是(3)在PUSH %EBX之前?

2 个答案:

答案 0 :(得分:3)

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

要解决此问题,您可以让汇编程序模板为您选择一个寄存器。我们只需将%ebx与编译器选择的未使用的寄存器进行交换,而不是推送和弹出,然后通过交换它来恢复它。由于我们不希望编译器在交换期间破坏我们的输入寄存器,因此我们指定了早期的clobber修饰符,因此在OPs代码中以=&r(而不是=b的约束结束) 。有关修饰符的更多信息,请参见here。您的代码(对于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.h宏。它定义了宏,以便在需要时仅保存ebxrbx寄存器。您可以在此处找到GCC 4.8.1宏定义,以了解它们如何处理cpuid.h中的cpuid

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

答案 1 :(得分:2)

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

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

...可能会插入其他指令,所以如果这不合适,则必须重写如下:

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

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

对于cpuid代码段,我们遇到两个问题:

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

  2. 我们希望提取cpuid放置在ebx中的值,而不会返回带有“错误”ebx值的已编译代码。

  3. 一种可能的解决方案是:

    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的工作原理。)