使用 char 数组块作为内存输出操作数的内联汇编

Inline assembly with chunks of a char array as memory output operands

我正在执行 cpuid(leaf 0) 以提供供应商字符串。代码(在 block1 下)工作正常并显示 GenuineIntel 正如我预期的那样。在下面的 asm block2 中,我想直接将 ebx, edx, ecx 值映射到 vendor 数组,而不是使用显式的 mov 指令。

目前我正在尝试将生成的 ebx 值(四个字节)移动到 vendor 数组的前四个字节中。这会在屏幕上显示 G 的值,这是 ebx.

的第一个字节

我尝试转换为 uint32_t*,但出现构建错误 lvalue required in asm statement

我想了解应该对代码进行哪些更改才能将前四个字节写入供应商数组?有没有办法不使用明确的 mov 指令来做到这一点?任何帮助表示赞赏。谢谢。

#include <iostream>
#include <cstdint>
using namespace std;

const int VENDORSIZE = 12;
int main(int argc, char **argv)
{
    char vendor[VENDORSIZE +1]{};
    uint32_t leaf = 0;
    vendor[VENDORSIZE] = '[=10=]';
    // Block 1
    /*asm volatile(
        "cpuid\n"
        "mov %%ebx, %0\n"
        "mov %%edx, %1\n"
        "mov %%ecx, %2\n"
        :"=m"(vendor[0]),"=m"(vendor[4]),"=m"(vendor[8])
        :"a"(leaf)
        :
    );*/

    // Block 2
    asm volatile(
    "cpuid\n"
    :"=b"(*vendor)
    :"a"(leaf)
    :
   );
    
    cout << vendor<< endl;
    return 0;
}

我对演员表的尝试:

// Block 2
    asm volatile(
    "cpuid\n"
    :"=b"((uint32_t*) vendor)
    :"a"(leaf)
    :
   );

这会产生一个错误:

cpuid.cpp:28:5: error: invalid lvalue in asm output 0

基于下面 Peter Corde 的 link - 我添加了缺失的 dereference。下面的代码现在输出 GenuineIntel。我 衷心感谢您的帮助。

// Block 2
    asm volatile(
    "cpuid\n"
    :"=b"(*(uint32_t*)vendor),"=d"(*(uint32_t*)(vendor+4)),"=c"(*(uint32_t*)(vendor+8))
    :"a"(leaf)
    :
   );

我不太熟悉这种内联汇编语法,但您可以尝试两种方法。

  1. 不要在内联汇编器中使用旧式转换

    :"=b"(* static_cast(供应商))

  2. 在 C++ 代码中添加一个 uint32_t* 变量并在汇编程序中使用它

    uint32_t* pVendor = static_cast(&vendor[0]);

    :"=b"(*pVendor)

首先,对于实际使用 cpuid,更喜欢使用 GCC cpuid.h 中的 __get_cpuid 或 GNU C 内置函数等内部包装器。

  • How do I call "cpuid" in Linux? 对于 __get_cpuid
  • Intrinsics for CPUID like informations? 对于像 __builtin_cpu_supports("avx")
  • 这样的东西
  • https://wiki.osdev.org/CPUID

这个答案的其余部分只是以 CPUID 为例来讨论字符和数组块作为 GNU C 内联 asm 的操作运行ds,以及其他正确性要点。


*vendor 的类型为 char,因此您已要求编译器在您的 asm 指令后将 BL 作为 vendor[0](又名 *vendor)的值 运行。这就是为什么它只存储 G,EBX 的低字节。

如果您查看编译器生成的 asm https://godbolt.org/z/5bva6zvvK 并注意 movb %bl, 2(%rsp)

,就可以看到这一点

您的 asm 中的其他错误:

  • 你不告诉编译器EAX被asm语句修改,而是告诉编译器"a"(0)是纯输入
  • 您的块 1(带有 mov 存储)未能告诉编译器 EBX、ECX 和 EDX 也被破坏了。使用 "=b""=c""=d" 输出可以解决这个问题。
  • 带有"=m"(vendor[0])vendor[4]等的版本只是告诉编译器数组的0、4、8字节被修改了,没有字节 1..3 或 5..7。所以你没有告诉编译器的asm存储到内存是一个输出。这在实践中不太可能成为问题,但请参阅 以了解将整个数组声明为输出的方法。

