【发布时间】: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