用户态调用 read() 时,glibc 将参数装入寄存器并执行 syscall 指令,触发 CPU 从 ring 3 切至 ring 0,跳转到 entry_SYSCALL_64;内核根据 %rax 中的系统调用号(__NR_read=0)查表调用 sys_read,再经 ksys_read 转入文件系统 read 回调。

用户态调用 read() 时到底发生了什么
Linux 系统调用不是函数跳转,而是软中断触发的特权级切换。当你在 C 程序里写 read(fd, buf, size),实际调用的是 glibc 封装的 wrapper,它把参数准备好,把系统调用号(比如 __NR_read = 0)写入 %rax(x86-64),然后执行 syscall 指令——这才是真正进入内核的开关。
关键点在于:syscall 是 CPU 提供的指令,会强制从 ring 3(用户态)切到 ring 0(内核态),并跳转到内核预先注册的入口(entry_SYSCALL_64)。这个过程不经过任何中间层,也不依赖链接器或动态库,是硬件支持的原子操作。
- glibc 的
read()只负责参数搬运和syscall触发,不实现读逻辑 - 不同架构指令不同:x86-64 用
syscall,x86-32 用int $0x80,ARM64 用svc #0 - 如果直接汇编调用
syscall而不设对%rax,内核会返回-ENOSYS
系统调用号怎么对应到内核函数
调用号是内核 ABI 的一部分,硬编码在头文件里:/usr/include/asm/unistd_64.h 定义了 __NR_read 为 0,而内核源码中 arch/x86/entry/syscalls/syscall_table_64.c 的第 0 号表项指向 sys_read 函数指针。
这个映射表在内核启动时加载进内存,entry_SYSCALL_64 入口根据 %rax 查表跳转。注意:sys_read 不是最终实现,它只是入口 wrapper,真正干活的是 ksys_read 和底层文件系统的 file_operations.read 回调。
- 系统调用号不能随意改,否则用户程序调用会失败或跳错函数
- 新增系统调用需同时修改头文件、syscall table、内核函数,并重新编译内核和 glibc
- 部分调用(如
openat)有多个变体,靠%rax区分,但参数布局必须严格匹配内核期望
为什么不能直接 call 内核函数
用户代码无法直接 call sys_read,因为内核地址空间默认对用户态不可见(页表标记为 supervisor-only),且函数签名、调用约定(如寄存器使用、栈帧处理)与用户态 ABI 不兼容。
更根本的是安全隔离:CPU 在 ring 3 下执行任意 call 指令都无法访问 ring 0 的代码段,会触发 general protection fault(#GP)。只有 syscall/int 这类特权指令才能合法切换模式并跳转到内核指定入口。
- 即使通过
mmap映射了内核内存(如 /dev/kmem,通常已被禁用),也无法绕过段保护和 SMAP/SMEP 防护 - 内核函数没有稳定 ABI,
sys_read在不同内核版本中可能被重命名、拆分或合并 - 用户态调试器(如 gdb)看到的
read调用栈停在 glibc,不会显示内核函数名——因为内核栈与用户栈完全分离
strace 是怎么抓到系统调用的
strace 利用的是 ptrace 机制,本质是让内核在每次目标进程执行 syscall 指令前后暂停它,并把寄存器状态(%rax, %rdi, %rsi 等)暴露给 tracer 进程。它并不解析内核符号表,而是查内置的系统调用号映射表来翻译 %rax 值。
例如,看到 read(3, "hello\n", 1024) = 6,是因为 strace 在 syscall 返回后读取了 %rax(返回值)和前三个参数寄存器,再按 read 的语义格式化输出。
-
strace -e trace=read,write并非过滤内核路径,而是只监听特定调用号的进出事件 - 调用耗时统计基于内核提供的
ptrace时间戳,不包含用户态开销 - 如果程序用
syscall(SYS_read, ...)直接调用,strace依然能捕获,因为它监控的是syscall指令本身,而非 glibc 符号
真正难理解的不是“怎么进”,而是“进之后怎么安全地把用户地址(如 buf)转成内核可访问的物理页,又不被恶意篡改”。这部分涉及地址空间隔离、copy_from_user/copy_to_user 的检查逻辑,以及 page fault 时的异常处理路径——它们才是系统调用背后最厚重的屏障。