此外,volatile 在这里是矫枉过正/不必要的。 CPUID 叶子 0(我认为其他叶子)每次都会给您相同的结果,并且整个 asm 语句除了写入其输出 ope运行ds 之外没有任何副作用,因此它是其输入 ope 的纯函数运行d.这就是 non-volatile asm 所暗示的。 (假设您出于某种原因不需要它作为序列化指令或内存屏障来执行双重任务。)不太重要,因为您希望无论如何都不会在循环中编写 运行 此语句的代码; CPUID 很慢,所以你想缓存结果,而不是依赖 common-subexpression-elimination。我想如果您根本没有实际打印结果,让这个优化消失可能会有用。

例如在 asm 模板中使用 mov 的安全代码如下所示:

const int VENDORSIZE = 12;
int main1()
{
    char vendor[VENDORSIZE+2];
    int leaf = 0;
    asm (   // doesn't need to be volatile; we'll get the same result for eax=0 every time
        "cpuid\n"
        "mov %%ebx, %0\n"
        "mov %%edx, 4 + %0\n"
        "mov %%ecx, 8 + %0\n"
        : "=m"(vendor)    // the whole local array is an output.
                       //  Only works for true arrays; pointers need casting to array
          ,"+a"(leaf)  // EAX is modified, too
        :  // no pure inputs
        : "ebx", "ecx", "edx"  // Tell compiler about registers we destroyed.
    );
    vendor[VENDORSIZE+0] = '\n';
    vendor[VENDORSIZE+1] = '[=10=]';
    std::cout << vendor;     // std::endl is pointless here
                             // so just make room for \n in the array
                             // instead of a separate << '\n'  function call.
    return 0;
}

我用整个数组(vendor)作为内存输出ope运行d,而不是*vendorvendor[4]等。优化的asm将是相同的,但在禁用优化的情况下,3 输出方式可能会生成 3 个单独的指针。更重要的是,它解决了告诉编译器每一个被写入的问题。

它还告诉编译器 整个 数组被写入,而不仅仅是前 12 个字节,所以如果我分配了 '\n''[=38= ]' 在 asm 语句之前,编译器可以合法地将它们作为死存储删除。 (它没有,但我认为它可以用 "=m"(vendor) 而不是 "+m"。)

AT&T 语法有很好的 属性 内存寻址模式是可偏移的,因此 4 + %0 扩展为类似 4 + 2(%rsp) 的内容,即 6(%rsp)。如果编译器碰巧选择了一个没有数字的寻址模式,例如 (%rsp),GAS 会接受 4 + (%rsp) 等同于 4(%rsp),尽管会出现 Warning: missing operand; zero assumed.[=94 这样的警告=]

如果这是一个采用 char* arg 的函数,那么您只有一个指针,而不是实际的 C 数组,您必须强制转换为指向数组的指针并取消引用。这看起来会违反严格别名,但这实际上是 GCC 手册所推荐的。参见

    ...  // if vendor is just a char* function arg

    : "=m"( *(char (*)[VENDORSIZE]) vendor )   
      // tells the compiler that we write 12 bytes
      // With empty [], would tell the compiler we might write an arbitrary size starting at that pointer.

使用寄存器输出ope运行ds

"=b"( *(uint32_t*)&vendor[0] ) 可以工作,但违反了严格别名规则 的指针转换,通过 uint32_t * 访问 char 对象。它恰好在当前 GCC/clang 中工作,但除非您使用 -fno-strict-aliasing.

编译,否则它不会真正安全/受支持

Example on Godbolt(也包括 mov 版本和下面的 uint32_t[] 版本)显示它编译并且 运行s 正确(使用 GCC、clang 和 ICC。)

    // works but violates strict-aliasing
    char vendor[VENDORSIZE + 2];

    asm( "cpuid"
    : "+a"(leaf),       // read/write operand
      "=b"( *(uint32_t*)&vendor[0] ),   // strict-aliasing violation in the pointer cast
      "=d"( *(uint32_t*)&vendor[4] ),
      "=c"( *(uint32_t*)&vendor[8] )
     // no pure inputs, no clobbers
   );

