使用内存的 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 中的地址。所以你的函数中有更多代码以使其无法正常工作是件好事偶然,所以你可以在你的代码中找到错误。)
这个系统调用代码根本不起作用。编译器正在优化并通常表现得很奇怪:
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 中的地址。所以你的函数中有更多代码以使其无法正常工作是件好事偶然,所以你可以在你的代码中找到错误。)