一行代码的千里之行

当你写下 write(1, "Hello, World!\n", 14) 这行代码时,你实际上正在发起一场跨越两个世界的旅行。这行代码将穿越CPU的特权级边界,从你熟悉的用户空间进入神秘的内核空间,完成一次完整的系统调用。

系统调用是操作系统最基础的概念之一,但它的实现细节却鲜为人知。为什么一个简单的函数调用需要几百纳秒?为什么KPTI补丁能让某些工作负载性能下降800%?vDSO是如何做到"不进入内核的系统调用"的?这篇文章将带你深入理解系统调用从用户态到内核态的完整旅程。

特权级的物理边界

理解系统调用,首先要理解现代CPU的特权级机制。x86架构定义了四个特权级别(Ring 0-3),但Linux只使用其中的两个:Ring 0用于内核,Ring 3用于用户程序。

这种分离不是软件层面的抽象,而是硬件强制执行的物理边界。当Ring 3的代码试图执行特权指令(如修改页表、访问I/O端口)时,CPU会触发异常。系统调用正是这种边界跨越的合法通道。

硬件层面的特权级检查发生在多个地方:

  • 页表项的U/S位:标识页面属于用户还是内核
  • 段描述符的DPL字段:定义访问该段所需的最低特权级
  • I/O权限位图:控制对特定I/O端口的访问权限

当系统调用发生时,CPU必须完成从Ring 3到Ring 0的转换。这个转换过程涉及栈指针切换、页表切换、以及最重要的——代码执行权限的提升。

系统调用的历史演进

系统调用的实现方式经历了三次重大演进,每一次都带来了显著的性能提升。

int 0x80:软件中断时代

最早的Linux系统调用通过软件中断实现。用户程序执行 int 0x80 指令,触发128号中断,CPU通过中断描述符表(IDT)找到内核入口点。

; 32位系统调用示例
mov eax, 4      ; sys_write的系统调用号
mov ebx, 1      ; 文件描述符 stdout
mov ecx, msg    ; 缓冲区地址
mov edx, 14     ; 长度
int 0x80        ; 触发系统调用

这种方式的开销主要来自中断处理:CPU需要保存完整的上下文、查询IDT、执行权限检查。在早期处理器上,一次 int 0x80 系统调用大约需要1000-1500个时钟周期。

sysenter/sysexit:快速系统调用

Intel在Pentium Pro处理器中引入了 sysentersysexit 指令,专门用于加速系统调用。这些指令绕过了中断处理的大部分开销,直接跳转到预定义的内核入口点。

sysenter 需要内核预先设置三个MSR寄存器:

  • IA32_SYSENTER_CS:内核代码段选择子
  • IA32_SYSENTER_ESP:内核栈指针
  • IA32_SYSENTER_EIP:内核入口点地址

syscall/sysret:现代标准

AMD在x86-64架构中引入了 syscallsysret 指令,这是目前Linux x86_64系统的标准系统调用方式。syscall 指令的设计比 sysenter 更加简洁高效:

  • 不需要显式加载栈指针
  • 自动保存返回地址到RCX寄存器
  • 自动保存RFLAGS到R11寄存器
  • 直接从MSR寄存器加载目标地址

一条 syscall 指令只需要几十个时钟周期就能完成特权级转换,相比 int 0x80 有了数量级的提升。

SYSCALL指令的工作原理

syscall 指令的执行过程可以用以下伪代码描述:

RCX ← RIP          ; 保存返回地址
R11 ← RFLAGS       ; 保存标志寄存器
RIP ← IA32_LSTAR   ; 从MSR加载内核入口点

; 从IA32_STAR加载段选择子
CS ← IA32_STAR[47:32]
SS ← IA32_STAR[47:32] + 8

; 清除IA32_FMASK中指定的标志位
RFLAGS ← RFLAGS AND (NOT IA32_FMASK)

这里有几个关键点值得注意:

首先syscall 不保存栈指针。内核必须通过其他方式获取内核栈地址,Linux使用 swapgs 指令配合per-CPU变量来实现。

其次,段寄存器的加载有特殊规则。syscall 不从GDT/LDT加载描述符缓存,而是使用固定值。这意味着内核必须确保GDT中对应的描述符与这些固定值一致。

