使用内存的 RISC-V 内联汇编行为不正确

RISC-V inline assembly using memory not behaving correctly

这个系统调用代码根本不起作用。编译器正在优化并通常表现得很奇怪:

template <typename... Args>
inline void print(Args&&... args)
{
    char buffer[1024];
    auto res = strf::to(buffer) (std::forward<Args> (args)...);
    const size_t size = res.ptr - buffer;

    register const char* a0 asm("a0") = buffer;
    register size_t      a1 asm("a1") = size;
    register long syscall_id asm("a7") = ECALL_WRITE;
    register long        a0_out asm("a0");

    asm volatile ("ecall" : "=r"(a0_out)
        : "m"(*(const char(*)[size]) a0), "r"(a1), "r"(syscall_id) : "memory");
}

这是一个以缓冲区和长度作为参数的自定义系统调用。 如果我使用全局汇编编写它,它会按预期工作,但如果我内联编写包装器,程序代码通常会非常好。

使用常量字符串调用 print 函数的函数产生无效的机器码:

0000000000120f54 <start>:
start():
  120f54:       fa1ff06f                j       120ef4 <public_donothing-0x5c>
-->
  120ef4:       747367b7                lui     a5,0x74736
  120ef8:       c0010113                addi    sp,sp,-1024
  120efc:       55478793                addi    a5,a5,1364 # 74736554 <add_work+0x74615310>
  120f00:       00f12023                sw      a5,0(sp)
  120f04:       00a00793                li      a5,10
  120f08:       00f10223                sb      a5,4(sp)
  120f0c:       000102a3                sb      zero,5(sp)
  120f10:       00500593                li      a1,5
  120f14:       06600893                li      a7,102
  120f18:       00000073                ecall
  120f1c:       40010113                addi    sp,sp,1024
  120f20:       00008067                ret

它没有用 sp 处的缓冲区加载 a0。

我做错了什么?

It's not loading a0 with the buffer at sp.

因为您没有要求指针作为寄存器中的 "r" 输入。 T foo asm("a0") 中唯一的 guaranteed/supported behaviour 是使 "r" 约束(包括 +r 或 =r)选择该寄存器。

但是您使用 "m" 让它为该缓冲区选择寻址模式,不一定是 0(a0),所以它可能选择了 SP-relative 模式。如果您在模板中添加 asm 注释,例如 "ecall # 0 = %0 1 = %1 2 = %2",您可以查看编译器的 asm 输出并查看它选择了什么。 (使用 clang,使用 -no-integrated-as 以便模板中的 asm 注释出现在 -S 输出中。)

包装系统调用确实需要特定寄存器中的指针,即使用"r"+"r"

    asm volatile ("ecall  # 0=%0   1=%1  2=%2  3=%3  4=%4"
        : "=r"(a0_out)
        : "r"(a0), "r"(a1), "r"(syscall_id), "m"(*(const char(*)[size]) a0)
        : // "memory"  unneeded; the "m" input tells the compiler which memory is read
    );

可以使用 "m" 输入 ,而不是 "r" 指针输入 。 (对于write具体来说,因为它只读取pointed-to内存的那一个区域而没有其他side-effects内存user-space可以看到,只能在内核写入缓冲区和file-descriptor 不是该程序可以直接访问的 C 对象的位置。对于 read 调用,您需要内存作为输出操作数。)

禁用优化后,编译器通常会选择另一个寄存器作为 "m" 输入的基础(例如 0(a5) 用于 GCC),但启用优化后 GCC 选择 0(a0) 所以它不需要额外的说明。 Clang 仍然选择 0(a2),浪费一条指令来设置该指针,即使 "=r"(a0_out) 不是 early-clobber。 (Godbolt,非常 cut-down 版本的函数不调用 strf::to,无论如何,只是将一个字节复制到缓冲区中。)


有趣的是,为我的 cut-down stand-alone 版本的函数启用了优化但没有修复错误,GCC 和 clang do 恰好放置了一个指针将 buffer 转换为 a0,选择 0(a0) 作为该操作数的模板扩展(参见上面的 Godbolt link)。与使用 16(sp) 相比,这似乎是一个错过的优化;我不明白他们为什么需要寄存器中的缓冲区地址。

但是没有优化,GCC 选择 ecall # 0 = a0 1 = 0(a5) 2 = a1。 (在我的简化版本的函数中,它将 a5 设置为 mv a5,a0,所以它实际上也有 a0 中的地址。所以你的函数中有更多代码以使其无法正常工作是件好事偶然,所以你可以在你的代码中找到错误。)