在内联 C 汇编中执行系统调用会导致段错误

Ank*_*zle 1 c ubuntu assembly system-calls

我最近涉足低级编程,并希望制作一个somesyscall接受(CType rax, CType rbx, CType rcx, CType rdx). struct CType 看起来像:

/*
    TYPES:
        0 int
        1 string
        2 bool
*/
typedef struct {
    void* val;
    int typev;
} CType;
Run Code Online (Sandbox Code Playgroud)

该功能有点混乱,但理论上应该可以工作:

#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;
}
Run Code Online (Sandbox Code Playgroud)

当我运行时:

#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));
}
Run Code Online (Sandbox Code Playgroud)

并编译(并运行二进制)

clang test.c
./a.out
Run Code Online (Sandbox Code Playgroud)

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

zwo*_*wol 5

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

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));
Run Code Online (Sandbox Code Playgroud)

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

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

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

  1. int在一个汇编指令,并表示所有的在用什么约束字母注册所发生的要求:

    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".
     );
    
    Run Code Online (Sandbox Code Playgroud)

    对于这个简单的示例,这是可能的,但通常并不总是可能的,因为每个寄存器都没有约束字母,尤其是在 x86 以外的 CPU 上。

         // use the native 64-bit syscall ABI
         // remove the r8..r11 clobbers for 32-bit mode
    
    Run Code Online (Sandbox Code Playgroud)
  2. int在一个汇编指令,并表达了在什么与什么注册那张要求明确寄存器变量

     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;
    
    Run Code Online (Sandbox Code Playgroud)

    无论您需要使用哪些寄存器,这都将起作用。它也可能与您尝试用于擦除类型的宏更兼容。

顺便说一句,如果这是 64 位 x86/Linux,那么您应该使用syscall而不是int $0x80,并且参数属于 ABI 标准传入参数寄存器(按顺序是 rdi、rsi、rdx、rcx、r8、r9),而不是在 rbx、rcx、rdx 等中。不过,系统调用号仍然在 rax 中。(使用#include <asm/unistd.h>或 的电话号码<sys/syscall.h>,这将适用于您正在编译的模式的本机 ABI,这是不在int $0x8064 位模式下使用的另一个原因。)

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

(作为一个微优化,我想你可以有一个读取内存、写入内存或修改虚拟地址空间的系统调用列表,并避免它们的内存破坏。这将是一个非常短的列表和我不确定这是否值得麻烦。或者使用如何指示可以使用内联 ASM 参数 * 指向 * 的内存? 中显示的语法来告诉 GCC 可以读取或写入哪个内存,而不是上"memory"撞,如果你编写特定的系统调用包装。

一些无指针情况包括调用 VDSO以避免往返内核模式和返回的getpid速度要快得多,就像 glibc 对适当的系统调用所做的那样。这也适用于which 确实需要指针。)clock_gettime


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