第三,RFLAGS的修改。syscall 会自动清除某些标志位(如中断标志IF),这是通过 IA32_FMASK MSR控制的。

MSR寄存器的完整配置

Linux内核在初始化时配置以下MSR寄存器来支持系统调用:

// arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
    // MSR_STAR: 存储内核和用户的CS选择子
    // [63:48] = 用户CS (用于sysret)
    // [47:32] = 内核CS (用于syscall)
    wrmsrl(MSR_STAR, ((u64)__USER32_CS)<<48 | ((u64)__KERNEL_CS)<<32);
    
    // MSR_LSTAR: 系统调用入口点
    wrmsrl(MSR_LSTAR, entry_SYSCALL_64);
    
    // MSR_CSTAR: 兼容模式系统调用入口
    wrmsrl(MSR_CSTAR, entry_SYSCALL_compat);
    
    // MSR_SYSCALL_MASK: 需要清除的标志位
    wrmsrl(MSR_SYSCALL_MASK,
           X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
           X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
}

MSR_LSTAR 是最关键的寄存器,它存储了 entry_SYSCALL_64 的地址。每当用户程序执行 syscall 指令,CPU就会跳转到这个地址开始执行内核代码。

内核入口点:entry_SYSCALL_64

entry_SYSCALL_64 是x86_64 Linux内核处理系统调用的汇编入口点,定义在 arch/x86/entry/entry_64.S。它的核心工作流程如下:

flowchart TD
    A[用户执行syscall指令] --> B[CPU跳转到entry_SYSCALL_64]
    B --> C[swapgs: 切换GS基址]
    C --> D[保存用户栈指针到rsp_scratch]
    D --> E[加载内核栈指针]
    E --> F[保存用户态上下文到栈]
    F --> G{检查系统调用号是否合法}
    G -->|合法| H[从sys_call_table查找处理函数]
    G -->|非法| I[返回-ENOSYS]
    H --> J[调用系统调用处理函数]
    J --> K[保存返回值到栈]
    K --> L[恢复用户态寄存器]
    L --> M[sysretq返回用户态]
    I --> L

第一阶段:切换栈和保存上下文

entry_SYSCALL_64:
    swapgs                      ; 交换GS基址
    movq    %rsp, PER_CPU_VAR(rsp_scratch)  ; 保存用户栈
    movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp  ; 加载内核栈
    
    /* 构造pt_regs结构 */
    pushq    $__USER_DS         ; SS
    pushq    PER_CPU_VAR(rsp_scratch)  ; RSP (用户栈)
    pushq    %r11               ; RFLAGS (保存在r11中)
    pushq    $__USER_CS         ; CS
    pushq    %rcx               ; RIP (返回地址在rcx中)
    
    /* 保存参数寄存器 */
    pushq    %rax               ; 系统调用号
    pushq    %rdi               ; 参数1
    pushq    %rsi               ; 参数2
    pushq    %rdx               ; 参数3
    pushq    %r12               ; 保存被调用者保存寄存器
    pushq    %r8                ; 参数5
    pushq    %r9                ; 参数6
    pushq    %r10               ; 参数4 (注意顺序)
    pushq    %r11               ; 已保存,但为对齐再压一次

swapgs 指令在这里扮演关键角色。在用户态,GS寄存器指向用户空间的TLS(线程局部存储);swapgs 将其与内核空间的per-CPU数据区指针交换。这样内核就能通过 PER_CPU_VAR 宏访问当前CPU的专用数据。

第二阶段:调用系统调用处理函数

    /* 检查系统调用号 */
    cmpq    $__NR_syscall_max, %rax
    ja      badsys             ; 超出范围则跳转
    
    /* 第四个参数从r10移到rcx */
    movq    %r10, %rcx
    
    /* 调用系统调用处理函数 */
    call    *sys_call_table(, %rax, 8)

注意这里有一个微妙的细节:系统调用的参数传递使用RDI、RSI、RDX、R10、R8、R9,而不是标准C调用约定的RDI、RSI、RDX、RCX、R8、R9。这是因为 syscall 指令会覆盖RCX(用于保存返回地址)。内核在调用实际的C函数之前,必须把R10的内容移动到RCX。

