当用户空间程序调用系统调用时,执行如何传回内核空间?

Naf*_*Kay 3 c assembly kernel linux-kernel

我一直在研究关于x86-64的ABI,编写汇编以及研究堆栈和堆如何工作的问题.

给出以下代码:

#include <linux/seccomp.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc, char *argv[]) {
    // execute the seccomp syscall (could be any syscall)
    seccomp(...);

    return 0;
}
Run Code Online (Sandbox Code Playgroud)

在Assembly for x86-64中,这将执行以下操作:

  1. 对齐堆栈指针(默认情况下,它关闭8个字节).
  2. 为调用的任何参数设置寄存器和堆栈seccomp.
  3. 执行以下程序集call seccomp.
  4. seccomp返回时,它可能是一,在C将调用exit(0)据我所知.

我想谈谈上面第三步和第四步之间发生的事情.

我目前拥有当前正在运行的进程的堆栈,它在寄存器和堆栈中有自己的数据.用户空间进程如何将执行转交给内核?内核只是在调用时接收,然后从同一堆栈中推送并弹出?

我相信我听说系统调用不会立即发生,而是在某些CPU滴答或中断时发生.这是真的?例如,在Linux上,这是怎么发生的?

Pet*_*des 7

系统调用不会立即发生,而是在某些CPU滴答或中断时发生

完全错了.在定时器中断之前,CPU不会在那里无所事事.在大多数架构上,包括x86-64,切换到内核模式需要几十到几百个周期,但不是因为CPU在等待任何事情.这只是一个缓慢的操作.


请注意,glibc几乎在每个系统调用周围提供函数包装器,所以如果你看一下反汇编,你就会看到一个看起来很正常的函数调用.


真正发生的事情(以x86-64为例):

请参阅从标记wiki 链接的AMD64 SysV ABI文档.它指定将args放入哪些寄存器,并使用该syscall指令进行系统调用.英特尔的insn参考手册(也从标签wiki链接)详细记录了每个syscall对CPU架构状态的变化.如果您对它的设计历史感兴趣,我会从AMD架构师和内核开发人员之间的amd64邮件列表中挖出一些有趣的邮件列表帖子.AMD在第一个AMD64硬件发布之前更新了这一行为,因此它实际上可用于Linux(和其他内核).

32位x86使用int 0x80系统调用指令,或sysenter. syscall在32位模式下不可用,并且sysenter在64位模式下不可用.您可以int 0x80使用64位代码运行,但仍然可以获得将指针视为32位的32位API.(即不要这样做).顺便说一句,也许你对系统调用不得不等待中断感到困惑int 0x80?运行该指令会在现场触发该中断,直接跳转到中断处理程序. 0x80也不是硬件可以触发的中断,因此中断处理程序只能在软件触发的系统调用之后运行.


AMD64系统调用示例:

#include <stdlib.h>
#include <unistd.h>
#include <linux/unistd.h>    // for __NR_write

const char msg[]="hello world!\n";

ssize_t amd64_write(int fd, const char*msg, size_t len) {
  ssize_t ret;
  asm volatile("syscall"  // volatile because we still need the side-effect of making the syscall even if the result is unused
               : "=a"(ret)                   // outputs
               : [callnum]"a"(__NR_write),   // inputs: syscall number in rax,
                "D" (fd), "S"(msg), "d"(len)    // and args, in same regs as the function calling convention
               : "rcx", "r11",               // clobbers: syscall always destroys rcx/r11, but Linux preserves all other regs
                 "memory"                    // "memory" to make sure any stores into buffers happen in program order relative to the syscall 
              );
}

int main(int argc, char *argv[]) {
    amd64_write(1, msg, sizeof(msg)-1);
    return 0;
}

int glibcwrite(int argc, char**argv) {
    write(1, msg, sizeof(msg)-1);  // don't write the trailing zero byte
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

用godbolt Compiler Explorer编译到这个asm输出:

gcc的-masm=intel输出有点像MASM,因为它使用OFFSETkeywork来获取标签的地址.

.rodata
msg:
        .string "hello world!\n"

.text
main:   // using an in-line syscall
        mov     eax, 1    # __NR_write
        mov     edx, 13   # string length
        mov     esi, OFFSET FLAT:msg      # string pointer
        mov     edi, eax  # file descriptor = 1 happens to be the same as __NR_write
        syscall
        xor     eax, eax  # zero the return value
        ret

glibcwrite:  // using the normal way that you get from compiler output
        sub     rsp, 8       // keep the stack 16B-aligned for the function call
        mov     edx, 13      // put args in registers
        mov     esi, OFFSET FLAT:msg
        mov     edi, 1
        call    write
        xor     eax, eax
        add     rsp, 8
        ret
Run Code Online (Sandbox Code Playgroud)

glibc的write包装函数只需将1放入eax并运行syscall,然后检查返回值并设置errno.还处理EINTR和东西上的重启系统调用.

// objdump -R -Mintel -d /lib/x86_64-linux-gnu/libc.so.6
...
00000000000f7480 <__write>:
   f7480:       83 3d f9 27 2d 00 00    cmp    DWORD PTR [rip+0x2d27f9],0x0        # 3c9c80 <argp_program_version_hook+0x1f8>
   f7487:       75 10                   jne    f7499 <__write+0x19>
   f7489:       b8 01 00 00 00          mov    eax,0x1
   f748e:       0f 05                   syscall
   f7490:       48 3d 01 f0 ff ff       cmp    rax,0xfffffffffffff001   // I think that's -EINTR
   f7496:       73 31                   jae    f74c9 <__write+0x49>
   f7498:       c3                      ret
   ... more code to handle cases where one of those branches was taken
Run Code Online (Sandbox Code Playgroud)


Bri*_*ain 5

系统调用不会立即发生,而是在某些CPU滴答或中断时发生

当然,你的系统调用的效果可能取决于很多东西,包括滴答声.调度程序粒度或时间分辨率可以限制为滴答周期,例如,但调用本身应该"立即"发生(内联执行).

用户空间进程如何将执行转交给内核?内核只是在调用时接收,然后从同一堆栈中推送并弹出?

它可能在架构之间略有不同,但通常syscall参数由汇编libc,然后生成处理器异常以更改上下文.

有关其他详细信息,请参阅:" 系统调用如何在x86 linux上运行 "