生成的代码与扩展 ASM 的预期不匹配

Generated code not matching expectations with Extended ASM

我有一个CpuFeaturesclass。 class 的要求很简单:(1) 保留 EBXRBX,以及 (2) 将 CPUID 返回的值记录在 EAX/EBX/ECX/EDX 中。我不确定生成的代码是否是我想要的代码。

CpuFeatures class 代码使用 GCC Extended ASM。这是相关代码:

struct CPUIDinfo
{
    word32 EAX;
    word32 EBX;
    word32 ECX;
    word32 EDX;
};

bool CpuId(word32 func, word32 subfunc, CPUIDinfo& info)
{
    uintptr_t scratch;

    __asm__ __volatile__ (

        ".att_syntax \n"

#if defined(__x86_64__)
        "\t xchgq %%rbx, %q1 \n"
#else
        "\t xchgl %%ebx, %k1 \n"
#endif

        "\t cpuid \n"

#if defined(__x86_64__)
        "\t xchgq %%rbx, %q1 \n"
#else
        "\t xchgl %%ebx, %k1 \n"
#endif

      : "=a"(info.EAX), "=&r"(scratch), "=c"(info.ECX), "=d"(info.EDX)
      : "a"(func), "c"(subfunc)
    );

    if(func == 0)
        return !!info.EAX;

    return true;
}

下面的代码是在 Cygwin i386 上用 -g3 -Og 编译的。当我在调试器下检查它时,我不喜欢我所看到的。

Dump of assembler code for function CpuFeatures::DoDetectX86Features():
   ...
   0x0048f355 <+1>:     sub    [=11=]x48,%esp
=> 0x0048f358 <+4>:     mov    [=11=]x0,%ecx
   0x0048f35d <+9>:     mov    %ecx,%eax
   0x0048f35f <+11>:    xchg   %ebx,%ebx
   0x0048f361 <+13>:    cpuid
   0x0048f363 <+15>:    xchg   %ebx,%ebx
   0x0048f365 <+17>:    mov    %eax,0x10(%esp)
   0x0048f369 <+21>:    mov    %ecx,0x18(%esp)
   0x0048f36d <+25>:    mov    %edx,0x1c(%esp)
   0x0048f371 <+29>:    mov    %ebx,0x14(%esp)
   0x0048f375 <+33>:    test   %eax,%eax
   ...

我不喜欢我所看到的,因为看起来 EBX/RBX 而不是 被保留(xchg %ebx,%ebx+11)。此外,看起来保留的 EBX/RBX 被保存为 CPUID 的结果,而不是 CPUID 返回的 EBX 的实际值(xchg %ebx,%ebx+15,在 +29 处的 mov %ebx,0x14(%esp) 之前。

如果我将操作数更改为使用带有 "=&m"(scratch) 的内存操作,则生成的代码为:

0x0048f35e <+10>:    xchg   %ebx,0x40(%esp)
0x0048f362 <+14>:    cpuid
0x0048f364 <+16>:    xchg   %ebx,0x40(%esp)

一个相关的问题是

我做错了什么(除了在本应花费 5 或 15 分钟的事情上浪费了无数时间)?

下面的代码是一个完整的示例,我用它来编译上面的示例代码,包括修改以直接交换(交换)到 info.EBX 变量。

#include <inttypes.h>
#define word32 uint32_t

struct CPUIDinfo
{
    word32 EAX;
    word32 EBX;
    word32 ECX;
    word32 EDX;
};

bool CpuId(word32 func, word32 subfunc, CPUIDinfo& info)
{
    __asm__ __volatile__ (

        ".att_syntax \n"

#if defined(__x86_64__)
        "\t xchgq %%rbx, %q1 \n"
#else
        "\t xchgl %%ebx, %k1 \n"
#endif

        "\t cpuid \n"

#if defined(__x86_64__)
        "\t xchgq %%rbx, %q1 \n"
#else
        "\t xchgl %%ebx, %k1 \n"
#endif

      : "=a"(info.EAX), "=&m"(info.EBX), "=c"(info.ECX), "=d"(info.EDX)
      : "a"(func), "c"(subfunc)
    );

    if(func == 0)
        return !!info.EAX;

    return true;
}

int main()
{
    CPUIDinfo  cpuInfo;
    CpuId(1, 0, cpuInfo);
}

您应该做的第一个观察是我选择使用 info.EBX 内存位置进行实际交换。这消除了对另一个临时变量或寄存器的需要。

我用 -g3 -Og -S -m32 汇编成 32 位代码,得到了这些感兴趣的指令:

xchgl %ebx, 4(%edi)
cpuid
xchgl %ebx, 4(%edi)

movl    %eax, (%edi)
movl    %ecx, 8(%edi)
movl    %edx, 12(%edi)

%edi恰好包含了info结构的地址。 4(%edi)恰好是info.EBX的地址。我们在 cpuid 之后交换 %ebx4(%edi)。通过该指令,ebx 恢复到 cpuid 之前的状态,并且 4(%edi) 现在具有 ebx 在执行 cpuid 之后的状态。剩余的 movl 行将 eaxecxedx 寄存器通过 %edi 寄存器放入 info 结构的其余部分。

上面生成的代码是我所期望的。

带有 scratch 变量(并使用约束 "=&m"(scratch))的代码在汇编程序模板之后永远不会被使用,因此 %ebx,0x40(%esp) 具有您想要的值,但它永远不会移动到任何地方有用。您必须将 scratch 变量复制到 info.EBX(即 info.EBX = scratch;)并查看生成的所有结果指令。在某些时候,数据将从 scratch 内存位置复制到生成的汇编指令中的 info.EBX

更新 - Cygwin 和 MinGW

我对 Cygwin 代码输出的正确性并不完全满意。半夜我有一个啊哈!片刻。当动态 link 加载程序加载图像(DLL 等)并通过重新定位修改图像时,Windows 已经执行了自己的位置独立代码。不需要像在 Linux 32 位共享库中那样进行额外的 PIC 处理,因此 ebx/rbx 没有问题。这就是为什么Cygwin和MinGW在用-fPIC

编译时会出现这样的警告

warning: -fPIC ignored for target (all code is position independent)

这是因为在 Windows 下,所有 32 位代码在被 Windows 动态加载器加载时都可以重新基址。可以在这个 Dr. Dobbs article. Information on the windows Portable Executable format (PE) can be found in this Wiki article 中找到更多关于重新定位的信息。 Cygwin 和 MinGW 不需要担心在针对 32 位代码时保留 ebx/rbx,因为在他们的平台上 PIC 已经由 OS、其他 re-base 工具和link呃。