第三阶段:返回用户空间

    /* 保存返回值 */
    movq    %rax, RAX(%rsp)
    
    /* 恢复寄存器 */
    RESTORE_C_REGS_EXCEPT_RCX_R11
    
    /* 恢复RCX和R11 */
    movq    RIP(%rsp), %rcx     ; 恢复返回地址
    movq    EFLAGS(%rsp), %r11  ; 恢复标志寄存器
    movq    RSP(%rsp), %rsp     ; 恢复用户栈
    
    swapgs                      ; 切换回用户GS
    sysretq                     ; 返回用户态

sysretq 指令是 syscall 的逆操作:它从RCX恢复RIP,从R11恢复RFLAGS,然后切换回Ring 3继续执行用户代码。

系统调用表与参数传递

系统调用表是内核维护的一个函数指针数组,每个系统调用号对应一个处理函数:

// arch/x86/entry/syscall_64.c
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,  // 默认:未实现
    [0] = sys_read,
    [1] = sys_write,
    [2] = sys_open,
    [3] = sys_close,
    // ...
};

系统调用处理函数通过 SYSCALL_DEFINE 宏定义:

// fs/read_write.c
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count)
{
    struct fd f = fdget_pos(fd);
    ssize_t ret = -EBADF;
    if (f.file) {
        loff_t pos = file_pos_read(f.file);
        ret = vfs_write(f.file, buf, count, &pos);
        if (ret >= 0)
            file_pos_write(f.file, pos);
        fdput_pos(f);
    }
    return ret;
}

这个宏展开后会产生多个函数,其中最重要的是安全处理CVE-2009-0029漏洞的包装函数。注意到第二个参数的类型是 const char __user *,这个 __user 注解告诉sparse工具这是一个用户空间指针,需要特殊处理。

安全边界:copy_from_user和copy_to_user

内核不能直接解引用用户空间指针。用户程序可能传递一个无效地址、已释放的地址、甚至是内核地址。直接访问可能导致:

  1. 内核崩溃:访问无效地址触发内核Oops
  2. 安全漏洞:用户程序通过精心构造的地址访问内核内存

Linux提供了 copy_from_usercopy_to_user 函数来安全地跨边界复制数据:

// 安全的用户空间内存访问
unsigned long copy_from_user(void *to, const void __user *from, unsigned long n);
unsigned long copy_to_user(void __user *to, const void *from, unsigned long n);

这些函数的工作原理:

flowchart TD
    A[copy_from_user调用] --> B{access_ok检查}
    B -->|地址合法| C[开启页面异常处理]
    B -->|地址非法| D[返回未复制字节数]
    C --> E[__copy_from_user复制]
    E --> F{复制成功?}
    F -->|成功| G[返回0]
    F -->|页面异常| H[异常处理程序介入]
    H --> I[返回未复制字节数]

access_ok 宏检查地址是否在用户空间范围内:

#define access_ok(addr, size) \
    (__addr_ok(addr) && __addr_ok((addr) + (size)))

但真正的安全来自于异常处理机制。copy_from_user 使用 __get_user_asm 等宏,这些宏会在访问用户内存时设置异常表项。如果发生页面异常,内核会查找异常表,如果找到匹配项就返回错误值而不是崩溃。

返回值与错误处理

系统调用的错误处理有一个统一约定:

  • 成功:返回非负值(通常是结果或0)
  • 失败:返回负的错误码

在内核内部,系统调用返回 -EINVAL-ENOMEM 等负值。glibc包装器会将其转换为POSIX风格的返回:

// glibc包装器的典型实现
ssize_t write(int fd, const void *buf, size_t count) {
    ssize_t result = syscall(SYS_write, fd, buf, count);
    if (result < 0) {
        errno = -result;  // 错误码取反存入errno
        return -1;        // 返回-1表示错误
    }
    return result;
}

这就是为什么用户程序检查 if (write(...) < 0) 然后查看 errno 来确定具体错误。

性能代价:一次跨越的代价

系统调用不是免费的。让我们看看具体的性能数据:

