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

Posted

技术标签:

【中文标题】使用 char 数组块作为内存输出操作数的内联汇编【英文标题】:Inline assembly with chunks of a char array as memory output operands 【发布时间】:2021-08-22 14:43:02 【问题描述】:

我正在执行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] = '\0';
    // 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 的链接 - 我添加了缺少的 dereference。下面的代码现在输出GenuineIntel。一世 衷心感谢您的帮助。

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

【问题讨论】:

转换为 uint32_t* 应该可以。显示您尝试过的内容。 可以肯定的是,我在选角方面搞砸了。上面的编辑 1 显示了我尝试过的内容。 您不需要leaf 作为输入/输出操作数吗,因为cpuid 会覆盖eax?编译器可能不想重用该值,但理论上它可以尝试。 我认为网站上有一个问题,其中有一个在 inline asm 中正确执行 cpuid 的示例,但我找不到。在大多数情况下,包括这种情况,最好使用内在函数。 @NateEldredge:确实,将leaf 声明为只读输入是一个问题。终于完成了我正在研究的答案。 【参考方案1】:

首先,对于实际使用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 内联汇编的操作数,以及其他正确性。


*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 存储到你没有告诉编译器的内存是一个输出。在实践中这不太可能成为问题,但请参阅 How can I indicate that the memory *pointed* to by an inline ASM argument may be used? 了解将整个数组声明为输出的方法。

另外,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) 作为内存输出操作数,而不是 *vendorvendor[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*)&amp;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&lt;&lt; 这样的函数是完全安全的。

这似乎也相当易于人类阅读:您基本上是从 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 数组

请记住,如果编译器需要它们在内存中(例如,当它传递 vendorcout::operator&lt;&lt;(char*) 时),它将不得不在您的 asm 模板之后发出 mov 存储指令。 C++ 变量和操作数位置之间的映射就像一个 = 赋值,在这种情况下,您不会保存 asm 指令。

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

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

【讨论】:

【参考方案2】:

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

    不要在内联汇编器中使用旧式强制转换

    :"=b"(* static_cast(vendor))

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

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

    :"=b"(*pVendor)

【讨论】:

:"=b"(static_cast&lt;uint32_t*&gt;(vendor)) - 不,它告诉编译器 RBX 中的值是指针,而不是 uint32_t。你忘了取消引用它。 (这将违反严格混叠规则)。你在第二个子弹中做对了。我自己正在寻找答案,但现在godbolt.org/z/3aqKhTx1P 关于取消引用的评论以及上面的链接帮助我解决了这个问题。我可以接受你的回答。

以上是关于使用 char 数组块作为内存输出操作数的内联汇编的主要内容,如果未能解决你的问题,请参考以下文章

Base64 汇编程序填充数组错误“操作数不同大小”Visual Studio

内联汇编代码和存储 128 位结果

通过内联汇编操作c变量[重复]

扩展内联汇编基础

GCC 内联汇编的副作用

GNU g++ 内联汇编块,如 Apple g++/Visual C++?