您可以合法地将 char* 指向任何对象,但将其他对象指向 char 对象并不是绝对安全的。如果 vendor 是指向您从 malloc 或其他东西获得的内存的指针,那么内存将没有底层类型,只需通过 uint32_t* 访问,然后通过 char * 读取,这样就安全了.但是对于一个实际的数组,我认为它不是,即使数组访问是根据指针取消引用工作的。

您可以将数组声明为uint32_t,然后使用char *访问那些字节:

完全安全版

int main3()  // fully safe without strict-aliasing violations.
{
    uint32_t vendor[VENDORSIZE/sizeof(uint32_t) + 1];  // wastes 2 bytes
    int leaf = 0;
    asm( "cpuid"
     : "+a"(leaf),      // read/write operand, compiler needs to know that CPUID writes EAX
       "=b"( vendor[0] ),  // ask the compiler to assign to the array
       "=d"( vendor[1] ),
       "=c"( vendor[2] )
      // no pure inputs, no clobbers
    );
    
    vendor[3] = '\n';  // x86 is little-endian so the [=13=] terminator is part of this.
    std::cout << reinterpret_cast<const char*>(vendor);
    return 0;
}

这是“更好”吗?它完全避免了任何未定义的行为,代价是浪费了 2 个字节(16 字节数组与 14 字节数组)。否则编译相同(除了带有换行符的双字存储实际上可能更好,因为 GCC 如何使用两条指令来确保避免在 Sandybridge 之前的 CPU 上出现 LCP 停顿)。使用指向 uint32_t[]char* 是合法的,取消引用它也是合法的,因此将它传递给 cout::operator<<.

这样的函数是完全安全的

它看起来也相当可读:你基本上是从 CPUID 中获取 uint32_t 的块,然后 reinterpret 将这些字节作为字符数组,所以代码的语义是written 确实很好地展示了正在发生的事情。在 '\n' 上添加有点不明显,但是 ((char*)vendor)[12] = '\n'; / ... [13] = 0;` 可以使它更清楚。

我不知道指针转换版本中的 C++ UB(char[] 数组上的严格别名违规)在任何未来的编译器上引起问题的可能性/可能性有多大。我非常有信心它在当前 GCC/clang/ICC 上很好,即使在内联到复杂的周围代码中,这些代码在之前/之后为其他事情重用数组。


如果您正在为双端架构(或简单地在大端机器上)编写可移植的内联 asm,您可能 memcpy(vendor+3, "\n", 2),或转换为 char* 进行分配确保将字符存储在正确的字节偏移处。当然,将寄存器存储到 char 数组的整个想法将取决于每个寄存器的 4 个字符的顺序与当前的字节顺序相匹配。


问题的其他部分

I tried casting to uint32_t* and that gives a build error lvalue required in asm statement.

可能是因为编译器抱怨右值而不是左值,所以您将转换放在其他地方或遗漏了一些取消引用。您放在括号内的 C++ 表达式必须是您要分配给的 C++ 对象,即使是 "=m" 内存操作 运行d。这就是你在第一个版本中使用 vendor[4] 而不是 vendor+4 的原因。

directly map the ebx, edx, ecx values to the vendor array

请记住,如果编译器在内存中需要它们(例如,当它将 vendor 传递给 cout::operator<<(char*) 时),它将必须在您的 asm 之后发出 mov 存储指令模板。 C++ 变量和 ope运行d 位置之间的映射就像一个 = 赋值,在这种情况下你没有保存 asm 指令。

如果您正在做 vendor[0] == 'G' 或其他可以内联的 memcmp,您将保存指令;编译器可以只检查 blebx 而不是存储然后使用内存操作 运行d.

但总的来说,是的,让编译器处理数据移动是个好主意,让您的 asm 模板保持最小,只告诉编译器输入和输出的位置。我只是想弄清楚“直接映射”的含义和含义。在您的 asm 模板字符串周围查看编译器生成的 asm(并检查它选择了什么)通常是个好主意。