操作类型 延迟(近似) 相对开销
普通函数调用 ~5.5 ns 1x
系统调用(无vDSO) ~250 ns ~45x
vDSO gettimeofday ~8 ns ~1.5x

这些数字来自实际的微基准测试。在现代CPU上,一次系统调用的开销大约是普通函数调用的40-50倍。

开销主要来自:

  1. 特权级切换:CPU流水线刷新、TLB可能的更新
  2. 上下文保存/恢复:保存用户寄存器、加载内核栈
  3. 内存屏障:确保内存操作的顺序一致性
  4. 安全检查:参数验证、权限检查

但这只是"直接开销"。间接开销可能更加显著。

KPTI:安全漏洞的代价

2018年初,Meltdown漏洞的披露彻底改变了系统调用的性能格局。KPTI(Kernel Page Table Isolation)补丁将内核页表与用户页表完全分离,导致每次系统调用都需要切换页表。

Brendan Gregg的测试结果揭示了惊人的性能影响:

xychart-beta
    title "KPTI对系统调用性能的影响"
    x-axis ["无KPTI", "KPTI(无PCID)", "KPTI+PCID", "KPTI+PCID+THP"]
    y-axis "相对性能损失" 0-->20
    bar [0, 8.7, 0.5, -3]

测试条件:100MB工作集,5k syscalls/sec/CPU

关键发现:

  • 最坏情况:KPTI可以导致超过800%的性能下降
  • PCID帮助很大:进程上下文标识符(PCID)允许在不刷新TLB的情况下切换页表
  • Huge Pages更佳:使用大页面减少TLB压力,甚至可能获得性能提升

这是因为系统调用导致的TLB刷新会清除所有的地址转换缓存。如果工作集较大,重建这些缓存需要大量内存访问。

vDSO:跨越边界的免费午餐

vDSO(virtual Dynamic Shared Object)是Linux的一个巧妙优化,它将某些"只读"的系统调用实现放在用户空间,完全避免了特权级切换。

vDSO的工作原理:

flowchart LR
    A[用户程序调用gettimeofday] --> B{查看vDSO映射}
    B --> C[直接读取内核写入的时间数据]
    C --> D[在用户态计算并返回]
    D --> E[无需syscall指令]

内核将一个只读内存页映射到每个进程的地址空间,这个页面包含:

  1. 时间数据(由内核周期性更新)
  2. 快速版本的系统调用实现
$ cat /proc/self/maps
7fffcbcb7000-7fffcbcb8000 r-xp 00000000 00:00 0    [vdso]

目前vDSO支持的系统调用包括:

  • gettimeofday
  • time
  • clock_gettime
  • clock_getres
  • getcpu

对于这些调用,glibc会自动使用vDSO版本。性能测试显示,vDSO版本的 gettimeofday 延迟仅为8纳秒,接近普通函数调用。

vDSO vs vsyscall

vDSO的前身是vsyscall,它有一个致命的安全缺陷:固定地址。vsyscall总是映射在 0xffffffffff600000,这使得攻击者可以利用它作为跳板执行任意系统调用。

// vsyscall的固定地址(已废弃)
#define VSYSCALL_ADDR 0xffffffffff600000

vDSO采用地址随机化(ASLR),每次加载地址都不同,从根本上解决了这个问题。现代Linux内核甚至可以用陷阱指令替换vsyscall的内容,让老程序继续工作但强制进入内核处理。

io_uring:重新思考系统调用

io_uring是Linux 5.1引入的异步I/O接口,它采用了一种全新的思路:通过共享内存队列批量提交请求,最小化系统调用次数。

flowchart TD
    A[用户程序] --> B[写入提交队列SQE]
    B --> C[提交队列 tail指针+1]
    C --> D{需要立即处理?}
    D -->|是| E[执行syscall提交]
    D -->|否| F[延迟批量提交]
    E --> G[内核处理SQE]
    F --> G
    G --> H[写入完成队列CQE]
    H --> I[用户程序轮询结果]

io_uring的核心是两个环形缓冲区:

  1. 提交队列(SQ):用户写入I/O请求
  2. 完成队列(CQ):内核写入完成结果

