【问题标题】:C++ function call into RISC-V system callsC++ 函数调用到 RISC-V 系统调用
【发布时间】:2020-04-10 19:57:53
【问题描述】:

我有一个奇怪的情况,似乎对我很有效,但我需要知道如何让这件事变得更好或如何生活。

我使用 C++ 作为游戏引擎的编译脚本语言。 RISC-V 系统调用 ABI 与 C 函数调用约定相同,不同之处在于 A7 用于系统调用号,而不是第 8 个整数或指针参数。是的,你知道这是怎么回事。看:

extern "C" long syscall_enter(...);

template <typename... Args>
inline long syscall(long syscall_n, Args&&... args)
{
    asm volatile ("li a7, %0" : : "i"(syscall_n));
    return syscall_enter(std::forward<Args>(args)...);
}

虽然 syscall_enter 只是 .text 中的一个符号,带有 syscall 指令和一个 ret。系统调用返回值也和普通函数返回是同一个寄存器。

000103f0 <syscall_enter>:
syscall_enter():
   103f0:       00000073                ecall
   103f4:       00008067                ret

在此之前,我必须创建 20 多个函数来涵盖使用整数和带有编译器屏障的指针进行系统调用的所有各种方法,当我想添加一个采用浮点值的函数时,它会说调用是模棱两可的,因为整数和浮点数可以来回转换。因此,我可以开始为函数添加唯一名称,或者以更好的方式解决这个问题。老实说,这很烦人,并阻碍了原本出色的体验。我真的很喜欢能够在“双方”上使用 C++。

编译器生成的指令似乎没问题。它 JAL 和 JALR syscall_enter,这很好。编译器似乎有点困惑,但我不介意多加一条指令。

   10204:       1f500793                li      a5,501
   10208:       00078893                mv      a7,a5
   1020c:       00000513                li      a0,0
   10210:       1e0000ef                jal     ra,103f0 <syscall_enter>

以及中心摄像头的位置:

   100d4:       19600793                li      a5,406
   100d8:       00078893                mv      a7,a5
   100dc:       000127b7                lui     a5,0x12
   100e0:       4207b587                fld     fa1,1056(a5) # 12420 <_exit+0x2308>
   100e4:       22b58553                fmv.d   fa0,fa1
   100e8:       010000ef                jal     ra,100f8 <syscall_enter>

又是一个额外的移动指令。看起来不错。该 API 已经被大量使用,并且还有一个线程 API 可以使用它。

现在,有没有更好的方法?我想不出更好的方法来加载带有数字的 a7,然后强制编译器设置函数调用,而不进行实际的函数调用。我正在考虑为系统调用号使用模板参数,但我不太确定其余的。也许我们可以将参数的数量限制为 7?当有整数和浮点参数时它是不正确的,但这很好。堆栈存储的结构很容易传递。

经过一些测试,我决定使用这个:

extern "C" long syscall_enter(...);

template <typename... Args>
inline long syscall(long syscall_n, Args&&... args)
{
    // This will prevent some cases of too many arguments,
    // but not a mix of float and integral arguments.
    static_assert(sizeof...(args) < 8, "There is a system call limit of 8 integer arguments");
    // The memory clobbering prevents reordering of a7
    asm volatile ("li a7, %0" : : "i"(syscall_n) : "a7", "memory");
    return syscall_enter(std::forward<Args>(args)...);
    asm volatile("" : : : "memory");
}

应该够了。不需要系统调用函数垃圾邮件。对参数计数的检查不是最佳的,因为它应该只防止使用第 8 个整数寄存器(这意味着对整数、指针和引用参数进行计数)。但它会阻止某些情况。

