3.1.1 地址
Last updated
Last updated
我们可以将内存想象成一排连续的存储坑位,这些坑位的最小寻址单位是字节(Byte),每个字节的编号就是该字节的地址(从0开始),CPU通过地址来定位并访问每个字节,我们将该地址称为内存的物理地址(Physical Address),因为他代表着目标字节的实际物理位置。CPU与内存通过总线(Bus)连接,总线分为三类:
地址总线
数据总线
控制总线
总体结构如下:
所谓总线,就是主板(Motherboard)上用于不同硬件单元通信的数据通道,可以简单理解成有很多引脚的一条链路,电子信号通过引脚进行传播,总线的带宽用位数(Bit)来表示,总线位数限制了一次能够传输的信号总量,这是非常重要的硬件指标。地址总线的位数限定了CPU的寻址空间,例如一个CPU的地址总线是20位,那么其最大的寻址范围就是 2^20 = 1M, 超过该范围的物理地址就访问不到了。
每当CPU需要从内存存取数据时,就将目标字节的地址输入地址总线,将控制信号(例如读取还是写入)输入控制总线,然后在数据总线读取或写入数据即可。这就是最简化的CPU与内存的交互方式。
内存地址的使用者是程序,如果在程序中直接使用内存的物理地址的话,会有什么效果呢?我们分别看一下这种寻址方式的优缺点:
Pros
思路简单,地址不需要转换
硬件设计简单,执行效率高
Cons
不够灵活
程序直接与内存的硬件概念联系在一起,耦合性太强,这会对程序或者硬件升级带来不变
缺乏安全性,程序可以访问任何物理地址,但不同程序所使用的内存很明显应该隔离开来
对多任务的管理复杂度上升
这种寻址方式叫着 Real Mode Flat Model, Real Mode 可以理解为逻辑地址就代表着真实的物理地址,而 Flat Model 代表着这种直接的映射关系。
寻址方式就是如何将程序中使用的地址转换成内存的物理地址
Intel 在 1974 年发布的8080处理器采用的便是这种寻址模式。8080 是一个8位处理器,地址总线为16 位,意味着其寻址空间是64KB.
注意:处理器位数与总线位数不一定是相同的。虽然 8080 是 8 位处理器,但其用于存放地址的寄存器 SP(Stack Pointer) 与 PC(Program Counter) 都是16位的,另外程序也可以将两个8位寄存器合并起来当着一个16位寄存器使用。8080 的寄存器架构请见这里
使用 8080 芯片最多的操作系统是 CP/M-80, 该操作系统的一个特点是操作系统代码存在于内存顶端,这样做的目的是为了将运行程序加载到统一的位置,CP/M-80 将应用程序加载到内存底部的 0100 处,即底部256字节处(前256 字节用着该程序的IO缓存,同时也用于存放一些其他信息)。这种内存模型简单直接,但缺乏灵活性,因为操作系统与应用程序所在的内存地址都是固定的,对于后期升级非常不友好。
至此,从物理地址的视角来看,内存是一个以字节为单位、连续、一维的存储单元,地址是不同字节的唯一标识。
为了能够使用更多的内存地址,CPU的地址总线总是越大越好,然而CPU在内部使用寄存器(Register)来存放各种信息,包括地址信息也存放在寄存器中,寄存器也有限定的位数,如果寄存器的位数小于数据总线的位数的话,那如何存放完整的地址信息呢?
寄存器位数就是我们常说的CPU位数,是指CPU能一次同时存储和处理的二进制信息的位数,通常与数据总线的位数相当。
一个简单的思路是将多个寄存器合并起来使用,形成逻辑上更大的存储单元。例如如果一个16 位的处理器拥有20 位的地址总线,那么我们可以使用一个寄存器存放地址的高4位,使用另一个存放低16位,每次寻址时将二者合并起来即可。
1976年,Intel发布了16位处理器8086,拉开了x86架构的序幕。该处理器的地址总线为20位,意味着寻址空间增大到了为1M,是8080的16倍。8086此时就面临寄存器位数小于地指总线的问题,但除此之外,8086还面临另一个问题:如何让8080的程序顺利完成迁移。即之前为8080 写的各种程序如何顺利跑在新处理器上。
让程序从8080迁移到8086的关键是让8080的16位寻址策略依然有效,即程序可以将1M内存区块中的某个64KB当着独立的内存空间来使用。
8086通过分段寻址(Segmented Addressing)的方式来支持这种策略,段(Segment)是一个连续的内存区块,不同的段通过不同的起始位置来进行区分。内存地址也由此划分成了两部分:一是段的起始位置,二是段内偏移量。因此两个寄存器一个用来存放段基址(Base Address),另一个用来存放偏移量(Offset),内存地址的表示形式为: SR:IR
,其中 SR 表示段寄存器,而 IR 表示偏移量寄存器。由于寄存器是16位的,所以每个段内最大的偏移量为64KB, 这样一个段就刚好与8080的寻址空间大小一致。
那么如何对段进行划分呢?8086 规定每个能被16整除的地址位置都可以作为段的起始位置,这样的话 1M 的内存空间最多可以有 1M / 16 = 64K 个段,段寄存器存放的实际上是从 0 开始的段标号,因此对于地址 SR:IR
, 将其转换为 20 位地址的算法为: SR * 16 + IR
, 这种转换通过硬件实现也是很容易的。
8086 引入了4 个段寄存器,分别是:
CS: Code Segment
DS: Data Segment
SS: Stack Segment
ES: Extra Segment
同时还引入了索引寄存器(Index Register)用来存放偏移量,例如:
IP: Instruction Pointer 指向下一条CPU 将要执行的指令地址,通常与CS 一起使用,即 CS:IP 代表着下一条指令地址。
SP: Stack Pointer 指向当前栈顶,通常与 SS 一起使用
这种寻址方式不仅能够使用 16 位寄存器完成 20 位地址空间的寻址,还完美地解决了8080 程序迁移的问题(任何 8080 的程序只需要使用一个段就够了),但却限制了新程序使用内存的方式。对于在 8086 上新写的程序而言,虽然理论上其可以使用完整的 1M 内存,但其寻址策略逼迫程序只能以分段的方式来使用内存,并且每个段最大只能是64KB, 如果程序的代码区块超过了64KB, 那么就必须使用多个代码段(Code Segment),OS 就必须管理好执行程序时 CS 在不同段之间来回跳转的逻辑。
这种模式被称为 Real Mode Segmented Model, 其中 Segmented Model 代表通过分段的方式完成逻辑地址到物理地址的转换,而Real Mode 依旧表示转换之后的地址代表物理地址。而转换之前的地址,被称为逻辑地址(Logic Address)。
在讨论 x86 架构的内存模型时,该模型也被直接叫做 Real Mode, 因为整个 x86 系列的芯片都沿用了分段寻址模式。
分段本质上就是把内存劈成一小块一小块来使用,每一块就是一个段,每个段可以根据程序设计者的需要用于不同的目的,例如上文提到的代码段与数据段。逻辑地址实际上是对内存地址进行了一次编码,一维的物理地址此时被编码成了一个二维的形态:段 + 段内偏移。
Real Mode简单直接,但基于这种机制很难构建出健壮、安全的内存管理模块。例如Real Mode存在着如下缺陷:
安全隐患 在Real Mode下,CPU 并没有有效的机制来禁止程序访问不属于自己地址空间,例如下图中,虽然 Process A 与操作系统的代码存放在不同的段之中,但处理器并没有任何机制阻止Process A访问操作系统的代码区块。这样程序Bug或者恶意代码很容易造成灾难性的后果。特别是在多任务系统中,这种缺陷会造成不同进程相互影响。
逻辑地址空间超过物理内存容量 在计算机系统中,逻辑地址空间通常远大于物理内存的容量。例如1985年Intel发布了32位的处理器80386,其寻址空间达到了4GB, 但八十年代的个人电脑几乎不可能拥有4GB的内存,而现在的64位系统的寻址空间更是大得惊人。因此我们需要某种技术来完成逻辑地址到物理地址之间的映射,这样的话我们可以通过内存加磁盘的方式来模拟出更大容量的内存。
解决安全问题的思路是引入权限级别,对内存的读取需要对应的授权,同时对不同任务的内存区域进行隔离;解决容量问题的思路是在逻辑地址与物理地址之间再引入一个抽象层,对二者进行解耦。为了应对这些问题,Intel引入了一种新的寻址方式:保护模式。保护模式扩展了分段机制的功能,引入了权限级别;并通过分页机制(Paging)来实现虚拟地址空间。
保护模型引入了权限级别的概念,CPU 总共有4 个权限级别,0 最高,3 最低。每个内存段都会标注自己的权限级别,当低权限级别段中的代码想要访问高权限级别段中的数据时,CPU 就会报错。Linux中内核使用的是权限0, 而用户代码使用的是权限3, 因此用户代码无法直接访问内核的地址空间中的任何数据。
如果在保护模式下并开启了分页功能,就会在逻辑地址与物理地址之间引入一个新的概念:线性地址,也叫虚拟地址。此时首先需要将逻辑地址转换为线性地址,再将线性地址转换为物理地址,如果没有开启分页功能,线性地址就是物理地址。
分页是对分段的扩展,虚拟地址到物理地址的转换需要借助页目录(Page Directory)与页表(Page Table)来完成,这部分的内容将在下一节中详细介绍。