计算机组成 -- 内存
程序装载
- 在Linux或Windows下,程序并不能直接访问物理内存
- 内存需要被分成固定大小的页,然后通过虚拟内存地址到物理内存地址的地址转换,才能到达实际存放数据的物理内存位置
- 程序看到的内存地址,都是虚拟内存地址
地址转换
简单页表
- 页表(Page Table,一一映射):<**虚拟**内存的页, **物理**内存的页>
- 页表:把一个内存地址分成页号(Directory)和偏移量(Offset)两部分
- 前面的高位,是内存地址的页号;后面的低位,是内存地址的偏移量
- 页表只需要保留虚拟内存地址的页号和物理内存地址的页号之间的映射关系即可
- 同一个页里面的内存,在物理层面是连续的
- 对于32位的内存地址,4KB大小的页,需要保留20位的高位,12位的低位
- 内存地址转换步骤
- 把虚拟内存地址,切分成页号和偏移量
- 从页表里面,查询出虚拟页号对应的物理页号
- 直接拿到物理页号,加上前面的偏移量,得到物理内存地址
空间问题
- 32位的内存地址空间,页表一共需要记录2^20个到物理页号的映射关系
- 一个页号是完整的32位的4 Bytes,一个页表就需要4MB的空间(2^20 * 4 Bytes = 4MB)
- 每一个进程,都有属于自己独立的虚拟内存地址空间,每个进程都需要这样的一个页表 – 占用的内存空间非常大
- 32位的内存地址空间只能支持4GB的内存,现在大多都是64位的计算机和操作系统
多级页表
- 其实没有必要存下2^20个物理页表,大部分进程所占用的内存是有限的,需要的页也自然是有限的
- 只需要去存那些用到的页之间的映射关系 – 多级页表
- 整个进程的内存地址空间,通常是两头实、中间空
- 栈:内存地址从顶向下,不断分配占用
- 堆:内存地址从底向下,不断分配占用
- 虚拟内存占用的地址空间,通常是两段连续的空间
- 多级页表特别适合这样的内存地址分布!!
4级的多级页表
- 同样一个虚拟内存地址,偏移量的部分和上面的简单页表是一样的,但原先的页号部分,拆分成了4部分
- 对应的,一个进程会有一个4级页表
- 先通过4级页表索引,找到4级页表里对应的条目
- 这个条目里存放的是一个3级页表所在的位置
- 4级页表里面的每一个条目,都对应着一张3级页表,因此可能会有多张3级页表
- 找到对应的3级页表之后,再用3级页表索引去3级页表找到对应的条目(指向一个2级页表)
- 2级页表里,可以用2级页表索引指向一个1级页表
- 最后一层的1级页表里面的条目,对应的数据内容就是物理页号了
- 拿到物理页号后,可以用页号+偏移量的方式,来获取最终的物理内存地址
- 因为实际的虚拟内存空间通常是连续的,可能只需要很少的2级页表,甚至只需要1张3级页表即可
- 多级页表类似于一个多叉树的数据结构,因此常常称之为页表树(Page Table Tree)
- 因为虚拟内地址分布的连续性,树的第一层节点的指针,很多是空的,即不需要对应的子树
- 不需要子树,也就是不需要对应的2级、3级的页表
- 找到最终的物理页号,相当于通过特定的访问路径,走到树最底层的叶子节点
- 因为虚拟内地址分布的连续性,树的第一层节点的指针,很多是空的,即不需要对应的子树
空间对比
- 多级页表
- 如果每一级都用5个bit来表示,那么每一张某1级的页表,只需要2^5=32个条目
- 如果每个条目都还是4 Bytes,一共需要128 Bytes
- 一个填满的1级索引表,对应32个Page(4KB),即128KB的大小
- 一个填满的2级索引表,对应32个1级索引表,即4MB的大小
- 如果每一级都用5个bit来表示,那么每一张某1级的页表,只需要2^5=32个条目
- 如果一个进程占用了8MB的内存空间,分成了2个4MB的连续空间,一共需要2个独立的、填满的2级索引表
- 意味着:64个1级索引表、2个独立的3级索引表、1个4级索引表
- 总共需要69个索引表,大概需要128Bytes * 69 ≈ 9KB的空间,相比于4MB,只有_1/464_
小结
- 多级页表节省了存储空间,但却带来了时间上的开销,是一种『以时间换空间』的策略
- 原本进行一次地址转换,只需要访问一次内存就能找到物理页号,就能计算出物理内存地址
- 但用了4级页表,就需要访问4次内存,才能找到物理页号
- 访问内存比访问Cache要慢很多!!
性能 + 安全
- 性能
- 机器指令里面的内存地址都是虚拟内存地址,每一个进程,都有一个独立的虚拟内存地址空间
- 通过地址转换来获得最终的实际物理地址
- 每一个指令都是放在内存里面,每一条数据都存放在内存里面
- 因此地址转换是一个非常高频的动作,地址转换的性能至关重要
- 安全
- 因为所有指令和数据都存放在内存里面,就不得不考虑内存安全问题
- 如果有人修改了内存里面的内容,CPU就可能会执行计划之外的指令
- 破坏服务器里面的数据、获取服务器里面的敏感信息
TLB – 加速地址转换
- 多级页表(空间换时间):节约了存储空间,但却带来了时间上的开销
- 程序所需要使用的指令,都顺序存放在虚拟内存里面(空间局部性);指令也是一条条顺序执行的(时间局部性)
- 因此对于指令地址的访问,存在空间局部性和时间局部性 – 缓存!!
- 计算机工程师专门在CPU里面存放了一块缓存芯片,称为TLB(Translation-Lookaside Buffer,地址变换高速缓冲)
- TLB里面存放了之前已经进行过地址转换的查询结果
- TLB与CPU Cache类似
- 可以分为指令TLB(ITLB)和数据TLB(DTLB)
- 可以根据大小对它进行分级,变成L1、L2 TLB
- 需要用脏标记位,来实现写回这样的缓存策略
- 为了性能,整个的内存转换过程也需要由硬件来执行
- 在CPU芯片里面,封装了内存管理单元(MMU,Memory Management Unit)芯片,用来完成地址转换
- 和TLB的访问和交互,都是由MMU控制的
安全性 + 内存保护
对于内存管理,计算机也有一些最底层的安全保护机制,这些机制统称为内存保护(Memory Protection)
可执行空间保护
- 对于一个进程使用的内存,只把其中的指令部分设置成可执行的
- 其实无论是指令还是数据,在CPU看来,都是二进制的数据
- 直接把数据部分拿给CPU,如果这些数据解码后,也能变成一条合理的指令,其实是可执行的
- 对于进程里内存空间的执行权限进行控制,可以使得CPU只能执行指定区域的代码
- 对于数据区域的内容,即使找到了其他漏洞想要加载成指令来执行,也会因为没有权限而被阻挡掉
地址空间布局随机化
- 内存层面的安全保护核心策略:在可能有漏洞的情况下进行安全预防
- 核心问题
- 其他的人、进程、程序,会去修改掉特定进程的指令和数据,然后,让当前进程去执行这些指令和数据,造成破坏
- 如果要想修改这些指令和数据,需要知道这些指令和数据所在的位置才行
- 原先一个进程的内存布局空间是固定的,任何第三方很容易就知道指令、程序栈、数据、堆的位置
- 地址空间布局随机化:让这些区域的位置不再固定,在内存空间随机去分配这些进程里不同部分所在的内存空间地址
- 如果随便做点修改,程序只会Crash掉,而不会去执行计划之外的代码
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.