【问题标题】:Performing syscalls in inline C assembly causes a segfault在内联 C 程序集中执行系统调用会导致段错误
【发布时间】:2020-09-26 22:22:27
【问题描述】:

我最近涉足低级编程,并想创建一个接受(CType rax, CType rbx, CType rcx, CType rdx) 的函数somesyscall。 struct 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 $0x80"); //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

我得到一个段错误。然而,一切似乎都是正确的。我在这里做错了吗?

【问题讨论】:

  • 另请注意,32 位 int $0x80 API(在 64 位代码中实际上是 shouldn't use)在 eaxebxecxedx 中接受其参数,而不是 raxrbxrcxrdx。它不需要 64 位参数。

标签: c ubuntu assembly system-calls


【解决方案1】:

宏扩展后你会得到类似的东西

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 $0x80");
asm("mov %%rax, %0" : "=r" (raxret));

这不起作用,因为您没有告诉编译器在asm 语句的序列中不允许将raxrbxrcxrdx 重用于其他内容。例如,寄存器分配器可能决定将a2从堆栈复制到rax,然后使用rax作为mov %0, %%rcx指令的输入操作数——破坏你在rax中输入的值。

(没有输出的asm语句是implicitly volatile,所以前5个不能相对于彼此重新排序,但最后一个可以移动到任何地方。例如,在后面的代码之后移动到编译器认为方便的地方在其选择的寄存器中生成raxret。RAX 可能不再具有系统调用返回值 - 您需要告诉编译器输出来自实际生成它的 asm 语句,而不假设任何寄存器在两者之​​间存在asm 语句。)

有两种不同的方法可以告诉编译器不要这样做:

  1. int 指令放入 asm 中,并用约束字母表达对寄存器中内容的所有要求:

    asm volatile ("int $0x80" 
        : "=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
    
  2. 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 $0x80 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 $0x80" 
            : "+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,那么 you should be using syscall rather than int $0x80,并且参数属于 ABI 标准传入参数寄存器(按顺序为 rdi、rsi、rdx、rcx、r8、r9),而不是在 rbx、rcx、rdx 等中。不过,系统调用号仍然在 rax 中。 (使用来自#include &lt;asm/unistd.h&gt;&lt;sys/syscall.h&gt; 的电话号码,这将适用于您正在编译的模式的本机ABI,另一个原因不在64 位模式下使用int $0x80。)

另外,系统调用指令的 asm 语句应该有一个“内存”破坏器并声明为volatile;几乎所有系统调用都以某种方式访问​​内存。

(作为一个微优化,我想你可以有一个系统调用列表读取内存、写入内存或修改虚拟地址空间,并避免内存破坏它们。这将是一个非常短的列表,我不确定它是否值得麻烦。或者使用How can I indicate that the memory *pointed* to by an inline ASM argument may be used? 中显示的语法告诉 GCC 可以读取或写入哪些内存,而不是 "memory" clobber,如果您为特定的系统调用编写包装器。

一些无指针情况包括getpid,其中call into the VDSO 会更快,以避免往返内核模式和返回,就像glibc 对适当的系统调用所做的那样。这也适用于clock_gettime,它确实需要指针。)


顺便提一下,请注意实际的内核接口与 C 库包装器提供的接口不匹配。这通常记录在手册页的 NOTES 部分,例如对于brk(2)getpriority(2)

【讨论】:

  • 感谢您提供指向为什么在 64 位代码中使用 syscall 的规范解释的链接,@fuz。
  • @Tim 和 zwol:我将 asm 语句编辑为实际上是“安全的”,以便在 64 位代码中使用 32 位 int 0x80 ABI,并使用正确的clobbers。我认为重要的是不要在 SO 上有危险的内联 asm 示例,这些示例可能看起来有效,但缺少clobbers 或其他约束。结果真的很笨重而且看起来很奇怪,我建议要么重写 64 位 syscall ABI 以便它可以支持 64 位指针,要么将 var 名称更改为 EAX 等,并指出这应该是仅用于 32 位代码。但这会更具侵入性,所以我没有这样做。
  • 就目前而言,这些内联 asm 语句基本上显示了在 64 位代码中不使用 int 0x80 的部分原因,基本上没有真正的用例。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-06-15
  • 1970-01-01
  • 2011-02-26
  • 2010-10-29
相关资源
最近更新 更多