首先,对于实际使用cpuid,更喜欢使用内部包装器,如来自GCC 的cpuid.h 的__get_cpuid,或GNU C 内置函数。
这个答案的其余部分只是以 CPUID 为例来讨论字符块和数组作为 GNU C 内联汇编的操作数,以及其他正确性。
*vendor 的类型为 char,因此您已要求编译器在您的 asm 指令运行后将 BL 作为vendor[0](又名*vendor)的值。这就是为什么它只存储G,EBX 的低字节。
如果您查看编译器生成的 asm https://godbolt.org/z/5bva6zvvK 并注意 movb %bl, 2(%rsp),您可以看到这一点
您的 asm 中的其他错误:
另外,volatile 在这里是多余的/不必要的。 CPUID 叶 0(我认为其他叶)每次都会给您相同的结果,并且整个 asm 语句除了写入其输出操作数之外没有任何副作用,因此它是其输入操作数的纯函数。这就是非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] = '\0';
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) 作为内存输出操作数,而不是 *vendor、vendor[4] 等。优化后的 asm 将是相同的,但优化禁用了 3 输出方式可能生成了 3 个单独的指针。更重要的是,它解决了告诉编译器所写的每一个内容的问题。
它还告诉编译器 整个 数组已写入,而不仅仅是前 12 个字节,所以如果我在 asm 语句之前分配了 '\n' 和 '\0',编译器可以合法地将它们作为死店移除。 (它没有,但我认为它可以用"=m"(vendor) 而不是"+m"。)
AT&T 语法具有内存寻址模式可偏移的良好特性,因此4 + %0 扩展为类似于4 + 2(%rsp) 的内容,即6(%rsp)。如果编译器碰巧选择了没有像(%rsp) 这样的数字的寻址模式,GAS 确实接受4 + (%rsp) 等同于4(%rsp),尽管带有像Warning: missing operand; zero assumed 这样的警告。
如果这是在一个接受 char* 参数的函数中,那么你只有一个指针,而不是一个实际的 C 数组,你必须强制转换为指向数组的指针并取消引用。这看起来会违反严格混叠,但它实际上是 GCC 手册所建议的。见How can I indicate that the memory *pointed* to by an inline ASM argument may be used?
... // 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.
使用寄存器输出操作数
"=b"( *(uint32_t*)&vendor[0] ) 可以工作,但使用该指针强制转换违反了严格别名规则,通过uint32_t * 访问char 对象。它恰好在当前的 GCC/clang 中工作,但除非您使用 -fno-strict-aliasing 编译,否则它不会真正安全/受支持。
Example on Godbolt(也包括 mov 版本和下面的uint32_t[] 版本)表明它可以正确编译和运行(使用 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 * 读取,这样就安全了。但是对于一个实际的数组,我认为不是,即使数组访问是根据指针 deref 工作的。
您可以将数组声明为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 \0 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 将这些字节作为字符数组,因此编写的代码的语义确实很好地显示了什么是继续。加入'\n' 有点不明显,但((char*)vendor)[12] = '\n'; / ... [13] = 0;`可以让它更清楚。
我不知道指针转换版本中的 C++ UB(char[] 数组上的严格混叠违规)在未来的任何编译器上造成问题的可能性有多大。我非常有信心它在当前的 GCC/clang/ICC 上很好,即使在内联到复杂的周围代码之后,这些代码在之前/之后将数组重用于其他事情。
如果您正在为双端架构(或只是在大端机器上)编写可移植的内联汇编,您可以memcpy(vendor+3, "\n", 2),或转换为char* 以确保您将字符存储在正确的字节偏移量。当然,将寄存器存储到 char 数组的整个想法取决于每个寄存器的 4 个字符的顺序是否与当前字节序匹配。
问题的其他部分
我尝试强制转换为 uint32_t*,这给出了 asm 语句中所需的构建错误左值。
大概是因为编译器抱怨的是右值而不是左值,所以你把你的演员放在其他地方或者省略了一些取消引用。您放在括号内的 C++ 表达式必须是您要分配的 C++ 对象,即使对于 "=m" 内存操作数也是如此。这就是您在第一个版本中使用vendor[4] 而不是vendor+4 的原因。
直接将ebx, edx, ecx 值映射到vendor 数组
请记住,如果编译器需要它们在内存中(例如,当它传递 vendor 到 cout::operator<<(char*) 时),它将不得不在您的 asm 模板之后发出 mov 存储指令。 C++ 变量和操作数位置之间的映射就像一个 = 赋值,在这种情况下,您不会保存 asm 指令。
如果您正在执行vendor[0] == 'G' 或其他操作,或者可以内联的memcmp,您将保存指令;编译器可以只检查 bl 或 ebx 而不是存储然后使用内存操作数。
但一般来说,让编译器处理数据移动是一个好主意,保持您的 asm 模板最小化,并且只告诉编译器输入和输出在哪里。我只是想弄清楚“直接映射”的含义和含义。查看编译器生成的 asm 围绕您的 asm 模板字符串(并检查它选择的内容)通常是一个好主意。