我一直在研究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;
}

在x86-64的程序集中,这将执行以下操作:
对齐堆栈指针(因为默认情况下它关闭了8个字节)。
为调用seccomp的任何参数设置寄存器和堆栈。
执行以下程序集call seccomp
seccomp返回时,据我所知,c可能会调用exit(0)
我想谈谈上面第三步到第四步之间发生了什么。
我目前有我的堆栈为当前正在运行的进程与它自己的数据在寄存器和堆栈上。用户空间进程如何将执行移交给内核?内核是在调用时才开始接收,然后从同一个堆栈中推送和弹出的吗?
我相信我在某个地方听说系统调用不是立即发生的,而是在某些CPU滴答声或中断时。这是真的吗?例如,在Linux上,这是如何发生的?

最佳答案

系统调用不会立即发生,但会在某些CPU计时或中断时发生
完全错了。CPU不只是坐在那里无所事事,直到计时器中断。在大多数架构(包括x86-64)上,切换到内核模式需要数十到数百个周期,但不是因为cpu在等待任何东西。只是一个缓慢的操作。
注意,glibc几乎在每个系统调用周围都提供了函数包装器,所以如果您查看反汇编,您将看到一个外观正常的函数调用。
实际情况(以x86-64为例):
请参阅从x86标记wiki链接的amd64 sysv abi文档。它指定要将参数放入哪个寄存器,并使用syscall指令进行系统调用。英特尔的insn ref手册(也链接到tag wiki)详细记录了syscall对cpu体系结构状态所做的每一个更改。如果您对它的设计历史感兴趣,可以从amd64邮件列表中的amd架构师和内核开发人员之间联系。AMD在第一个AMD64硬件发布之前更新了行为。
32位x86使用int 0x80指令进行系统调用,或sysentersyscall在32位模式下不可用,而sysenter在64位模式下不可用。您可以在64位代码中运行int 0x80,但仍然可以获得将指针视为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;
}

I dug up some interesting mailing list posts
gcc的-masm=intel输出有点像masm,因为它使用OFFSET键来获取标签的地址。
.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

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

08-04 15:56