冯诺依曼体系结构
冯诺依曼体系结构
复习定位
冯诺依曼结构不是五部件的名词解释——它的全部意义都在一句话里:"指令和数据放在同一个可读写的存储器中"。这一句话的硬件后果直接解释了为什么shellcode攻击在物理层面上成立、为什么W^X是必须的安全补救、为什么现代CPU要花大量晶体管在Cache和流水线上。
存储程序:一个前提推出全部后果
1945年冯诺依曼在EDVAC报告中提出一个看似平淡的设想——程序本身和数据以相同的二进制形式存放在同一个可读写的存储器中。ENIAC改程序要插线两天——冯诺依曼让计算机通过读取存储器中的不同内容就能执行不同任务。从此计算机从"专用计算器"变成"通用计算设备"——这是计算机科学最重要的概念转折。
这个前提的第一个推论——CPU在取指时无法分辨当前地址存的是指令还是数据。控制器只是按程序计数器(PC)指向的地址取若干字节,把它当作指令去译码执行。如果这个地址实际存的是字符串或整数——CPU照样"忠实地"把它当作指令执行。
举一个具体场景证明这一点:x86-64下,字节序列48 31 C0按指令解码是xor rax, rax(把rax寄存器清零)。同样的字节序列按数据看就是三个无符号整数72、49、192。CPU取到这三个字节后——它的解释完全取决于程序计数器指向了这里。如果攻击者能让PC指向一段他写入的、按指令编码"有意义"的字节序列——CPU会忠实地逐条执行。
这就是shellcode攻击的物理前提——攻击者在内存中放入的字节序列,只要能让PC指向它,CPU就会把它当指令执行。后续所有的W^X(写和执行互斥)、DEP(数据执行保护)、ASLR等防护措施——本质都是在这个冯诺依曼前提下尽量限制"攻击者写入的数据被当作指令执行"的可能性。
第二个推论——程序可以修改自己或别的程序。因为存储器可写——JIT编译器在内存中动态生成机器码再执行(JavaScript的V8、Java HotSpot都是这个机制)。病毒、加壳软件、自修改代码也基于同一条原理。这一灵活性在安全上是双刃剑——现代CPU通过页表中的W^X权限位试图缓解——但在冯诺依曼模型本身的逻辑下指令和数据无法根本隔离。
五部件的数据流和控制流
冯诺依曼模型给出五个部件:运算器、控制器、存储器、输入设备、输出设备。它们之间有两条流:
数据流:输入设备→存储器→运算器→存储器→输出设备。用户敲键盘的字符进入存储器、被运算器加工、结果写回存储器、最终通过输出设备显示。整条路径都在存储器中转——这是"存储为中心"的设计。
控制流:控制器按PC指向的地址从存储器取指令→解码→向其他部件发出控制信号→更新PC。控制器自己不参与数据运算——它只发信号指挥。例如执行add eax, ebx——控制器向寄存器文件发出"读eax和ebx"的信号→向ALU发出"做加法"的信号→向寄存器文件发出"写回eax"的信号。整条控制信号链由控制器在指令周期内产生。
控制器和运算器的边界
现代CPU在物理上把控制器和运算器都集成在同一芯片——但从冯诺依曼的功能划分看二者逻辑独立。控制器读指令、发控制信号;运算器执行实际的算术逻辑运算。这种逻辑分离在硬件描述语言(如Verilog)实现CPU时仍能清晰看到——控制单元(control unit)和数据通路(datapath)是两个独立模块。
程序计数器(PC)——控制流的核心
PC寄存器保存下一条要取的指令地址。CPU的工作循环简化到极点:
loop:
IR = Memory[PC] # 取指:从PC指向的内存读指令到指令寄存器
decode(IR) # 译码:解析指令含义,决定要发哪些控制信号
execute(IR) # 执行:根据指令类型做运算/访存/跳转
PC = PC + instruction_length # 更新PC(除非是跳转指令直接改写PC)跳转指令(jmp/jcc/call/ret)的"跳转"本质是直接改写PC的值。jmp 0x401000把PC设为0x401000——下一条取的指令来自这个地址。call func额外把返回地址(当前PC的下一条指令地址)压栈、然后跳转到func的地址——ret从栈弹出返回地址写入PC——这构成了函数调用的完整机制。
PC为什么是控制流的核心——因为它决定了CPU接下来要执行什么。攻击者只要能改写PC的值——就能让CPU执行任意地址的代码。栈溢出覆盖返回地址就是改写PC的攻击路径:
函数func的栈布局(从高地址到低地址):
[返回地址] ← ret指令会弹出这个值到PC
[保存的rbp]
[局部变量buf[64]]
攻击者向buf写入超过64字节——溢出的部分覆盖到返回地址
当func执行ret时——CPU从栈弹出被覆盖的返回地址写入PC
PC指向攻击者构造的shellcode——CPU开始执行恶意代码这就是为什么栈溢出如此危险——它直接劫持了冯诺依曼模型中最核心的PC寄存器。
冯诺依曼瓶颈的量化
指令和数据共享同一存储器和总线——CPU取指时数据总线被占用,无法同时读数据。CPU的处理速度每18个月翻倍(Moore定律)、内存访问速度提升缓慢——两者的剪刀差越来越大。
量化的速度差距(以2024年的典型值为参考):
CPU一个时钟周期: 0.3 ns (3 GHz)
L1 Cache命中: 0.5 ns
L2 Cache命中: 3 ns
L3 Cache命中: 12 ns
内存访问: 80-100 nsCPU执行一条指令只需要0.3ns——但要从内存取指令加取数据需要约200ns——CPU在99%以上的时间在等内存。这就是冯诺依曼瓶颈——物理总线成了CPU和存储器之间的吞吐天花板。
缓解手段及其根本逻辑
所有缓解手段的根本逻辑都是"减少对慢速存储器的访问次数":
Cache层:在CPU和内存之间插入由SRAM构成的小容量高速缓冲。利用局部性原理(程序在短时间内访问的指令和数据集中在很小的地址范围内)——把热点数据提前读入Cache——大多数访存命中Cache而不是回到内存。L1命中0.5ns比内存快160倍——这一差距是Cache存在的物理理由。
指令Cache和数据Cache分离(哈佛结构的局部实现):L1分为L1I和L1D两个独立阵列——CPU可以在同一周期从L1I取指、从L1D读数据——两者不互相阻塞。这从根本上缓解了"取指和访存不能同时"的冯诺依曼原始瓶颈。
超标量和乱序执行:让多条指令的独立部分并行——一条在等内存时其他继续执行。CPU内部的保留站和重排序缓冲区(ROB)让指令的执行顺序不必严格等于程序顺序——只要数据依赖满足即可——从而填补等内存的空隙。
多核:多个CPU核心共享最后一级Cache(LLC)和内存控制器——每个核心有自己的L1/L2缓解共享内存的压力。操作系统把不同进程调度到不同核心——多个进程真正并行而非时分复用。
哈佛结构作为冯诺依曼的对立面
哈佛结构用两套独立的存储器(指令存储器+数据存储器)和两套独立总线——取指和访存完全并行。DSP(数字信号处理器)完全采用哈佛结构——音频/视频处理的卷积和FIR滤波算法高度规律——每周期都要取下一条指令和读新的数据样本——哈佛结构让两者并行实现接近每周期一条指令的吞吐。
通用CPU没有完全采用哈佛结构——因为统一编址的灵活性(代码可以修改自己、JIT编译)对通用计算很重要。但L1 Cache层做了局部哈佛——L1I和L1D分离——在CPU的"核心区"内部实现了哈佛结构的并行优势、在外部仍保持冯诺依曼的统一编址灵活性。
五部件的输入输出视角
从外部世界看计算机——输入设备和输出设备是计算机和外部世界的接口。键盘、鼠标、网卡、硬盘是输入设备——把外部信息转换为二进制数据送入存储器。显示器、打印机、网卡(双向)、硬盘(双向)是输出设备——把存储器中的二进制数据转换为人能感知的形式或传送给其他系统。
值得注意的是硬盘既是输入设备也是输出设备——读文件时它是输入、写文件时它是输出。网卡同理——收包是输入、发包是输出。冯诺依曼模型中"输入"和"输出"的分类是按数据流向分的——不是按物理设备分的。
I/O设备速度远慢于CPU——一次硬盘寻道约10ms——在这10ms内CPU可以执行约3000万条指令。这种速度差距是中断机制和DMA机制存在的根本原因——CPU不能傻等I/O——必须能切换去做其他工作、I/O完成后再回来。
存储器的层次——冯诺依曼模型的演化
冯诺依曼原始模型中只有一个"存储器"——但现代计算机的存储器已经分化为多级层次:
寄存器 ← CPU内部,纳秒级,KB级容量
L1 Cache ← CPU内部SRAM,纳秒级,几十KB
L2 Cache ← CPU内部SRAM,几纳秒,几百KB
L3 Cache ← CPU封装内多核共享SRAM,十几纳秒,几MB到几十MB
主存DRAM ← CPU外部,百纳秒级,GB级
SSD/HDD ← 持久存储,微秒到毫秒级,TB级每一层的速度差大约1-3个数量级。越靠近CPU越快越小越贵。操作系统和硬件协作——把热点数据保存在高层(快)、冷数据下沉到低层(慢)——对程序透明地呈现一个"又快又大"的虚拟存储空间。
这一层次结构在冯诺依曼原始模型中是不存在的——它是为了应对冯诺依曼瓶颈(存储器速度跟不上CPU)而在数十年工程实践中逐步演化出的复杂体系。Cache、虚拟内存、页缓存都是这一演化的产物。
与现代CPU微架构的对应
冯诺依曼的五部件在现代CPU微架构中的对应:
- 运算器 → ALU(算术逻辑单元) + 整数/浮点执行单元 + 向量执行单元(SSE/AVX)
- 控制器 → 取指单元(IF) + 译码单元(ID) + 微码翻译器(把x86复杂指令翻译为微操作uop)
- 存储器 → 寄存器文件 + L1/L2/L3 Cache + 主存 + 磁盘(通过虚拟内存机制统一编址)
- 输入设备 → 通过I/O控制器连接的键盘/鼠标/网卡/USB等
- 输出设备 → 通过I/O控制器连接的显示器/打印机/网卡等
每个部件都被极大地扩展了——但冯诺依曼模型的核心(存储程序、PC驱动的取指执行循环、指令和数据共享存储)至今未变。这就是为什么冯诺依曼结构至今仍是计算机体系结构教学的起点——所有现代优化都是在这一基础上的"补丁"而非颠覆。
复习检查
CPU执行
call func时PC和栈各发生了什么?栈溢出覆盖返回地址如何让PC跳到任意位置——用具体的栈布局图说明。程序计数器(PC)和指令寄存器(IR)各存什么?为什么需要这两个独立的寄存器?能否合并?
为什么冯诺依曼模型下指令和数据无法在硬件层面隔离?W^X权限位如何在软件层面补救这个缺陷——它的局限是什么?
冯诺依曼瓶颈的物理根源是什么?L1 Cache分离(指令Cache和数据Cache)如何缓解这一瓶颈?
哈佛结构(分离的指令存储器和数据存储器)解决了冯诺依曼的什么问题?为什么通用CPU没有完全采用哈佛结构——而在哪一层做了局部哈佛实现?