gpt4 book ai didi

GCC 代码似乎违反了内联汇编规则,但专家认为并非如此

转载 作者:行者123 更新时间:2023-12-04 14:37:49 30 4
gpt4 key购买 nike

我与一位专家合作,据称他的编码技能比我自己高得多,他对内联汇编的理解比我以往任何时候都好。

其中一项主张是,只要操作数作为输入约束出现,您就无需将其列为破坏项或指定寄存器可能已被内联程序集修改。当其他人试图通过 memset 寻求帮助时,对话就开始了。以这种方式有效编码的实现:

void *memset(void *dest, int value, size_t count)
{
asm volatile ("cld; rep stosb" :: "D"(dest), "c"(count), "a"(value));
return dest;
}

当我在没有告诉编译器的情况下评论破坏寄存器的问题时,专家的主张是告诉我们:

"c"(count) already tells the compiler c is clobbered



我找到了 an example在专家自己的操作系统中,他们使用相同的设计模式编写相似的代码。他们使用 Intel 语法进行内联汇编。这个爱好操作系统代码在内核 (ring0) 上下文中运行。一个例子是这个缓冲区交换函数1:
void swap_vbufs(void) {
asm volatile (
"1: "
"lodsd;"
"cmp eax, dword ptr ds:[rbx];"
"jne 2f;"
"add rdi, 4;"
"jmp 3f;"
"2: "
"stosd;"
"3: "
"add rbx, 4;"
"dec rcx;"
"jnz 1b;"
:
: "S" (antibuffer0),
"D" (framebuffer),
"b" (antibuffer1),
"c" ((vbe_pitch / sizeof(uint32_t)) * vbe_height)
: "rax"
);

return;
}
antibuffer0 , antibuffer1 , 和 framebuffer内存中的所有缓冲区都被视为 uint32_t 的数组. framebuffer是实际显存 (MMIO) 和 antibuffer0 , antibuffer1是在内存中分配的缓冲区。

在调用此函数之前正确设置了全局变量。它们被声明为:
volatile uint32_t *framebuffer;
volatile uint32_t *antibuffer0;
volatile uint32_t *antibuffer1;

int vbe_width = 1024;
int vbe_height = 768;
int vbe_pitch;

我对这种代码的问题和担忧