使用io_uring,程序可以:

// 提交100个读请求,只需要一次系统调用
for (int i = 0; i < 100; i++) {
    struct io_uring_sqe *sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, fd, bufs[i], sizes[i], offsets[i]);
}
io_uring_submit(&ring);  // 一次系统调用提交所有请求

// 轮询完成结果,甚至不需要系统调用
for (int i = 0; i < 100; i++) {
    struct io_uring_cqe *cqe;
    io_uring_wait_cqe(&ring, &cqe);  // 可选:等待完成
    // 处理结果...
}

在高I/O密集的场景下,io_uring可以将系统调用次数减少一个数量级,带来显著的性能提升。

seccomp:过滤系统调用

seccomp(Secure Computing Mode)是Linux的安全机制,允许进程限制自己能执行的系统调用。这在沙箱和容器安全中至关重要。

seccomp有两种模式:

严格模式

只允许4个系统调用:readwrite_exitsigreturn。其他任何系统调用都会导致进程被杀死。

prctl(PR_SET_SECCOMP, SECCOMP_MODE_STRICT);

过滤模式

使用BPF(Berkeley Packet Filter)程序定义复杂的过滤规则:

struct sock_filter filter[] = {
    /* 只允许read, write, exit */
    BPF_STMT(BPF_LD+BPF_W+BPF_ABS, offsetof(struct seccomp_data, nr)),
    BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_read, 0, 1),
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP+BPF_JEQ+BPF_K, __NR_exit, 0, 1),
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_ALLOW),
    BPF_STMT(BPF_RET+BPF_K, SECCOMP_RET_KILL),
};

Docker和Kubernetes使用seccomp配置文件来限制容器的系统调用:

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "architectures": ["SCMP_ARCH_X86_64"],
    "syscalls": [
        {
            "names": ["read", "write", "exit"],
            "action": "SCMP_ACT_ALLOW"
        }
    ]
}

seccomp过滤在系统调用入口时执行,在内核实际处理系统调用之前。这增加了一定的开销,但对于安全性来说是值得的。

监控与调试:strace和ptrace

strace 是调试系统调用的常用工具,它使用 ptrace 系统调用实现:

sequenceDiagram
    participant T as 被追踪进程
    participant K as 内核
    participant S as strace
    
    T->>K: 执行syscall指令
    K->>K: 检测到ptrace附加
    K->>S: SIGTRAP信号
    S->>K: PTRACE_GETREGS读取参数
    K->>S: 返回寄存器值
    S->>S: 打印系统调用信息
    S->>K: PTRACE_SYSCALL继续执行
    K->>T: 执行系统调用
    T->>K: 系统调用返回
    K->>S: SIGTRAP信号
    S->>K: PTRACE_GETREGS读取返回值
    K->>S: 返回寄存器值
    S->>S: 打印返回值
    S->>K: PTRACE_SYSCALL继续

ptrace 的工作原理:

  1. 父进程调用 ptrace(PTRACE_TRACEME) 或被 strace 附加
  2. 每次系统调用前后,内核都会暂停进程并通知tracer
  3. Tracer可以读取和修改寄存器、内存
  4. Tracer可以决定是否允许系统调用继续

这种机制的开销是巨大的——每个系统调用都会触发多次上下文切换。这就是为什么在生产环境不应该使用strace。

/proc文件系统:实时查看

Linux提供了 /proc/[pid]/syscall 来查看进程当前正在执行的系统调用:

$ cat /proc/1/syscall
232 0x4 0x7ffdf82e11b0 0x1f 0xffffffff 0x100 0x7ffdf82e11bf 0x7ffdf82e11a0 0x7f9114681193

第一个数字是系统调用号(232 = epoll_wait),后面是参数寄存器的值。

完整的系统调用流程

让我们把所有内容整合起来,看看一个完整的系统调用旅程:

