内联汇编导致没有前缀的错误

Inline Assembly Causing Errors about No Prefixes

您好,

因此,我正在优化我为正在开发的简单操作系统编写的一些函数。这个函数,putpixel(),目前看起来像这样(以防我的汇编不清楚或错误):

uint32_t loc  = (x*pixel_w)+(y*pitch);
vidmem[loc]   = color & 255;
vidmem[loc+1] = (color >> 8) & 255;
vidmem[loc+2] = (color >> 16) & 255;

这需要一点解释。首先,loc 是我要写入显存中的像素索引。 X 和 Y 坐标传递给函数。然后,我们将 X 乘以以字节为单位的像素宽度(在本例中为 3),将 Y 乘以每行中的字节数。可以找到更多信息 here.

vidmem是一个全局变量,一个uint8_t指向显存的指针。

话虽这么说,任何熟悉按位运算的人都应该能够相当容易地理解 putpixel() 的工作原理。

现在,这是我的程序集。请注意,它尚未经过测试,甚至可能会变慢或根本无法正常工作。这个问题是关于如何编译的。

我已将 loc 定义后的所有内容替换为:

__asm(
    "push %%rdi;"
    "push %%rbx;"
    "mov %0, %%rdi;"
    "lea %1, %%rbx;" 
    "add %%rbx, %%rdi;"
    "pop %%rbx;"
    "mov %2, %%rax;"
    "stosb;"
    "shr , %%rax;"
    "stosb;"
    "shr , %%rax;"
    "stosb;"
    "pop %%rdi;" : :
    "r"(loc), "r"(vidmem), "r"(color)
);

当我编译这个时,clang 为每个 push 指令都给我这个错误:

因此,当我看到该错误时,我认为这与我遗漏了 GAS 后缀有关(无论如何,这应该是隐式决定的)。但是当我添加“l”后缀(我所有的变量都是uint32_ts)时,我得到了同样的错误!我不太确定是什么原因造成的,我们将不胜感激。提前致谢!

找到问题了!

它出现在很多地方,但主要的是 vidmem。我以为它会传递地址,但它导致了错误。在将其称为 dword 后,它运行完美。我还不得不将其他约束更改为"m",最后得到了这个结果(经过一些优化):

__asm(
    "movl %0, %%edi;"
    "movl %k1, %%ebx;" 
    "addl %%ebx, %%edi;"
    "movl %2, %%eax;"
    "stosb;"
    "shrl , %%eax;"
    "stosw;" : :
    "m"(loc), "r"(vidmem), "m"(color)
    : "edi", "ebx", "eax"
);

感谢所有在评论中回答的人!

您可以通过在存储之前将 vidmem 加载到局部变量中,使编译器针对您的 C 版本的输出更加高效。事实上,它不能假定存储不使用别名 vidmem,因此它会在每个字节存储之前重新加载指针。嗯,这确实让 gcc 4.9.2 避免重新加载 vidmem,但它仍然会生成一些讨厌的代码。 clang 3.5 稍微好一点。

实现我在对您的回答的评论中所说的(stos 是 3 微指令,而 mov 是 1 微指令):

#include <stdint.h>

extern uint8_t *vidmem;
void putpixel_asm_peter(uint32_t color, uint32_t loc)
{
    // uint32_t loc  = (x*pixel_w)+(y*pitch);
    __asm(  "\n"
        "\t movb %b[col], (%[ptr])\n"
        "\t shrl , %[col];\n"
        "\t movw %w[col], 1(%[ptr]);\n"
        : [col] "+r" (color),  "=m" (vidmem[loc])
        : [ptr] "r" (vidmem+loc)
        :
        );
}

编译为非常有效的实现:

gcc -O3 -S -o- putpixel.c 2>&1 | less  # (with extra lines removed)

putpixel_asm_peter:
        movl    %esi, %esi
        addq    vidmem(%rip), %rsi
#APP
        movb %dil, (%rsi)
        shrl , %edi;
        movw %di, 1(%rsi);
#NO_APP
        ret

所有这些指令在 Intel CPU 上解码为单个 uop。 (存储可以微融合,因为它们使用单​​寄存器寻址模式。)movl %esi, %esi 将高位 32 归零,因为调用者可能已经使用 64 位指令生成了该函数 arg,在高位 32 中留下了垃圾%rsi。您的版本可以通过首先使用约束来请求所需寄存器中的值来保存一些指令,但这仍然比 stos

还要注意我是如何让编译器负责将 loc 添加到 vidmem 的。你本可以更有效地完成它,使用 lea 将添加与移动结合起来。但是,如果编译器想在循环中使用它时变得聪明,它可以递增指针而不是递增地址。最后,这意味着相同的代码将适用于 32 位和 64 位。 %[ptr] 在 64 位模式下是 64 位 reg,但在 32 位模式下是 32 位 reg。因为我不需要对它做任何数学运算,所以它很管用。

我使用 =m 输出约束来告诉编译器我们在内存中写入的位置。 (我应该将指针指向 struct { char a[3]; } 或其他东西,以告诉 gcc 它实际写入了多少内存,根据 the gcc manual 中 "Clobbers" 部分末尾的提示)

我还使用了color作为input/output约束来告诉编译器我们修改了它。如果它被内联,并且后面的代码预计仍会在寄存器中找到 color 的值,我们就会遇到问题。在函数中使用 this 意味着 color 已经是调用者值的 tmp 副本,因此编译器将知道它需要丢弃旧颜色。使用两个只读输入在循环中调用它可能会稍微更有效:一个用于 color,一个用于 color >> 8.

请注意,我可以将约束写为

    : [col] "+r" (color), [memref] "=m" (vidmem[loc])
    :
    :

但是使用 %[memref]1 %[memref] 生成所需的地址会导致 gcc 发出

    movl    %esi, %esi
    movq    vidmem(%rip), %rax
# APP
    movb %edi, (%rax,%rsi)
    shrl , %edi;
    movw %edi, 1 (%rax,%rsi);

双寄存器寻址模式意味着存储指令不能微融合(至少在 Sandybridge 和更高版本上)。

你甚至不需要内联 asm 来获得像样的代码:

void putpixel_cast(uint32_t color, uint32_t loc)
{
    // uint32_t loc  = (x*pixel_w)+(y*pitch);
    typeof(vidmem) vmem = vidmem;
    vmem[loc]   = color & 255;
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    *(uint16_t *)(vmem+loc+1) = color >> 8;
#else
    vmem[loc+1] = (color >> 8) & 255; // gcc sucks at optimizing this for little endian :(
    vmem[loc+2] = (color >> 16) & 255;
#endif
}

编译为(gcc 4.9.2 和 clang 3.5 给出相同的输出):

    movq    vidmem(%rip), %rax
    movl    %esi, %esi
    movb    %dil, (%rax,%rsi)
    shrl    , %edi
    movw    %di, 1(%rax,%rsi)
    ret

这只比我们使用内联 asm 得到的效率低一点点,如果内联到循环中,优化器应该更容易优化。

整体表现

在循环中调用它可能是一个错误。将多个像素组合在一个寄存器(尤其是矢量寄存器)中,然后一次写入会更有效。或者,执行 4 字节写入,重叠前一个写入的最后一个字节,直到到达结尾并且必须保留最后一个块 3 之后的字节。

参见 http://agner.org/optimize/ for more stuff about optimizing C and asm. That and other links can be found at https://whosebug.com/tags/x86/info