作为一个明显的内联汇编新手,对这个主题有明显的天真理解,我想知道我明显没有受过教育的信念是否是正确的。我想知道这些担忧是否有任何值(value):
  • RDI、RSI、RBX 和 RCX 均由此代码修改。 RDI 和 RSI 由 LODSD 和 STOSD 隐式递增。其余的显式修改为
        "add rbx, 4;"
    "dec rcx;"

    这些寄存器均未列为输入/输出,也未列为输出操作数。我相信需要修改这些约束以通知编译器这些寄存器可能已被修改/破坏。我认为正确的唯一被列为 clobbered 的寄存器是 RAX。我的理解正确吗?我的感觉是 RDI、RSI、RBX 和 RCX 应该是输入/输出约束(使用 + 修饰符)。即使有人试图争辩说 64 位 System V ABI 调用约定将保存它们(假设编写此类代码的方式很糟糕,恕我直言)RBX 是一个非 volatile 寄存器,它将在此代码中更改。
  • 由于地址是通过寄存器(而不是内存约束)传递的,我相信这是一个潜在的错误,编译器没有被告知这些指针指向的内存已被读取和/或修改。我的理解正确吗?
  • RBX 和 RCX 是硬编码寄存器。允许编译器通过约束自动选择这些寄存器难道没有意义吗?
  • 如果假设必须在这里使用内联汇编(假设)该函数的无错误 GCC 内联汇编代码会是什么样的?这个函数是否正常,我只是不像专家那样了解 GCC 的扩展内联汇编的基础知识?


  • 脚注
  • 1 swap_vbufs 未经版权所有者许可,函数和相关变量声明已逐字复制 fair use用于对更大范围的工作进行评论。
  • 最佳答案

    从各方面来说你都是正确的,这段代码对编译器来说充满了谎言,可能会咬你。 例如使用不同的周围代码,或不同的编译器版本/选项(尤其是链接时优化以启用跨文件内联)。
    swap_vbufs 甚至看起来效率不高,我怀疑 gcc 与纯 C 版本相同或更好。 https://gcc.gnu.org/wiki/DontUseInlineAsmstosd 在英特尔上是 3 uops,比普通的 mov -store + add rdi,4 差。并且使 add rdi,4 无条件将避免对 else 块的需要,该块将额外的 jmp 放在(希望)快速路径上,因为缓冲区相等,因此没有 MMIO 存储到视频 RAM。

    ( lodsd 在 Haswell 和更新版本上只有 2 个 uops,所以如果您不关心 IvyBridge 或更旧版本也没关系)。

    在内核代码中,我猜他们正在避免 SSE2,即使它是 x86-64 的基线,否则您可能想要使用它。对于普通的内存目标,您只需将 memcpyrep movsd 或 ERMSB rep movsb 一起使用,但我想这里的重点是通过检查视频 RAM 的缓存副本来尽可能避免 MMIO 存储。尽管如此,使用 movnti 的无条件流存储可能是有效的,除非视频 RAM 映射到 UC(不可缓存)而不是 WC。

    很容易构建在实践中确实会中断的示例,例如在同一函数中的内联 asm 语句之后再次使用相关的 C 变量。 (或在内联 asm 的父函数中)。

    您要销毁的输入通常必须使用匹配的虚拟输出或带有 C tmp var 的 RMW 输出来处理,而不仅仅是 "r" 。或 "a"
    "r" 或任何特定寄存器约束,如 "D" 意味着这是一个只读输入,编译器可以期望在之后找到不受干扰的值。没有“我想销毁的输入”约束;您必须将其与虚拟输出或变量进行合成。

    这一切都适用于支持 GNU C 内联 asm 语法的其他编译器(clang 和 ICC)。

    从 GCC 手册: Extended asm Input Operands :

    Do not modify the contents of input-only operands (except for inputs tied to outputs). The compiler assumes that on exit from the asm statement these operands contain the same values as they had before executing the statement. It is not possible to use clobbers to inform the compiler that the values in these inputs are changing.



    ( rax clobber 会导致使用 "a" 作为输入出错;clobbers 和操作数不能重叠。)

    示例 1:寄存器输入操作数
    int plain_C(int in) {   return (in+1) + in;  }

    // buggy: modifies an input read-only operand
    int bad_asm(int in) {
    int out;
    asm ("inc %%edi;\n\t mov %%edi, %0" : "=a"(out) : [in]"D"(in) );
    return out + in;
    }

    Godbolt compiler explorer 上编译

    请注意,gcc 的 addledi 用于 in ,即使内联 asm 使用该寄存器作为输入 。 (因此中断,因为这个有问题的内联 asm 修改了寄存器)。在这种情况下,它恰好持有 in+1。我使用了 gcc9.1,但这不是新行为。
    ## gcc9.1 -O3 -fverbose-asm
    bad(int):
    inc %edi;
    mov %edi, %eax # out (comment mentions out because I used %0)

    addl %edi, %eax # in, tmp86
    ret

    我们通过告诉编译器同一个输入寄存器也是一个输出来解决这个问题,所以它不能再指望那个了。 (或通过使用 auto tmp = in; asm("..." : "+r"(tmp)); )
    int safe(int in) {
    int out;
    int dummy;
    asm ("inc %%edi;\n\t mov %%edi, %%eax"
    : "=a"(out),
    "=&D"(dummy)
    : [in]"1"(in) // matching constraint, or "D" works.
    );
    return out + in;
    }
    # gcc9.1 again.
    safe_asm(int):
    movl %edi, %edx # tmp89, in compiler-generated save of in
    # start inline asm
    inc %edi;
    mov %edi, %eax
    # end inline asm
    addl %edx, %eax # in, tmp88
    ret

    显然 "lea 1(%%rdi), %0" 会通过不修改输入来避免这些问题, mov/ inc 也是如此。这是一个故意破坏输入的人为示例。

    如果函数没有内联并且没有在 asm 语句之后使用输入变量,那么你通常不会对编译器撒谎,只要它是一个调用破坏的寄存器。

    发现编写不安全代码的人在他们使用它的上下文中工作的人并不罕见。他们相信,在该上下文中使用一个编译器版本/选项简单地测试它就足以验证其安全性或正确性。

    但这不是 asm 的工作方式。编译器相信你能够准确地描述 asm 的行为,并且只是在模板部分进行文本替换。

    如果 gcc 假设 asm 语句总是破坏它们的输入,那将是一个糟糕的错过优化。事实上,内联 asm 使用的相同约束(我认为)在内部机器描述文件中使用,这些文件教 gcc 关于 ISA。 (因此破坏的输入对于代码生成来说是可怕的)。

    GNU C 内联汇编的整个设计基于包装单个指令,这就是为什么即使是用于输出的 early-clobber 也不是默认设置。如果需要在内联汇编中编写多个指令或循环,您必须手动执行此操作。

    a potential bug that the compiler hasn't been told that memory that these pointers are pointing at has been read and or modified.



    这也是正确的。寄存器输入操作数并不意味着指向的内存也是输入操作数。在不能内联的函数中,这实际上不会引起问题,但是一旦启用链接时优化,跨文件内联和过程间优化就成为可能。

    有一个现有的 Informing clang that inline assembly reads a particular region of memory 未回答的问题。这个 Godbolt link 显示了一些可以揭示这个问题的方法,例如
       arr[2] = 1;
    asm(...);
    arr[2] = 0;

    如果 gcc 假设 arr[2] 不是 asm 的输入,而只是 arr 地址本身,它将执行死存储消除并删除 =1 分配。 (或者将其视为使用 asm 语句重新排序商店,然后将 2 个商店折叠到同一位置)。

    数组很好,因为它表明即使 "m"(*arr) 也不适用于指针,只能用于实际数组。该输入操作数只会告诉编译器 arr[0] 是一个输入,仍然不是 arr[2] 。如果这就是你所有的 asm 读取,那是一件好事,因为它不会阻止其他部分的优化。

    对于那个 memset 示例,要正确声明指向的内存是输出操作数,请将指针转换为指向数组的指针并取消引用它,以告诉 gcc 整个内存范围是操作数。 *(char (*)[count])pointer 。 (您可以将 [] 留空以指定通过此指针访问的任意长度的内存区域。)
    // correct version written by @MichaelPetch.  
    void *memset(void *dest, int value, size_t count)
    {
    void *tmp = dest;
    asm ("rep stosb # mem output is %2"
    : "+D"(tmp), "+c"(count), // tell the compiler we modify the regs
    "=m"(*(char (*)[count])tmp) // dummy memory output
    : "a"(value) // EAX actually is read-only
    : // no clobbers
    );
    return dest;
    }

    使用虚拟操作数包含一个 asm 注释让我们看到编译器如何分配它。我们可以看到编译器使用 AT&T 语法选择 (%rdi),因此它愿意使用同时也是输入/输出操作数的寄存器。

    如果输出上有早期破坏,它可能想要使用另一个寄存器,但如果没有它,我们不会花费任何代价来获得正确性。

    使用不返回指针的 void 函数(或内联到不使用返回值的函数之后),在让 rep stosb 销毁它之前不必将指针 arg 复制到任何地方。

    关于GCC 代码似乎违反了内联汇编规则,但专家认为并非如此,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/56142479/

    30 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com