术语俗话 --- 用户态与内核态
一、一个比喻秒懂
text
把电脑想象成一家餐厅:
🍽️ 用餐区(用户态)
│ 顾客(普通程序)在这里活动
│ 可以点菜、吃饭、聊天
│ 但不能进厨房!
│
├── 想要菜?→ 只能通过服务员(系统调用)传话
│
🔒 厨房(内核态)
厨师(操作系统内核)在这里工作
掌控所有食材(硬件资源)
刀具火源(危险操作)都在这里
顾客绝对不能直接进来
二、到底是什么
用户态(User Mode)
text
就是「普通程序运行时的状态」
你打开的所有软件都在用户态:
📱 微信 → 用户态
🎮 游戏 → 用户态
🌐 浏览器 → 用户态
📝 记事本 → 用户态
特点:权限低,很多事"不让干"
内核态(Kernel Mode)
text
就是「操作系统核心运行时的状态」
操作系统管家婆在这里干活:
💾 读写硬盘
📡 收发网络数据
🖥️ 控制屏幕显示
🧠 管理内存分配
特点:权限最高,什么都能干
三、为什么要分两个态?
如果不分会怎样?
text
❌ 不分用户态和内核态的世界:
任何程序都能直接操作硬件
流氓软件:我要直接读硬盘! → 偷你所有文件
恶意程序:我要改内存! → 篡改其他程序
bug程序:我写错地址了! → 整个系统崩溃💥
结果:一个烂程序就能搞垮整台电脑
分开之后
text
✅ 分了用户态和内核态的世界:
普通程序(用户态):
"我想读文件"
↓
操作系统(内核态)检查:
"你有权限吗?路径合法吗?"
↓ 检查通过
"好的,我帮你读,结果给你"
恶意程序(用户态):
"我想读别人的密码文件"
↓
操作系统(内核态):
"❌ 没权限,拒绝!"
一句话:分开是为了安全和稳定。
四、它们怎么配合工作
text
举例:你的程序想保存一个文件
你的程序(用户态) 操作系统(内核态)
│ │
│ 1⃣ 正常运行自己的代码 │
│ a = 1 + 1 │
│ str = "hello" │
│ 这些不需要内核帮忙 ✅ │
│ │
│ 2⃣ 我要写文件了! │
│ 我自己没权限操作硬盘... │
│ │
│──── 系统调用 write() ─────────────→│
│ (敲厨房的窗口) │ 3⃣ 收到请求
│ │ 检查权限 ✅
│ ⚡ 此时CPU切换到内核态 ⚡ │ 操作硬盘
│ │ 写入数据
│←──── 返回结果:"写好了" ────────────│
│ (菜做好端出来) │
│ ⚡ CPU切回用户态 ⚡ │
│ │
│ 4⃣ 继续执行自己的代码 │
│ print("保存成功") │
│ │
五、系统调用 = 两个态之间的桥梁
text
系统调用就是「用户态请求内核态帮忙的唯一通道」
常见的系统调用:
程序想做的事 系统调用 内核帮你干的事
───────── ────────── ─────────────
读文件 read() 操作硬盘读数据
写文件 write() 操作硬盘写数据
发网络请求 send() 操作网卡发数据
申请内存 mmap() 分配物理内存
启动新程序 execve() 加载程序到内存
获取时间 gettimeofday() 读硬件时钟
六、CPU是怎么区分的
text
CPU芯片里有一个"特权级别"标志位:
┌─────────────────────────┐
│ CPU │
│ │
│ Ring 0 (内核态) │ ← 最高权限,什么指令都能执行
│ ┌──────────────────┐ │
│ │ Ring 3(用户态) │ │ ← 受限权限,危险指令会报错
│ └──────────────────┘ │
│ │
└─────────────────────────┘
用户态程序执行特权指令时:
mov cr3, eax ← 试图修改页表(内核才能干)
↓
CPU:你是Ring 3,没资格!💥 触发异常
↓
操作系统接管处理
七、一个完整的生活类比
text
你(程序)住在一个小区里
┌────────────────────────────────────────┐
│ 小区(计算机) │
│ │
│ 🏠 你家(用户态-你的进程) │
│ - 你在家里随便干什么都行 │
│ - 做饭、看电视、写作业 │
│ - 这些不需要物业帮忙 │
│ │
│ 🏠 邻居家(用户态-别的进程) │
│ - 你进不去邻居家(进程隔离) │
│ - 各过各的,互不干扰 │
│ │
│ 🏢 物业办公室(内核态) │
│ - 管水电(硬件资源) │
│ - 管门禁(安全控制) │
│ - 管快递收发(网络I/O) │
│ - 管公共设施(共享资源) │
│ │
│ 📞 物业电话(系统调用) │
│ - 水管坏了?打电话给物业 │
│ - 你不能自己去挖主水管! │
│ │
└────────────────────────────────────────┘
八、什么时候会从用户态切到内核态
text
三种情况会切换:
1⃣ 系统调用(主动的)
程序主动请求:我要读文件 / 发网络包
类比:你主动打电话给物业
2⃣ 异常(被动的)
程序出错了:除以0 / 访问非法地址
类比:你家水管爆了,物业自动来处理
3⃣ 中断(外部的)
硬件来消息了:键盘按下 / 网卡收到数据
类比:有快递到了,物业通知你来取
九、态切换的代价
text
切换一次大约需要 1~10 微秒
用户态 ──切换──→ 内核态 ──切换──→ 用户态
保存现场 恢复现场
切换权限 切换权限
这个过程有开销!所以:
❌ 坏的写法:一个字节一个字节地写文件
write(fd, "a", 1) ← 切一次
write(fd, "b", 1) ← 又切一次
write(fd, "c", 1) ← 又切一次... 太慢了!
✅ 好的写法:攒一批再写
write(fd, "abc...很多数据", 1000) ← 只切一次
十、一句话总结
text
┌─────────────────────────────────────────────────┐
│ │
│ 用户态 = 普通程序活动的地方,权限低,不能碰硬件 │
│ 内核态 = 操作系统干活的地方,权限高,掌控一切 │
│ 系统调用 = 用户态请内核态帮忙的唯一合法通道 │
│ │
│ 为什么要分?→ 保护系统安全稳定,防止程序乱搞 │
│ │
└────────────────────────────
系统调用(System Call)
一句话定义
系统调用是操作系统内核向用户程序提供的一组标准接口,是用户态程序请求内核态服务的唯一合法入口。
为什么需要系统调用?
text
┌─────────────────────────────────┐
│ 用户态 (User Mode) │ ← 权限受限,不能直接访问硬件
│ 应用程序 (App / Library) │
├────────────── ↕ ────────────────┤ ← 系统调用接口(唯一的"门")
│ 内核态 (Kernel Mode) │ ← 拥有全部权限
│ 进程管理 / 内存管理 / 文件系统 │
│ 设备驱动 / 网络协议栈 │
└─────────────────────────────────┘
核心原因:安全与隔离
- CPU 将运行状态分为 用户态 和 内核态
- 用户程序运行在用户态,不能直接操作硬件、访问内核数据
- 必须通过系统调用 "陷入"(trap) 内核,由内核代为完成特权操作
系统调用的完整执行流程
text
用户程序 内核
│
│ 1. 调用 C 库封装函数(如 write())
▼
│ 2. 将系统调用号放入寄存器(如 eax = 1)
│ 将参数放入约定的寄存器/栈中
▼
│ 3. 执行特殊指令触发陷入
│ x86: int 0x80 / syscall
│ ARM: svc #0
▼
├──────────── 用户态 → 内核态切换 ────────────►│
│ │ 4. 保存用户态上下文
│ │ 5. 根据系统调用号查表(sys_call_table)
│ │ 6. 执行对应的内核函数
│ │ 7. 将返回值放入寄存器
│◄──────────── 内核态 → 用户态切换 ────────────┤
│
│ 8. C 库检查返回值,设置 errno
▼
│ 9. 返回给用户程序
常见系统调用分类
| 类别 | 典型系统调用 | 说明 |
|---|---|---|
| 进程控制 | fork(), exec(), exit(), wait() |
创建/终止/等待进程 |
| 文件操作 | open(), read(), write(), close() |
文件的打开、读写、关闭 |
| 设备管理 | ioctl(), mmap() |
设备控制、内存映射 |
| 内存管理 | brk(), mmap(), munmap() |
堆扩展、内存映射 |
| 网络通信 | socket(), bind(), send(), recv() |
网络套接字操作 |
| 信息维护 | getpid(), time(), uname() |
获取系统/进程信息 |
代码示例
用 C 库封装(日常使用)
C
#include <unistd.h>
int main() {
// write() 是 C 库对 sys_write 系统调用的封装
write(1, "Hello\n", 6); // fd=1 → 标准输出
return 0;
}
直接调用(Linux x86-64 汇编)
asm
; sys_write 的系统调用号 = 1
mov rax, 1 ; 系统调用号
mov rdi, 1 ; 参数1: fd = stdout
lea rsi, [msg] ; 参数2: 缓冲区地址
mov rdx, 6 ; 参数3: 字节数
syscall ; 触发系统调用
; sys_exit
mov rax, 60 ; exit 的系统调用号
xor rdi, rdi ; 退出码 0
syscall
系统调用 vs 普通函数调用
| 对比项 | 普通函数调用 | 系统调用 |
|---|---|---|
| 执行空间 | 用户态 → 用户态 | 用户态 → 内核态 → 用户态 |
| 切换开销 | 极小(几ns) | 较大(需保存/恢复上下文,约几百ns) |
| 触发方式 | call 指令 |
syscall / int 0x80 / svc 等特权指令 |
| 权限 | 无特权提升 | 临时获得内核权限 |
| 安全检查 | 无 | 内核会验证参数合法性 |
关键要点总结
- 系统调用是用户程序与内核之间的桥梁,保证了安全隔离
- 每个系统调用有唯一编号,通过系统调用表(
sys_call_table)分发 - 开销比函数调用大,因此存在
vDSO等优化机制(如gettimeofday可不陷入内核) - 日常编程中,我们通常通过 C 标准库(glibc)间接使用系统调用,而不是直接写汇编
- Linux 内核约有 400+ 个系统调用(可通过
man syscalls查看)
─────────────────────┘
免责声明
本文档所有内容仅供安全研究、学术交流与技术学习使用,严禁用于任何未经授权的逆向破解、网络攻击、隐私窃取、恶意软件开发及其他违反《中华人民共和国网络安全法》《数据安全法》等法律法规的行为,使用者应确保已获得目标软件权利人的合法授权并自行承担因使用本文档内容所产生的一切法律责任与后果,作者不对任何直接或间接损害承担任何责任,继续阅读即视为您已知悉并同意上述全部条款。
浙公网安备 33010602011771号