【问题讨论】:

  • asm volatile ("mv a7, %0" : : "r"(syscall_n)); 你在没有告诉编译器的情况下写了一个寄存器。当这个函数内联时,如果编译器将它用于任何事情,它会通过踩a7 来破坏事情。此外,零保证a7 仍将设置在jal 之前。执行系统调用包装宏的常用方法是为每个可能的 args 数量创建不同的宏,并在 asm 语句中使用 "memory" clobber。 RISC-V inline assembly struct optimized away 是在正确的轨道上,只是错过了一个 "memory" clobber。
  • 例如MUSL libc 有 __syscall3 用于 3-arg 系统调用。 IDK 关于采用 FP args 的系统调用;我猜你需要不同的宏来排列它。例如github.com/bpowers/musl/blob/master/arch/mips/syscall_arch.h 用于 MIPS,使用 asm register locals。
  • 编译器似乎有点糊涂,但我不介意多加一条指令。 糊涂怎么办?设置 a0=0 可能是可变参数的 FP reg args 的数量。并且您在寄存器中要求"r"(syscall_n),因此编译器必须在您的 asm 语句中的 asm 之前将 li 放入某个寄存器中。您可以使用带有空 asm 语句的 asm 寄存器本地 var 将值强制转换为 a7,或者使用 "i" 约束作为 asm 中的 li 指令。当然,除非您从 asm 语句中调用,否则这些都不安全。
  • 嗯?您谈论它就像您对内存破坏器使用单独的 asm 语句,而不是将其附加到包含 syscall 指令的 asm 语句。 (或包含jal syscall_enter 指令但从内联asm 进行调用是一团糟)这可能会起作用,但为什么不直接告诉编译器到底发生了什么?如果你想做这样一个愚蠢的 hack,你可以在 asm 语句中包含一个 "memory" clobber,将 callnum 放入 a7。这可能碰巧是安全的,尤其是如果您在 syscall_enter() 之后也加上 asm("":::"memory"),IDK。
  • 是的,我认为唯一的失败模式是让编译器使用a7 做某事。正如您所说,希望在任何给定的呼叫站点以明显、易于调试的方式失败。除非它在一个很少使用的错误处理代码路径或其他东西中。这是一个内联函数,因此每个调用站点都有自己独立的失败可能性,具体取决于周围的代码。编译器可能不太可能想要对本地变量做这么多事情,以至于它最终想要在函数调用之前的 li / mem clobber 之后使用 a7

标签: c++ system-calls inline-assembly riscv


【解决方案1】:

这有两个问题。

首先是您没有告诉编译器您正在使用 a7,因此它可能会尝试将其他内容放在那里,从而导致代码不正确。您需要将 a7 添加到 asm 的 clobbers 列表中:

asm volatile ("mv a7, %0" : : "r"(syscall_n) : "a7");

第二个是 asm 语句没有连接到调用,所以编译器可能会重新排序,特别是在 asm mv 指令和调用之间移动其他代码。如果发生这种情况并且有问题的代码修改了 a7,那么您最终会调用错误的系统调用。

【讨论】:

    【解决方案2】:

    这是我现在正在使用的功能。非常感谢 @PeterCordes 提供的所有帮助。

    extern "C" long syscall_enter(...);
    
    template <typename... Args>
    inline long apicall(long syscall_n, Args&&... args)
    {
        // This will prevent some cases of too many arguments,
        // but not a mix of float and integral arguments.
        static_assert(sizeof...(args) < 8, "There is a system call limit of 8 integer arguments");
        // The memory clobbering prevents reordering of a7
        asm volatile ("li a7, %0" : : "i"(syscall_n) : "a7", "memory");
        return syscall_enter(std::forward<Args>(args)...);
        asm volatile("" : : : "memory");
    }
    

    它对我很有效。同样,避免使用 syscall-function-spam 解决方案的主要原因是,如果您有 2 个函数,其中一个采用整数参数,另一个采用浮点参数,那么函数调用将是模棱两可的,现在您需要开始考虑调用哪个函数。我已经混合使用浮点和整数参数测试了这个解决方案,它可以正常工作。一个缺点是它将浮点参数放入 64 位寄存器中,因此在系统调用期间会稍微慢一些。

    同样,有一个 C++ 解决方案!

    【讨论】:

    • syscall_enter 成为一个单独的非内联函数似乎违背了使用内联 asm 的大部分目的,但我猜这个自定义调用约定(在 a7 中传递 callnum 而不管其他参数的数量) 让该函数的定义比传统的包装函数更简单,传统的包装函数首先采用 callnum 并且必须将所有其他可能的 arg 复制一个。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-03-14
    • 2016-02-09
    • 2011-02-09
    • 1970-01-01
    • 2020-01-04
    • 2015-04-26
    相关资源
    最近更新 更多