在内联 C 汇编中执行系统调用会导致段错误
Performing syscalls in inline C assembly causes a segfault
我最近涉足低级编程,想制作一个接受 (CType rax, CType rbx, CType rcx, CType rdx)
的函数 somesyscall
。结构 CType 看起来像:
/*
TYPES:
0 int
1 string
2 bool
*/
typedef struct {
void* val;
int typev;
} CType;
功能有点乱,但理论上应该可以:
#include <errno.h>
#include <stdbool.h>
#include "ctypes.h"
//define functions to set registers
#define seteax(val) asm("mov %0, %%rax" :: "g" (val) : "%rax")
#define setebx(val) asm("mov %0, %%rbx" :: "g" (val) : "%rbx")
#define setecx(val) asm("mov %0, %%rcx" :: "g" (val) : "%rcx")
#define setedx(val) asm("mov %0, %%rdx" :: "g" (val) : "%rdx")
///////////////////////////////////
#define setregister(value, register) \
switch (value.typev) { \
case 0: { \
register(*((double*)value.val)); \
break; \
} \
case 1: { \
register(*((char**)value.val)); \
break; \
} \
case 2: { \
register(*((bool*)value.val)); \
break; \
} \
}
static inline long int somesyscall(CType a0, CType a1, CType a2, CType a3) {
//set the registers
setregister(a0, seteax);
setregister(a1, setebx);
setregister(a2, setecx);
setregister(a3, setedx);
///////////////////
asm("int [=11=]x80"); //interrupt
//fetch back the rax
long int raxret;
asm("mov %%rax, %0" : "=r" (raxret));
return raxret;
}
当我 运行 与:
#include "syscall_unix.h"
int main() {
CType rax;
rax.val = 39;
rax.typev = 0;
CType rbx;
rbx.val = 0;
rbx.typev = 0;
CType rcx;
rcx.val = 0;
rcx.typev = 0;
CType rdx;
rdx.val = 0;
rdx.typev = 0;
printf("%ld", somesyscall(rax, rbx, rcx, rdx));
}
并使用
编译(和运行二进制文件)
clang test.c
./a.out
我遇到段错误。然而,一切似乎都是正确的。我在这里做错了什么吗?
宏展开后你会得到类似
的东西
long int raxret;
asm("mov %0, %%rax" :: "g" (a0) : "%rax");
asm("mov %0, %%rbx" :: "g" (a1) : "%rbx");
asm("mov %0, %%rcx" :: "g" (a2) : "%rcx");
asm("mov %0, %%rdx" :: "g" (a3) : "%rdx");
asm("int [=10=]x80");
asm("mov %%rax, %0" : "=r" (raxret));
这不起作用,因为您没有告诉编译器不允许将 rax
、rbx
、rcx
和 rdx
重用于某些东西else 在 asm
语句的序列中。例如,寄存器分配器可能决定将 a2
从堆栈复制到 rax
,然后使用 rax
作为 mov %0, %%rcx
指令的输入操作数——破坏你的值输入 rax
.
(没有输出的 asm 语句是 implicitly volatile
所以前 5 个不能相对于彼此重新排序,但最后一个可以移动到任何地方。例如,在后面的代码之后移动到编译器所在的位置发现在其选择的寄存器中生成 raxret
很方便。此时 RAX 可能不再具有系统调用 return 值 - 您需要告诉编译器输出来自 asm 语句实际上产生它,而不假设任何寄存器在 asm 语句之间存在。)
有两种不同的方法可以告诉编译器不要这样做:
将仅 int
指令放入asm中,并用约束字母表达对什么进入什么寄存器的所有要求:
asm volatile ("int [=11=]x80"
: "=a" (raxret) // outputs
: "a" (a0), "b" (a1), "c" (a2), "d" (a3) // pure inputs
: "memory", "r8", "r9", "r10", "r11" // clobbers
// 32-bit int 0x80 system calls in 64-bit code zero R8..R11
// for native "syscall", clobber "rcx", "r11".
);
对于这个简单的例子来说这是可行的,但通常并不总是可行,因为没有针对每个寄存器的约束字母,尤其是在 x86 以外的 CPU 上。
// use the native 64-bit syscall ABI
// remove the r8..r11 clobbers for 32-bit mode
只把int
指令放在一个asm中,用explicit register variables表达出什么进什么寄存器的要求:
register long rax asm("rax") = a0;
register long rbx asm("rbx") = a1;
register long rcx asm("rcx") = a2;
register long rdx asm("rdx") = r3;
// Note that int [=13=]x80 only looks at the low 32 bits of input regs
// so `uint32_t` would be more appropriate than long
// but really you should just use "syscall" in 64-bit code.
asm volatile ("int [=13=]x80"
: "+r" (rax) // read-write: in=call num, out=retval
: "r" (rbx), "r" (rcx), "r" (rdx) // read-only inputs
: "memory", "r8", "r9", "r10", "r11"
);
return rax;
无论您需要使用哪个寄存器,这都会起作用。它也可能与您试图用来擦除类型的宏更兼容。
顺便说一句,如果这是 64 位 x86/Linux,则 ,参数属于 ABI-standard incoming-argument 寄存器(rdi、rsi、rdx、rcx , r8, r9 按此顺序),而不是在 rbx、rcx、rdx 等中。不过,系统调用号仍然在 rax 中。 (使用来自 #include <asm/unistd.h>
或 <sys/syscall.h>
的调用编号,这将适用于您正在编译的模式的本机 ABI,这是不在 64 位模式下使用 int [=29=]x80
的另一个原因。 )
此外,system-call 指令的 asm 语句应该有一个“内存”破坏并被声明为 volatile
;几乎所有系统调用都以某种方式访问内存。
(作为 micro-optimization,我想你可以有一个系统调用列表,不读取内存、写入内存或修改虚拟地址space,并避免它们的内存破坏。这将是一个非常短的列表,我不确定是否值得麻烦。或者使用 中显示的语法告诉 GCC 哪个内存可能被读取或写入,而不是 "memory"
破坏,如果你为特定的系统调用编写包装器。
一些 no-pointer 案例包括 getpid
,其中 call into the VDSO 会更快,以避免往返内核模式和返回,就像 glibc 为适当的系统调用。这也适用于确实需要指针的 clock_gettime
。)
顺便提一下,请注意实际的内核接口与 C 库包装器提供的接口不匹配。这通常记录在手册页的注释部分中,例如对于 brk(2)
and getpriority(2)
我最近涉足低级编程,想制作一个接受 (CType rax, CType rbx, CType rcx, CType rdx)
的函数 somesyscall
。结构 CType 看起来像:
/*
TYPES:
0 int
1 string
2 bool
*/
typedef struct {
void* val;
int typev;
} CType;
功能有点乱,但理论上应该可以:
#include <errno.h>
#include <stdbool.h>
#include "ctypes.h"
//define functions to set registers
#define seteax(val) asm("mov %0, %%rax" :: "g" (val) : "%rax")
#define setebx(val) asm("mov %0, %%rbx" :: "g" (val) : "%rbx")
#define setecx(val) asm("mov %0, %%rcx" :: "g" (val) : "%rcx")
#define setedx(val) asm("mov %0, %%rdx" :: "g" (val) : "%rdx")
///////////////////////////////////
#define setregister(value, register) \
switch (value.typev) { \
case 0: { \
register(*((double*)value.val)); \
break; \
} \
case 1: { \
register(*((char**)value.val)); \
break; \
} \
case 2: { \
register(*((bool*)value.val)); \
break; \
} \
}
static inline long int somesyscall(CType a0, CType a1, CType a2, CType a3) {
//set the registers
setregister(a0, seteax);
setregister(a1, setebx);
setregister(a2, setecx);
setregister(a3, setedx);
///////////////////
asm("int [=11=]x80"); //interrupt
//fetch back the rax
long int raxret;
asm("mov %%rax, %0" : "=r" (raxret));
return raxret;
}
当我 运行 与:
#include "syscall_unix.h"
int main() {
CType rax;
rax.val = 39;
rax.typev = 0;
CType rbx;
rbx.val = 0;
rbx.typev = 0;
CType rcx;
rcx.val = 0;
rcx.typev = 0;
CType rdx;
rdx.val = 0;
rdx.typev = 0;
printf("%ld", somesyscall(rax, rbx, rcx, rdx));
}
并使用
编译(和运行二进制文件)clang test.c
./a.out
我遇到段错误。然而,一切似乎都是正确的。我在这里做错了什么吗?
宏展开后你会得到类似
的东西long int raxret;
asm("mov %0, %%rax" :: "g" (a0) : "%rax");
asm("mov %0, %%rbx" :: "g" (a1) : "%rbx");
asm("mov %0, %%rcx" :: "g" (a2) : "%rcx");
asm("mov %0, %%rdx" :: "g" (a3) : "%rdx");
asm("int [=10=]x80");
asm("mov %%rax, %0" : "=r" (raxret));
这不起作用,因为您没有告诉编译器不允许将 rax
、rbx
、rcx
和 rdx
重用于某些东西else 在 asm
语句的序列中。例如,寄存器分配器可能决定将 a2
从堆栈复制到 rax
,然后使用 rax
作为 mov %0, %%rcx
指令的输入操作数——破坏你的值输入 rax
.
(没有输出的 asm 语句是 implicitly volatile
所以前 5 个不能相对于彼此重新排序,但最后一个可以移动到任何地方。例如,在后面的代码之后移动到编译器所在的位置发现在其选择的寄存器中生成 raxret
很方便。此时 RAX 可能不再具有系统调用 return 值 - 您需要告诉编译器输出来自 asm 语句实际上产生它,而不假设任何寄存器在 asm 语句之间存在。)
有两种不同的方法可以告诉编译器不要这样做:
将仅
int
指令放入asm中,并用约束字母表达对什么进入什么寄存器的所有要求:asm volatile ("int [=11=]x80" : "=a" (raxret) // outputs : "a" (a0), "b" (a1), "c" (a2), "d" (a3) // pure inputs : "memory", "r8", "r9", "r10", "r11" // clobbers // 32-bit int 0x80 system calls in 64-bit code zero R8..R11 // for native "syscall", clobber "rcx", "r11". );
对于这个简单的例子来说这是可行的,但通常并不总是可行,因为没有针对每个寄存器的约束字母,尤其是在 x86 以外的 CPU 上。
// use the native 64-bit syscall ABI // remove the r8..r11 clobbers for 32-bit mode
只把
int
指令放在一个asm中,用explicit register variables表达出什么进什么寄存器的要求:register long rax asm("rax") = a0; register long rbx asm("rbx") = a1; register long rcx asm("rcx") = a2; register long rdx asm("rdx") = r3; // Note that int [=13=]x80 only looks at the low 32 bits of input regs // so `uint32_t` would be more appropriate than long // but really you should just use "syscall" in 64-bit code. asm volatile ("int [=13=]x80" : "+r" (rax) // read-write: in=call num, out=retval : "r" (rbx), "r" (rcx), "r" (rdx) // read-only inputs : "memory", "r8", "r9", "r10", "r11" ); return rax;
无论您需要使用哪个寄存器,这都会起作用。它也可能与您试图用来擦除类型的宏更兼容。
顺便说一句,如果这是 64 位 x86/Linux,则 #include <asm/unistd.h>
或 <sys/syscall.h>
的调用编号,这将适用于您正在编译的模式的本机 ABI,这是不在 64 位模式下使用 int [=29=]x80
的另一个原因。 )
此外,system-call 指令的 asm 语句应该有一个“内存”破坏并被声明为 volatile
;几乎所有系统调用都以某种方式访问内存。
(作为 micro-optimization,我想你可以有一个系统调用列表,不读取内存、写入内存或修改虚拟地址space,并避免它们的内存破坏。这将是一个非常短的列表,我不确定是否值得麻烦。或者使用 "memory"
破坏,如果你为特定的系统调用编写包装器。
一些 no-pointer 案例包括 getpid
,其中 call into the VDSO 会更快,以避免往返内核模式和返回,就像 glibc 为适当的系统调用。这也适用于确实需要指针的 clock_gettime
。)
顺便提一下,请注意实际的内核接口与 C 库包装器提供的接口不匹配。这通常记录在手册页的注释部分中,例如对于 brk(2)
and getpriority(2)