3.1 从单纯的"计算机"开始
一个厨师的厨房
想象一下——你有一个私人厨房。没有别人。灶台是你的,刀具是你的,冰箱里那瓶藏了好久的酱也是你的。你切菜、开火、翻炒,整个空间里唯一的变量就是你自己的操作节奏。没有人和你抢水槽,没有人挪走你刚洗好的锅,更不会有人偷吃你切好的菜。
香,实在是太香了。
早期的计算机就是这样——一台机器,一个用户,跑一个程序。从 ENIAC 到那些早期的个人计算机,"计算"这件事是高度私有的。你坐上去,CPU 从你启动到你关机,全程只为你一个人服务。内存里只有你的指令和数据,不存在"别人的代码偷偷读走你的数据"这种问题——因为根本没有"别人"。
这个阶段的安全模型简单到令人感动:物理安全就是全部。谁能坐到这台机器前面,谁就能用它。不需要进程隔离,不需要内存保护,甚至连操作系统都是一个简单的循环(executive loop),程序跑完就结束,干净利落。
但这种"单纯"……很快就结束了。
厨房里来了第二个厨师
好,现在问题来了。
你正用着你的私人厨房做着菜,突然有人跟你说——"不好意思,从今天起这个厨房是共享的,还有一个厨师要跟你一起用。"
???
几个厨师同时用同一个灶台、同一个冰箱、同一把菜刀。A 厨师正要颠勺的时候,B 厨师伸手过来拿你的油壶。C 厨师找不到自己的碗,顺手把你的碗拿走了。你切好的菜放在案板上,回头一看——B 厨师在上面剁了他的排骨。
乱不乱?乱死了。
有人可能说:"没事,大家讲好规矩就行了嘛。"——但问题是,总有人不自觉,或者总有人"不小心"。而更重要的是,在没有管理员的情况下,一个厨师根本没办法阻止另一个厨师动他的东西。