flowchart TD
    subgraph 用户空间
        A[应用程序调用write] --> B[glibc包装函数]
        B --> C[设置RAX=系统调用号]
        C --> D[设置RDI/RSI/RDX/R10/R8/R9=参数]
        D --> E[执行syscall指令]
    end
    
    subgraph 硬件
        E --> F[CPU保存RCX=RIP, R11=RFLAGS]
        F --> G[CPU从MSR_LSTAR加载RIP]
        G --> H[CPU切换到Ring 0]
    end
    
    subgraph 内核空间
        H --> I[entry_SYSCALL_64]
        I --> J[swapgs切换GS基址]
        J --> K[保存用户寄存器到栈]
        K --> L[检查系统调用号]
        L --> M[从sys_call_table查找函数]
        M --> N[调用sys_write]
        N --> O[vfs_write处理]
        O --> P[返回值存入RAX]
        P --> Q[恢复用户寄存器]
        Q --> R[swapgs恢复用户GS]
        R --> S[sysretq返回]
    end
    
    subgraph 硬件返回
        S --> T[CPU恢复RIP=RCX, RFLAGS=R11]
        T --> U[CPU切换回Ring 3]
    end
    
    subgraph 用户空间返回
        U --> V[glibc检查RAX]
        V --> W{RAX < 0?}
        W -->|是| X[设置errno, 返回-1]
        W -->|否| Y[直接返回RAX]
    end

总结:边界跨越的艺术

系统调用是操作系统设计中最精妙的部分之一。它不仅是一个技术机制,更是用户空间和内核空间、自由与控制、性能与安全之间平衡的体现。

从硬件角度看,系统调用依赖于CPU的特权级机制和专门的指令。从软件角度看,它是内核向用户程序提供服务的统一接口。从安全角度看,它是需要仔细过滤和监控的攻击面。从性能角度看,它是需要尽可能优化的热点路径。

理解系统调用的完整旅程,有助于我们:

  1. 写出更高效的程序——减少不必要的系统调用,利用vDSO和io_uring
  2. 更好地调试问题——知道strace能看到什么、不能看到什么
  3. 更深入地理解操作系统——从抽象概念到具体实现

当你下次写下 write(fd, buf, len) 时,你会知道这行代码背后发生的一切:CPU的特权级跳转、内核栈的切换、参数的安全复制、系统调用表的查找、以及最终的返回。这是一次跨越两个世界的旅程,而这一切,只需要一条指令。


