了解 Linux x86_64 NASM 中的系统调用实现

Understanding Linux x86_64 Syscall Implementation in NASM

我正在使用 linux 内核作为我的 'OS'(这只是一个游戏)的基础。显然,我使用系统调用与内核交互,例如输入、声音、图形等。因为我在这个系统上没有链接器来使用 <sys/syscall.h> 中的预定义包装器,所以我在 NASM asm:[=16 中为 C 使用这个系统调用包装器=]

global _syscall
_syscall:
  mov rax, rdi 
  mov rdi, rsi
  mov rsi, rdx
  mov rdx, rcx
  mov r10, r8
  mov r8, r9
  mov r9, [rsp + 8]
  syscall
  ret

我了解基于 System V 调用约定使用的寄存器。但是,我不明白最后一行mov r9, [rsp + 8]。我觉得这与 return 地址有关,但我不确定。

第 7 个参数在 x86-64 系统 V 中传递到堆栈上。

这是获取第一个 C arg 并将其放入 RAX,然后将接下来的 6 个 C arg 复制到内核系统调用调用约定的 6 个 arg 传递寄存器。 (类似功能,但使用 R10 而不是 RCX)。


craptastic glibc syscall() 函数存在的唯一原因 / 以这种方式编写是因为没有办法告诉 C 编译器关于自定义调用约定,其中 arg也在RAX中传递。该包装器使它看起来就像任何其他带有原型的 C 函数。

搞乱新的系统调用没问题,但正如您所指出的那样效率低下。如果你想要 C 中更好的东西,请为你的 ISA 使用内联 asm 宏,例如https://github.com/linux-on-ibm-z/linux-syscall-support/blob/master/linux_syscall_support.h。内联 asm 很难,并且历史上一些 syscall1 / syscall2(每个参数的数量)宏已经丢失了诸如 "memory" clobber 之类的东西来告诉编译器指向内存也可能是输入或输出。 github 项目是安全的,并且有各种 ISA 的代码。 (一些遗漏的优化,比如可以使用虚拟输入操作数而不是完整的“内存”破坏......但这与 asm 无关)


当然,如果你用 asm 编写,你可以做得更好:

只需在正确的寄存器(RDI、RSI、RDX、R10、R8、R9)中直接使用带有 args 的 syscall 指令,而不是使用函数调用约定的 call _syscall。这比内联 syscall 指令更糟糕:使用 syscall 你知道除了 RAX(return 值)和 RCX/R11(系统调用本身使用它们来保存在内核代码运行之前进行 RIP 和 RFLAGS。)将 args 放入函数调用的寄存器所需的代码与 syscall.

一样多

如果你真的想要一个包装函数(例如之后到 cmp rax, -4095 / jae handle_syscall_error 并且可能设置 errno),使用与内核期望的相同的调用约定,所以第一条指令可以是 syscall,而不是所有那些愚蠢的 args 改组 1.

asm 中的函数(您只需要从 asm 中调用)can use whatever calling convention is convenient。大多数时候使用一个好的标准是个好主意,但任何“明显特殊”的函数当然可以使用特殊的约定。