(自如合租做得到吗???)
好,这就是 资源共享带来的冲突。
所以——为什么我没感觉到"乱"?
看到这里你可能会说:"不对啊,我电脑上同时开着浏览器、写文档、挂QQ、后台还在编译代码,也没见它们打架啊?它们不都在一个CPU上跑吗?"
对,这是个好问题。让我们仔细想想——
你的电脑有几个CPU(或者说几个核心)?
(低头检查中……)
可能是4核心、8核心、16核心。但你同时运行的程序呢?光浏览器可能就开了几十个标签页,再加上IDE、聊天软件、音乐播放器、后台服务……随便数数都几十上百个。
核心数(4、8、16)远小于同时运行的程序数(几十上百个)。 那它们是怎么"同时"工作的?
其实别说是多核时代了,回到更早的计算机时代,这个问题更明显:
1950年代,一台计算机价值数百万美元,运行起来像一间房子那么大。程序员要把写好的程序打在穿孔纸带或者穿孔卡片上,交给操作员。操作员把卡片喂进机器,机器跑完,打印出结果——然后操作员再喂下一张卡片的程序。
这就是 批处理系统(Batch Processing)。一次只跑一个程序,跑完换下一个。计算机很贵,不能让它闲着,所以操作员要保证它一直在跑。但程序之间怎么切换?人工切换。操作员手动把下一张纸带塞进去。
想象一下这个画面:一个穿着白大褂的操作员,抱着一摞穿孔卡片,像喂老虎机一样一张一张往里塞。前面的程序跑崩了?把结果撕下来,塞下一张。前面的程序算了半天算错了?那不关我的事,我只负责喂卡片。
那时候的程序员写好代码交给操作员之后,可能要等好几个小时甚至到第二天才能拿到结果——这就是传说中的"Turnaround Time"(周转时间)。
所以,真正的问题是:
如果一次只能跑一个程序,怎么变成"同时跑很多程序"的?而且为什么我没感觉到它们是在"轮流用"CPU?
为了让大伙儿感受一下"同时运行"是怎么回事,我们回到共享厨房的场景——
管理员悄悄地做了一个决定:让厨师们轮流用厨房。
他让A厨师先用10秒钟,然后喊停,把A厨师的东西迅速收拾好、码到一边,再把B厨师请进来,把之前给B收拾好的东西恢复回原位。B厨师干10秒,再收拾出去,C恢复进来……
关键是——要切换得足够快。快到每个厨师走进厨房的时候,一切都井井有条,所有工具都放在自己熟悉的位置上,完全看不出刚刚被别人用过。
每个厨师都觉得自己独占了整个厨房。
这个思路,在计算机里就是 时分复用(Time-Division Multiplexing)。CPU 就是那个灶台:
- 收拾(Save State) = 进程切换(Process Switch):把当前程序的所有现场(寄存器值、栈指针、程序计数器等)保存下来
- 恢复(Restore State) = 上下文切换(Context Switch):把下一个程序之前保存的现场重新装回 CPU 寄存器
这种"快速轮换"的调度策略,就是 时间片轮转(Round-Robin Scheduling)。一秒切一百次,你坐在电脑前感觉所有程序都在"同时运行"。
但问题随之而来——
这些程序在切换的时候,共享着同一块物理内存。A 程序正在内存里处理你的银行卡密码,B 程序——一个不太老实的程序——能不能直接读 A 程序的内存?
答案是:在没有隔离的系统中,可以。
两个程序的代码和数据在物理内存里紧挨着,一个程序直接去读另一个程序的内存地址,跟串门一样容易。如果 A 程序存了敏感数据,B 程序可以直接抄走。甚至更坏——B 程序可以直接改写 A 程序的数据,让它执行恶意代码。
这就回到了我们共享厨房的比喻:在没有管理员的情况下,一个厨师根本没办法保护自己的食材不被别的厨师偷走或破坏。
Note
这个问题本质上就是 计算域隔离 的问题:一个"域"(程序、进程、虚拟机)中的计算活动,如何不被另一个"域"中的活动干扰或窃取?
| 概念 | 类比 |
|---|---|
| 一个程序/进程 | 一个厨师 |
| CPU(时间片) | 灶台(轮流使用) |
| 内存(RAM) | 冰箱和储物柜 |
| 磁盘文件 | 自己的菜谱和笔记 |
| 域隔离 | 每个厨师有自己的独立工作台和储物柜 |
叫个管理员吧
面对共享厨房的混乱,一个自然而然的办法就是——请个管理员。
这个管理员干什么呢?
- 分配资源:规定哪个厨师在什么时间段用哪个灶台
- 划定区域:每个厨师只能在指定的台面上操作
- 仲裁冲突:两个厨师同时要一个工具时,管理员说了算
- 保护边界:如果 A 的菜被动了,管理员得负责查清楚
翻译成计算机语言,这个"管理员"就是 操作系统内核。它通过几个关键机制来干活:
1. 进程抽象(Process Abstraction)
每个程序被包装成一个"进程",有自己的执行上下文(寄存器的值、栈、堆等)。操作系统通过进程控制块(PCB)来记录每个进程的状态。程序 A 切换到程序 B 时,内核会先保存 A 的现场(寄存器的值),再恢复 B 的现场。这就是传说中的 上下文切换(Context Switch)。
2. 虚拟内存(Virtual Memory)
每个进程拥有自己独立的"虚拟地址空间"。进程 A 的内存地址 0x1000,和进程 B 的内存地址 0x1000,物理上根本就不在同一个地方。但从每个进程的角度看,它们都感觉自己"独占"了整个内存。
这个设计简直绝了——每个进程都觉得自己是宇宙的中心,全内存都是它的。但实际上,操作系统在背后偷偷做了地址翻译(通过 MMU + 页表),把每个进程的"错觉"映射到物理内存的不同区域。
等等,那如果 A 程序想去读 B 程序的地址呢?
硬件层面的 MMU 会在地址翻译时发现——"哎,这个地址不是你的,你不许读"——然后抛出一个异常(Page Fault / Segmentation Fault),操作系统捕到这个异常后,通常就会把不守规矩的程序杀掉。
这就像你刚想伸手去够隔壁厨师案板上的肉,管理员一巴掌把你手拍了回去。
3. 特权级(Privilege Levels)
并不是所有程序都有权限做所有事情。CPU 提供不同的运行模式——x86 上的 Ring 0(最高权限)和 Ring 3(最低权限),Arm 上的 EL0/EL1/EL2/EL3——限制了不同程序可以执行的指令。
- 敏感操作(修改页表、操作 I/O 设备、关闭中断)只能在最高特权级执行
- 普通操作(加减乘除、读写自己的内存)在最低特权级就能干
操作系统内核跑在 Ring 0,应用程序跑在 Ring 3。如果 Ring 3 的程序试图执行 Ring 0 的指令,CPU 直接拒绝并抛异常。
Note
| 概念 | 类比 |
|---|---|
| 进程 | 一个在厨房工作的厨师 |
| 虚拟内存 | 每个厨师眼里"整个世界都是我的"的幻觉 |
| MMU + 页表 | 背后偷偷做地址翻译的行政助手 |
| 特权级(Ring 0) | 管理员(啥都能干) |
| 特权级(Ring 3) | 普通厨师(只能在自己台面上干活) |
| 系统调用 | 厨师需要敏感材料时,举手叫管理员帮忙拿 |
有了这三板斧,操作系统看起来是把"域隔离"做得不错了。三个厨师各忙各的,互不干扰,岁月静好。
当初为什么要隔离?—— Safety 而非 Security
看到这里你可能以为:操作系统的进程隔离、虚拟内存这些机制,从一开始就是为了防止恶意程序偷数据吧?
其实不是。早期操作系统引入这些隔离手段,第一个动机是 Safety(稳定性),而不是 Security(安全性)。
想象一下:在批处理时代,如果一个程序跑崩了(比如写了不该写的内存地址),整个机器也跟着崩溃,所有在排队的作业全白费。在分时系统里,如果一个 Buggy 程序崩了导致整个系统挂掉,其他所有用户都受影响。
所以最早的隔离设计,核心诉求是:"一个程序挂了,别把其他程序也拖下水。"
这就好比共享厨房里,管理员最早画出工作区域的初衷,不是为了防止厨师之间互相偷东西——而是防止一个厨师笨手笨脚打翻油锅,把整个厨房烧了。
直到今天这个初衷依然重要:内核对失控进程的首要处理动作是保护系统稳定性,而不是保护数据机密性。 只不过后来人们发现——这套"防止互相搞破坏"的机制,恰好也能用来"防止互相偷东西"。Security 是 Safety 的一个美丽的副产品。
这个视角值得记住——在计算域安全的历史里,很多后来被视为"安全隔离"的技术,追根溯源,最初都是为了别的目的被发明出来的。
但问题在于——这个管理员本身并不完美。
管理员也会翻车
共享厨房的管理员虽然能解决大部分冲突,但仔细想想:
- 管理员自己也要在厨房里走动吧?他可能不小心碰翻别人的锅(内核代码自身也有 bug)
- 管理员制定的规则可能有漏洞——规定灶台使用时间,但没规定油烟机的使用规则(内核的攻击面很大,总有没考虑到的角落)
- 狡猾的厨师可能利用规则漏洞,在管理员眼皮底下搞小动作(对内核接口的巧妙利用)
用计算机的话说就是:
- 内核漏洞:内核也是软件,也有 bug。一个不怀好意的程序可能利用内核漏洞,从 Ring 3 提到 Ring 0。权限一旦提升,所有的"隔离"在它面前就跟纸糊的一样。
- TOCTOU 漏洞:管理员在"检查权限"和"执行操作"之间有一瞬间的空隙——"Time of Check to Time of Use"。狡猾的厨师(恶意程序)可以利用这个时间窗口,在管理员检查完后、操作之前,偷偷把东西换了。
- 侧信道攻击:就算内存隔离了,CPU 缓存、分支预测器这些共享硬件资源还是会留下"痕迹"。一个进程可以通过测量缓存访问时间来推断另一个进程的数据——我们在第5章会细聊这个。
- 接口攻击面:管理员要提供各种服务接口给厨师们用(系统调用)。接口越多,漏洞的可能性就越大。"拿一瓶酱油"这个接口可能很安全,但"帮我拿一下那个柜子第二层左边第三个格子里的红色瓶子"这种接口,中间出错的概率就大多了。
重点是:操作系统提供的隔离是逻辑层面的隔离,它依赖于内核代码的正确性和完整性。 一旦内核被攻破,所有隔离都化为乌有。
"管理员也是打工人,也会犯错"——这就是第三章我要带你探讨的核心问题。
时代共鸣:AI 也说'我会犯错'
这种"管理员承认自己会犯错"的坦诚,在今天的人工智能时代以一种意想不到的方式重演了。
你打开任何一个大模型产品,几乎都会看到一行免责声明——"本AI可能会犯错,请仔细核实关键信息"。这句话表面上是免责,实质上是划定一条责任边界:系统提供服务,但不担保完美,用户需自行承担风险。
操作系统和 AI 在这一点上惊人地相似:一个告诉你"我的隔离可能有漏洞",另一个告诉你"我的回答可能有错误"。它们的潜台词是同一个——系统的复杂性决定了它不可能完美,你选择用它,你就接受了这个前提。
你会发现,无论是 OS 管理员还是大模型,"我会犯错"这句话从来不是终点。真正的问题在于:在明知不完美的情况下,你还愿意信任它吗?如果愿意,你愿意付出多少代价来兜底? 这个问题贯穿了整个计算域安全的历史,也是今天每一个 AI 用户同样要回答的问题。
从"共享厨房"到"独门独户"
虚拟化的真正起源:不是安全,是省钱
聊到这里,你可能会觉得整个故事是这样的:
"操作系统隔离不够完美 → 所以需要虚拟化来加强隔离 → 于是有了硬件虚拟化来保证安全。"
听起来顺理成章,但历史的剧本并不是这么写的。
回到1960年代。IBM 投入了大约 50亿美元 来开发 System/360 系列——按今天的市值算超过300亿美元,是当时除了阿波罗计划之外最大的单笔技术投资。一台 System/360 大型机售价在数百万美元级别,最便宜的 Model 30 月租金就要 8,000 美元,放在现在的物价相当于一台超级跑车的月供。企业、大学、研究机构要倾家荡产才能买得起一台。
但问题来了——
这些贵到离谱的机器,CPU 利用率却低得可怜。
批处理时代,一个程序跑完,操作员要手动换下一张纸带,中间 CPU 只能空转。分时系统出现后,多个用户能"同时"用一台机器了,但更根本的矛盾浮出水面:一台机器上只能跑一个操作系统。 你习惯用这个系统,他习惯用那个系统——两种需求在同一台硬件上水火不容。
这就好比你花了几百万买了整套顶级商用厨房,但一天只开一餐,剩下时间炉灶冰凉。任何有商业头脑的人看到这个画面都睡不着觉。
省钱提效永远是第一位,安全永远是被打疼了才会愿意付钱。
这话说得直白得有点难听,但历史会反复证明它是对的。
这时候 IBM 剑桥科学中心(Cambridge Scientific Center)的一群工程师想了一个更大胆的主意——
既然没法让所有用户在同一台机器上用同一个 OS,那就干脆把这台机器"分身"成多台机器,每台跑自己的 OS。
这就是 虚拟机 的起源。
1966-1967年,IBM CP-40 和后续的 CP-67 系统诞生了。CP 的全称是 Control Program(控制程序)——也就是最早的 虚拟机监视器(Hypervisor/VMM)。它运行在裸机上,负责把硬件"分身"成多台虚拟的 IBM System/360 大型机citation:IBM CP-40 Wikipedia。每台虚拟机上运行 CMS(Cambridge Monitor System)——一个轻量级的单用户操作系统。用户通过终端登录,看到的是一台"完整"的 IBM 大型机,想怎么用就怎么用citation:CP/CMS History。
它的根本动机,从来都不是安全隔离——而是提高昂贵硬件的利用率,让多个用户共享一台机器的计算资源。
套用厨房比喻来说:
你有一间价值几百万的超大商用厨房,只租给一家餐厅用,大部分时间空着。你想到的办法不是"找个更好的管理员"——而是把大厨房拆分成多个独立的功能完整的迷你厨房。每个迷你厨房有自己的灶台、水槽、冰箱、刀具。租给不同餐厅,每家自己布置、自己管理。你只需要保证隔断墙够结实。
隔断墙的存在,自然也让一家餐厅没法偷另一家的菜。但这个"安全隔离",是你为了"省钱、提高利用率"顺便得到的,不是初衷。
1974年,Popek 和 Goldberg 在《Formal Requirements for Virtualizable Third Generation Architectures》中正式提出了虚拟化架构的三个必要条件citation:Popek-Goldberg requirements:
- 等价性(Equivalence):虚拟机运行的程序效果与裸机一致
- 资源控制(Resource Control):VMM 对所有硬件资源有完全控制权
- 效率(Efficiency):绝大多数指令直接在硬件上执行,无需 VMM 模拟
注意这三条里没有任何一条跟"安全"直接相关。等价性是功能要求,资源控制是管理权要求,效率是性能要求。安全隔离是通过满足这三条之后的自然结果——因为 VMM 控制了所有资源,Guest 根本没有途径访问不属于自己的东西。
当虚拟化遇见 x86:硬件辅助的姗姗来迟
虚拟机技术从此成为大型机(IBM System/370 及其后继者)的标准配置。但在 x86 平台上,故事完全不同。
1980-90年代的 x86 指令集架构有一个长期被忽略的问题:它不是经典意义上"可虚拟化"的。
Popek-Goldberg 形式化要求中有一条隐含条件:所有敏感指令(读写特权寄存器、操作 I/O 等)在非特权模式下执行时,必须触发 trap(异常)以便 VMM 拦截模拟。但 x86 上有十几条指令——比如 POPF、SIDT、SGDT、SMSW——它们在低特权级执行时会静默失效(悄无声息地啥也不干),而不是触发异常citation:x86 virtualization。
这意味着 VMM 没法在软件层面优雅地拦截和模拟这些指令——因为它根本不知道 Guest OS 执行了它们。
为了在 x86 上跑虚拟机,工程师只能靠"补丁"。VMware 采用了 二进制翻译(Binary Translation) 技术:Guest OS 被降级运行,敏感指令在执行前被动态扫描并替换为安全的指令序列citation:VMware binary translation。效率可以接受,但实现极其复杂。
这个僵局直到2005-2006年才被打破。Intel 推出了 VT-x(代号 Vanderpool),AMD 推出了 AMD-V(代号 Pacifica)citation:Intel VT-x。两家 CPU 厂商在硬件层面加了一套全新的运行模式,专门为虚拟化服务:
┌──────────────────┐
│ VM Entry/Exit │
└────────┬─────────┘
│
┌──────────────────────┴──────────────────────┐
│ Root Mode (VMM 运行于此) │
│ Ring 0 ~ Ring 3 │
└──────────────────────┬──────────────────────┘
┌────────────────────┼────────────────────┐
│ VM1 (Non-Root) │ VM2 (Non-Root) │
│ │ │
│ ┌──────┐ ┌──────┐ │ ┌──────┐ ┌──────┐ │
│ │Ring 0│ │Ring 3│ │ │Ring 0│ │Ring 3│ │
│ │Guest│ │Guest │ │ │Guest│ │Guest │ │
│ │ OS │ │ Apps │ │ │ OS │ │ Apps │ │
│ └──────┘ └──────┘ │ └──────┘ └──────┘ │
└────────────────────┴──────────────────────┘
VT-x/AMD-V 引入了"根模式(Root Mode)"和"非根模式(Non-root Mode)"。VMM 跑在根模式的 Ring 0,Guest 跑在非根模式。任何敏感操作——比如执行 CPUID、修改页表、访问 I/O 端口——在非根模式下都会触发 VM Exit,自动陷入根模式,由 VMM 处理。处理完再通过 VM Entry 回到 Guest。
这下,所有敏感指令的拦截都在硬件层面完成,Guest OS 无需任何修改就能直接运行。x86 终于变得"可虚拟化"了。
重新理解虚拟化隔离
所以,当我们回顾这段历史,会发现一个有趣的错位:
| 常见叙事 | 历史真相 |
|---|---|
| "虚拟化提供了强大的安全隔离" | 隔离是副产品,最初动机是资源共享 |
| "虚拟化是操作系统隔离不够好的替代方案" | 最初是为了不同 OS 的共存,不是隔离强度不足 |
| "硬件虚拟化是为了更安全" | 是为了解决 x86 不可虚拟化的架构问题 |
这并不否定虚拟化在事实上的隔离能力。VMM 控制的硬件资源隔断,确实提供了一层比操作系统更强有力的边界——一个 VM 里的程序哪怕攻破了自己 VM 的内核,也无法直接访问另一个 VM 的内存,因为这种跨 VM 访问在硬件层面就会被拦截。
只是我们需要看清:安全隔离从来不是这项技术被发明时的初心。它是资源高效利用这个更基础的需求,带来的一个极为重要的副产品。
而这个副产品,恰恰让云计算成为了可能。你今天租一台云服务器,和陌生人共享同一台物理机,却丝毫不担心对方能偷走你的数据——靠的就是这层"本不是为安全而设计"的隔离。
再往前一步:还可以更安全吗?
看到这里你可能又会问:
那 Hypervisor 也是软件啊,它就不出 bug 吗?万一 Hypervisor 也翻车了呢?
好问题。确实,Hypervisor 也有漏洞。KVM、VMware、Xen 历史上都被爆出过严重漏洞,攻击者利用这些漏洞从 Guest 虚拟机逃逸到宿主机(这叫 VM Escape)。
所以,比"虚拟化"更进一步的方案是:不信任任何人——连 Hypervisor 也不信任。
这就是 可信执行环境(TEE) 的思路,比如 Intel SGX。在 CPU 内部划出一块"飞地"(Enclave),这块内存连操作系统和 Hypervisor 都访问不了。数据在 CPU 外面是加密的,进到 CPU 内部才解密处理——即使你的整台机器都被攻破了,攻击者也读不到 Enclave 里的数据。
(这个我们留到第4章再详细展开。)
一张图总结我们的旅程
| 阶段 | 比喻 | 隔离粒度 | 隔离强度 | 核心驱动力 |
|---|---|---|---|---|
| 单程序计算机 | 一个人用整间厨房 | 无需隔离 | - | - |
| 批处理 → 时分复用 | 多个厨师轮着用同一间厨房 | ❌ 无隔离 | ❌ 乱成一团 | 提高CPU利用率 |
| 操作系统出场 | 管理员来了!划定区域和规则 | 进程级隔离 | ⚠️ 依赖管理员不打瞌睡 | Safety(稳定性) |
| 硬件虚拟化 | 大厨房拆成独立迷你厨房 | 虚拟机级隔离 | ✅ 硬件隔断 | 资源共享 → 安全隔离(副产物) |
| 更进一步(TEE) | 自己的保险柜,钥匙自己拿着 | 硬件飞地隔离 | ✅✅ 连管理员也看不到 | 机密性 |
那么,接下来聊什么?
这个表格展示了计算域隔离的演进路径。但"域"这个概念,远不止进程和虚拟机两个层次。
这一章(第3章)的核心——计算域间安全——关注的是:跨越这些域边界时,会发生什么?
一个程序试图从它的"厨房"跑到别人的"厨房"里搞破坏——它会用什么方法?CPU 给我们准备了哪些防御?攻击者又想出了哪些骚操作来绕过?
下一节(3.2),我们就从最常见、也最"经典"的域间攻击开始讲起——
权限提升(Privilege Escalation)。
也就是:一个 Ring 3 的普通程序,怎么变成 Ring 0 的"超级管理员"?以及——CPU 和操作系统又是怎么堵住这条路的?
敬请期待。