参考文献

  1. Intel Corporation. Intel 64 and IA-32 Architectures Software Developer’s Manual, Volume 2B. SYSCALL instruction reference.

  2. AMD. AMD64 Architecture Programmer’s Manual, Volume 2: System Programming. System call mechanism details.

  3. Torvalds, L. Linux kernel source code, arch/x86/entry/entry_64.S. https://github.com/torvalds/linux/blob/master/arch/x86/entry/entry_64.S

  4. Torvalds, L. Linux kernel source code, arch/x86/kernel/cpu/common.c. syscall_init function.

  5. Gregg, B. “KPTI/KAISER Meltdown Performance Overhead.” Brendan Gregg’s Blog, February 2018. https://www.brendangregg.com/blog/2018-02-09/kpti-kaiser-meltdown-performance.html

  6. “On vsyscalls and the vDSO.” LWN.net, June 2011. https://lwn.net/Articles/446528/

  7. 0xAX. “Introduction to system calls.” Linux Insides. https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-1.html

  8. 0xAX. “How does the Linux kernel handle a system call.” Linux Insides. https://0xax.gitbooks.io/linux-insides/content/SysCall/linux-syscall-2.html

  9. “Measurements of system call performance and overhead.” Arkanis Development, January 2017. https://arkanis.de/weblog/2017-01-05-measurements-of-system-call-performance-and-overhead/

  10. “io_uring(7) - Linux manual page.” man7.org. https://man7.org/linux/man-pages/man7/io_uring.7.html

  11. “Seccomp BPF (SECure COMPuting with filters).” Linux Kernel Documentation. https://www.kernel.org/doc/html/v4.16/userspace-api/seccomp_filter.html

  12. “seccomp(2) - Linux manual page.” man7.org. https://man7.org/linux/man-pages/man2/seccomp.2.html

  13. “strace(1) - Linux manual page.” man7.org. https://man7.org/linux/man-pages/man1/strace.1.html

  14. “User space memory access from the Linux kernel.” IBM Developer. https://developer.ibm.com/articles/l-kernel-memory-access/

  15. “The Definitive Guide to Linux System Calls.” Packagecloud Blog. https://blog.packagecloud.io/the-definitive-guide-to-linux-system-calls/

  16. “Why do you have to use copy_to_user()/copy_from_user() to access user space from kernel?” Stack Overflow. https://stackoverflow.com/questions/12666493/

  17. “Why return a negative errno?” Stack Overflow. https://stackoverflow.com/questions/1848729/

  18. “x86 calling conventions.” Wikipedia. https://en.wikipedia.org/wiki/X86_calling_conventions

  19. “AMD64 Linux Kernel Conventions.” UCW. https://www.ucw.cz/~hubicka/papers/abi/node33.html

  20. “Adding a New System Call.” Linux Kernel Documentation. https://www.kernel.org/doc/html/v4.17/process/adding-syscalls.html

  21. “Intercepting and modifying Linux system calls with ptrace.” Phil Eaton’s Blog. https://notes.eatonphil.com/2023-10-01-intercepting-and-modifying-linux-system-calls-with-ptrace.html

  22. “Intercepting and Emulating Linux System Calls with Ptrace.” null program. https://nullprogram.com/blog/2018/06/23/

  23. “Why you should use io_uring for network I/O.” Red Hat Developer. https://developers.redhat.com/articles/2023/04/12/why-you-should-use-iouring-network-io

  24. “Configure Linux system auditing with auditd.” Red Hat. https://www.redhat.com/en/blog/configure-linux-auditing-auditd

  25. “Glibc wrappers for (nearly all) Linux system calls.” LWN.net. https://lwn.net/Articles/655028/

  26. “Time To Get Rid Of errno.” LWN.net. https://lwn.net/Articles/655134/

  27. “Linux System Calls, Error Numbers, and In-Band Signaling.” null program. https://nullprogram.com/blog/2016/09/23/

  28. “How much do function calls impact performance?” Software Engineering Stack Exchange. https://softwareengineering.stackexchange.com/questions/318055/

  29. “Do system calls in x86_64 linux still generate interrupts?” Server Fault. https://serverfault.com/questions/879875/

  30. “The difference between entry_SYSCALL64_slow_path and entry_SYSCALL64_fast_path.” Stack Overflow. https://stackoverflow.com/questions/61840459/

  31. “System V Application Binary Interface.” AMD. https://gitlab.com/x86-psABIs/x86-64-ABI

  32. “errno(3) - Linux manual page.” man7.org. https://man7.org/linux/man-pages/man3/errno.3.html

  33. “audit.rules(7) - Linux manual page.” man7.org. https://man7.org/linux/man-pages/man7/audit.rules.7.html

  34. “Understanding Linux Audit.” SUSE Documentation. https://documentation.suse.com/sles/12-SP5/html/SLES-all/cha-audit-comp.html

  35. “Seccomp-bpf and Its Performance Impact.” Zatoichi’s Engineering Blog. https://zatoichi-engineer.github.io/2017/11/06/seccomp-bpf.html

  36. “Learning About Syscall Filtering With Seccomp.” Ben Burwell. https://www.benburwell.com/posts/learning-about-syscall-filtering-with-seccomp/

  37. “Glibc and System Calls Documentation.” https://media.readthedocs.org/pdf/sys/latest/sys.pdf

  38. “How to wrap a system call (libc function) in Linux.” Saman Barghi. http://samanbarghi.com/post/2014-09-05-how-to-wrap-a-system-call-libc-function-in-linux/

  39. “Measuring syscall overhead in Linux.” Stack Overflow. https://stackoverflow.com/questions/62884891/

  40. “io_uring: Linux Performance Boost or Security Headache?” Upwind. https://www.upwind.io/feed/io_uring-linux-performance-boost-or-security-headache

  41. “The Secret Weapon for Performance Boosting — Asynchronous Operations in the Kernel.” Medium. https://vicxu.medium.com/the-secret-weapon-for-performance-boosting-asynchronous-operations-in-the-kernel-io-uring-6345c81be936

  42. “Linux kernel entry_64.txt documentation.” https://www.kernel.org/doc/Documentation/x86/entry_64.txt