第09节-用户态内核态和系统调用
第 9 节:用户态、内核态和系统调用
所属课程:操作系统自学路线:面向网络安全、逆向工程与漏洞分析
所属周次:第 5 周
课程主题:系统调用
本节目标:理解用户态、内核态和系统调用的基本概念,掌握普通程序为什么必须通过操作系统访问文件、网络、进程等资源,并能用strace观察 Linux 系统调用。
1. 本节课你要学会什么
学完这一节,你应该能回答下面几个问题:
- 什么是用户态?什么是内核态?
- 为什么普通程序不能直接访问硬件?
- CPU 特权级大致是什么?
- 系统调用是什么?
- API、库函数、系统调用有什么区别?
printf和write有什么关系?- Linux 下常见系统调用有哪些?
- Windows 下 Win32 API、Native API、系统调用大致是什么关系?
- 如何用
strace观察系统调用? - 为什么系统调用对逆向工程、漏洞分析、恶意代码分析很重要?
本节课的主线是:
1 | |
前面我们学习了进程如何创建,这节课开始理解进程如何请求操作系统做事。
2. 为什么需要用户态和内核态
操作系统要保护系统资源。
如果普通程序可以随便操作硬件,会出现严重问题。
例如普通程序如果可以直接:
- 修改硬盘任意扇区
- 读取其他进程内存
- 控制网卡发送任意数据
- 修改页表
- 禁用中断
- 改写内核代码
那么一个 bug 或恶意程序就可能破坏整个系统。
所以现代操作系统会区分不同权限级别。
最重要的两个概念是:
1 | |
3. 用户态是什么
用户态英文常写作:
1 | |
普通应用程序通常运行在用户态。
例如:
- 浏览器
- 编辑器
- 游戏
- 命令行程序
- Python 程序
- 你自己写的 C 程序
用户态程序权限受限。
它不能直接:
- 访问任意物理内存
- 执行特权指令
- 直接操作硬件设备
- 修改内核数据结构
- 任意读写其他进程内存
用户态程序想做这些事情,必须请求内核帮忙。
这个请求通道就是系统调用。
4. 内核态是什么
内核态英文常写作:
1 | |
操作系统内核运行在内核态。
内核态拥有更高权限,可以:
- 管理进程
- 管理内存
- 访问硬件
- 操作文件系统
- 处理网络协议栈
- 管理权限
- 调度 CPU
- 加载驱动
- 响应中断和异常
内核态代码非常关键。
如果内核崩溃,通常会影响整个系统。
例如:
- Linux kernel panic
- Windows 蓝屏 BSOD
所以内核代码必须非常谨慎。
5. CPU 特权级的初步概念
CPU 本身也支持权限等级。
在 x86/x86-64 架构中,常见说法是 Ring。
1 | |
还有 Ring 1、Ring 2,但现代主流操作系统通常主要使用 Ring 0 和 Ring 3。
简化理解:
| 层级 | 常见用途 | 权限 |
|---|---|---|
| Ring 0 | 操作系统内核、驱动 | 高 |
| Ring 3 | 普通用户程序 | 低 |
用户态到内核态不是普通函数调用。
它需要通过特定机制进行受控切换。
系统调用就是这种受控入口之一。
6. 为什么不能直接访问硬件
假设普通程序可以直接写磁盘。
那么一个错误程序可能执行:
1 | |
后果可能是系统无法启动。
假设普通程序可以直接访问其他进程内存。
那么它可以读取:
- 浏览器密码
- SSH 私钥
- 聊天记录
- 加密钱包数据
假设普通程序可以直接修改页表。
那么进程隔离就失效。
所以操作系统要求:
1 | |
7. 系统调用是什么
系统调用英文是:
1 | |
简称:
1 | |
它是用户态程序请求内核服务的接口。
例如用户程序想读文件:
1 | |
常见系统调用包括:
| 类别 | Linux 常见系统调用 |
|---|---|
| 进程 | fork、execve、wait4、exit |
| 文件 | openat、read、write、close |
| 内存 | mmap、munmap、brk、mprotect |
| 网络 | socket、connect、sendto、recvfrom |
| 权限 | setuid、setgid、chmod、chown |
| 信号 | rt_sigaction、kill |
| 时间 | nanosleep、clock_gettime |
8. 系统调用不是普通函数调用
普通函数调用发生在同一权限级别内。
例如:
1 | |
这通常是用户态内部的函数调用。
系统调用不同。
它需要从用户态切换到内核态。
大致过程:
1 | |
在 x86-64 Linux 中,常见进入内核的指令是:
1 | |
早期或其他架构中还可能见到:
1 | |
9. 系统调用号是什么
内核中有很多系统调用。
用户程序需要告诉内核:
1 | |
这通常通过系统调用号实现。
在 Linux x86-64 中,系统调用号通常放在:
1 | |
参数通常放在:
1 | |
注意这里第 4 个参数是 r10,不是普通函数调用中的 rcx。
例如 Linux x86-64 中:
1 | |
write(fd, buf, count) 大致需要:
1 | |
你现在不需要背所有系统调用号,只要知道系统调用通过编号进入内核分发。
10. API、库函数、系统调用的区别
这三个词很容易混淆。
10.1 库函数
库函数是库提供的普通函数。
例如 C 标准库中的:
1 | |
它们通常运行在用户态。
有些库函数内部会调用系统调用,有些不会。
例如:
strlen一般只是用户态计算字符串长度printf最终可能调用writemalloc可能调用brk或mmap
10.2 API
API 是应用程序编程接口。
它是更广泛的概念。
Windows 中常说 Win32 API,例如:
1 | |
Linux 中也可以说 POSIX API 或 libc API,例如:
1 | |
API 不一定等于系统调用。
API 可能是对系统调用的封装。
10.3 系统调用
系统调用是进入内核的真正接口。
例如 Linux 中:
1 | |
它们最终由内核处理。
总结:
1 | |
11. printf 背后发生了什么
看这段代码:
1 | |
从 C 语言视角看,只是调用 printf。
但底层可能是:
1 | |
注意:
printf 不一定每次立刻调用 write。
因为 C 标准库可能有缓冲。
例如输出到终端、文件、管道时,缓冲策略可能不同。
但最终要把内容输出到终端或文件,通常需要系统调用。
12. write 是更接近系统调用的接口
看这个程序:
1 | |
这里调用的是:
1 | |
write 是 POSIX API,同时在 Linux 下通常是系统调用封装。
参数:
1 | |
含义:
| 参数 | 含义 |
|---|---|
| fd | 文件描述符 |
| buf | 要写出的缓冲区地址 |
| count | 写出多少字节 |
其中:
1 | |
常见标准文件描述符:
| fd | 含义 |
|---|---|
| 0 | 标准输入 stdin |
| 1 | 标准输出 stdout |
| 2 | 标准错误 stderr |
13. 用 strace 观察系统调用
strace 可以跟踪 Linux 程序执行过程中的系统调用。
创建 write_demo.c:
1 | |
编译:
1 | |
运行:
1 | |
跟踪:
1 | |
你会看到很多输出。
重点找:
1 | |
意思是:
1 | |
14. 为什么 strace 输出很多内容
你可能会疑惑:
1 | |
原因是程序启动和退出也需要很多系统调用。
例如:
- 加载动态库
- 设置内存映射
- 初始化运行时
- 查询系统信息
- 写输出
- 退出进程
你可能看到:
1 | |
这些不一定是你手写代码直接调用的,但运行时和动态链接器可能会触发。
这也说明:
1 | |
15. 过滤 strace 输出
如果只想看某类系统调用,可以过滤。
只看写操作:
1 | |
只看文件相关:
1 | |
只看进程相关:
1 | |
跟踪子进程:
1 | |
显示时间:
1 | |
这些选项在后面分析进程行为、文件行为、网络行为时很常用。
16. read 系统调用示例
创建 read_demo.c:
1 | |
编译运行:
1 | |
输入一行文字,它会原样输出。
用 strace:
1 | |
观察:
1 | |
这说明:
1 | |
17. 文件读写系统调用示例
创建 file_syscall_demo.c:
1 | |
编译运行:
1 | |
你可能看到:
1 | |
注意:
你写的是 open,但 strace 可能显示 openat。
这是因为现代 libc 可能用 openat 系统调用实现 open。
这再次说明:
1 | |
18. 系统调用返回值和错误码
系统调用通常会返回结果。
例如:
1 | |
表示成功写入 14 字节。
如果失败,可能返回:
1 | |
并设置错误码 errno。
例如打开不存在文件失败:
1 | |
常见错误:
| 错误 | 含义 |
|---|---|
| ENOENT | 文件或目录不存在 |
| EACCES | 权限不足 |
| EPERM | 操作不允许 |
| EBADF | 文件描述符无效 |
| EFAULT | 地址无效 |
| EINVAL | 参数无效 |
错误码对漏洞分析也很有用。
例如某个系统调用返回 EACCES,说明权限检查失败。
19. 系统调用和权限检查
系统调用进入内核后,内核通常会做权限检查。
例如打开文件时,内核会检查:
- 文件是否存在
- 当前用户是否有权限
- 路径是否可访问
- 文件系统是否允许写入
- 是否违反安全策略
所以用户程序即使调用:
1 | |
如果权限不足,内核也会拒绝。
这说明:
1 | |
很多漏洞的关键,就是内核或高权限程序在处理系统调用/API 参数时出现错误。
20. 用户态到内核态的数据传递
系统调用经常需要用户程序传指针给内核。
例如:
1 | |
这里 buf 是用户态地址。
内核要从这个地址读取数据。
内核不能盲目信任用户传来的指针。
因为用户可能传:
- 空指针
- 非法地址
- 没有权限访问的地址
- 指向被并发修改的内存
- 指向内核地址的伪造指针
所以内核必须谨慎检查用户态指针。
这在内核漏洞中非常重要。
很多驱动漏洞的根源就是:
1 | |
21. Windows 中的 API 层次
Windows 下常见调用层次可以简化为:
1 | |
例如应用程序调用:
1 | |
它可能经过:
1 | |
简化理解:
| 层次 | 示例 |
|---|---|
| Win32 API | CreateFileW、ReadFile、WriteFile |
| Native API | NtCreateFile、NtReadFile、NtWriteFile |
| 系统调用入口 | ntdll 中的 syscall stub |
| 内核处理 | Windows 内核函数 |
Windows 逆向和恶意代码分析中,经常会看到这些层次。
22. Windows API 和系统调用不是一回事
很多初学者会把 Windows API 和系统调用混为一谈。
例如:
1 | |
严格来说,CreateFileW 是 Win32 API。
它是用户态 DLL 提供的函数。
它底层可能调用 Native API,最终进入内核系统调用。
所以:
1 | |
恶意代码分析中,经常以 API 行为描述程序:
1 | |
这足够表达行为,不一定每次都要追到 syscall 层。
23. 为什么恶意代码关注 API 和系统调用
恶意代码要做事,也必须请求系统资源。
例如:
- 写文件
- 修改注册表
- 创建进程
- 连接网络
- 分配内存
- 修改内存权限
- 创建线程
- 读取敏感信息
这些行为通常会通过 API 或系统调用体现出来。
安全软件、沙箱、EDR 常常监控:
- API 调用序列
- 系统调用序列
- 参数
- 调用上下文
- 进程树
- 文件路径
- 网络目标
例如行为链:
1 | |
可能表示:
1 | |
24. 直接系统调用的基本概念
有些程序可能不通过常规 API,而是直接执行系统调用。
在 Windows 恶意代码分析中,有时会提到:
1 | |
它的目的可能是绕过某些用户态 API Hook。
但对入门阶段来说,你只需要知道:
1 | |
本课程强调防御和分析视角。
你需要理解这种行为为什么可疑,以及如何从动态行为和内核层日志中分析它。
25. 系统调用和逆向工程
逆向时,系统调用/API 是理解程序行为的重要线索。
如果你看到一个 Linux 程序调用:
1 | |
它可能在读写文件。
看到:
1 | |
它可能有网络行为。
看到:
1 | |
它可能创建子进程。
看到:
1 | |
它可能在管理内存权限。
系统调用比普通计算指令更能体现程序和操作系统的交互。
26. 系统调用和漏洞分析
漏洞分析中,系统调用很重要。
原因包括:
- 系统调用是用户态进入内核态的边界
- 内核必须解析用户传入的参数
- 权限检查发生在系统调用路径上
- 文件、网络、进程、内存操作都通过系统调用体现
- 沙箱逃逸、内核漏洞、驱动漏洞都和边界处理有关
例如:
1 | |
这就是很多内核/驱动漏洞的基本思路。
在用户态漏洞中,系统调用也能帮助判断程序行为。
例如崩溃前是否:
- 打开了特定文件
- 连接了网络
- mmap 了可执行内存
- 创建了子进程
27. 系统调用和沙箱分析
沙箱常通过系统调用或 API 记录程序行为。
例如报告中可能写:
1 | |
这些行为底层都对应系统调用或 API。
所以如果你懂系统调用,就更容易看懂沙箱报告。
你不会只看到“创建文件”四个字,而会想到:
1 | |
28. Linux 实验:printf 和 write 对比
创建 printf_demo.c:
1 | |
创建 write_demo.c:
1 | |
编译:
1 | |
分别观察:
1 | |
思考:
- 两个程序是否都出现
write? printf是库函数还是系统调用?write更接近哪一层?
29. Linux 实验:观察文件系统调用
创建 open_read_demo.c:
1 | |
编译并跟踪:
1 | |
观察:
- 打开了哪个文件
- 返回的文件描述符是多少
- 读取了多少字节
- 写到了哪个 fd
- 是否成功关闭文件
30. Linux 实验:观察错误码
创建 error_demo.c:
1 | |
编译并跟踪:
1 | |
你可能看到:
1 | |
这说明系统调用失败,原因是文件不存在。
31. Windows 实验:Process Monitor 观察 API 行为
在 Windows 下:
- 打开 Process Monitor
- 清空事件
- 设置过滤器:
1 | |
- 启动 notepad
- 保存一个文本文件
- 观察事件
重点看:
- CreateFile
- WriteFile
- CloseFile
- QueryInformationFile
这些是 Process Monitor 以 Windows 行为形式展示的文件操作。
它们不一定直接等于系统调用名,但能反映程序与操作系统交互。
32. Windows 实验:x64dbg 观察 API 调用
用 x64dbg 打开一个简单 Windows 程序。
可以尝试在导入函数上下断点,例如:
1 | |
观察:
- 调用前参数寄存器
rcx、rdx、r8、r9 - 调用后
rax返回值 - 栈上是否有额外参数
- API 调用后程序行为
Windows x64 前四个参数通常在:
1 | |
这和前面汇编课程中讲的调用约定对应起来。
33. 本节重点总结
你需要记住这些核心结论:
- 用户态是普通程序运行的低权限状态。
- 内核态是操作系统内核运行的高权限状态。
- 普通程序不能直接访问硬件和关键系统资源。
- 系统调用是用户态请求内核服务的受控入口。
- 系统调用会导致用户态到内核态的切换。
- API、库函数和系统调用不是完全一样的概念。
printf是库函数,最终可能通过write系统调用输出。strace可以观察 Linux 程序的系统调用。- Windows 中常见层次是 Win32 API -> Native API -> syscall -> 内核。
- 系统调用/API 是逆向工程、漏洞分析和恶意代码行为分析的重要线索。
34. 本节课后作业
作业 1:观察 write 系统调用
写 write_demo.c:
1 | |
执行:
1 | |
提交内容:
1 | |
作业 2:对比 printf 和 write
写 printf_demo.c 和 write_demo.c。
执行:
1 | |
提交内容:
1 | |
作业 3:观察文件读写系统调用
写 open_read_demo.c。
执行:
1 | |
提交内容:
1 | |
作业 4:观察系统调用错误码
写 error_demo.c,打开一个不存在的文件。
执行:
1 | |
提交内容:
1 | |
作业 5:Windows API 行为观察
如果你有 Windows 环境:
- 用 Process Monitor 观察 notepad 保存文件
- 过滤 notepad.exe
- 记录 CreateFile / WriteFile / CloseFile 事件
- 用 x64dbg 观察一个简单程序的 API 调用
提交内容:
1 | |
35. 自测题
题 1
用户态和内核态有什么区别?
题 2
为什么普通程序不能直接访问硬件?
题 3
系统调用是什么?
题 4
printf、write、syscall 三者有什么层次关系?
题 5
Linux 下 strace 用来做什么?
题 6
为什么 open 在 strace 中可能显示为 openat?
题 7
Windows 下 Win32 API 和 Native API 大致是什么关系?
题 8
为什么系统调用对恶意代码分析重要?
题 9
为什么内核不能盲目信任用户态传入的指针?
36. 自测题参考答案
答 1
用户态是普通应用程序运行的低权限状态,不能直接访问硬件和内核资源;内核态是操作系统内核运行的高权限状态,可以管理硬件、内存、进程、文件系统和网络等资源。
答 2
为了稳定性、安全性和权限控制。如果普通程序能直接访问硬件或任意内存,一个 bug 或恶意程序就可能破坏系统、泄露数据或绕过权限隔离。
答 3
系统调用是用户态程序请求内核服务的受控接口。程序通过系统调用进入内核态,由内核完成文件、进程、内存、网络等操作并返回结果。
答 4
printf 是 C 标准库函数,运行在用户态;它最终可能调用 write 这类更接近系统调用的接口;真正进入内核时会通过系统调用机制完成用户态到内核态的切换。
答 5
strace 用于跟踪 Linux 程序执行过程中的系统调用,显示系统调用名称、参数、返回值和错误码。
答 6
因为 open 是用户态 API,现代 libc 可能用 openat 系统调用实现它。所以源代码调用 open,底层系统调用可能显示为 openat。
答 7
Win32 API 是应用程序常用的高层 Windows API;Native API / NTAPI 是更底层的用户态接口,通常位于 ntdll.dll,最终通过 syscall stub 进入 Windows 内核。
答 8
因为恶意代码要进行文件、网络、进程、内存、注册表等操作,通常都会通过 API 或系统调用体现出来。监控这些调用及其参数可以帮助判断程序行为。
答 9
因为用户态指针可能为空、非法、不可访问、指向恶意构造的数据,或在检查后被修改。内核如果盲目信任这些指针,可能导致崩溃、信息泄露或权限提升漏洞。
37. 下一节预告
下一节课会讲:
1 | |
你会学习:
- 库函数、API、系统调用之间的调用链
- Linux libc 与 syscall wrapper
- Windows kernel32.dll、KernelBase.dll、ntdll.dll 的关系
- 为什么恶意代码行为常被描述为 API 序列
- 如何用
ltrace、strace、Process Monitor、x64dbg 对照分析程序行为