![]() |
计算机内存地址转换与分段 |
(2019-9-26)
|
本文是Intel兼容计算机(x86)的内存与 掩护系列文章的第一篇,连续了启动 疏导系列文章的主题,进一步 综合操作系统内核的工作流程 。与以往一样,我将 引用Linux内核的源代码,但对Windows只给出示例( 抱歉,我 忽略了BSD,Mac等系统,但大 部分的 探讨对它们一样 实用) 。文中假如有 舛误,请不吝赐教 。 在 支撑Intel的主板芯片组上,CPU对内存的 拜访是通过衔接着CPU和北桥芯片的前端总线来 实现的 。在前端总线上传输的内存地址都是物理内存地址,编号从0开始向来到可用物理内存的最高端 。这些数字被北桥映射到实际的内存条上 。物理地址是明确的、最后用在总线上的编号, 无须转换, 无须分页,也没有特权级 审查 。但是,在CPU内部,程序所 使用的是逻辑内存地址,它必须被转换成物理地址后, 能力用于实际内存 拜访 。从概念上讲,地址转换的过程如下图所示:
x86 CPU开启分页 性能后的内存地址转换过程
此图并未指出详实的转换 模式,它仅仅 形容了在CPU的分页 性能开启的状况下内存地址的转换过程 。假如CPU关闭了分页 性能,或运行于16位实模式,那么从分段单元(segmentation unit)输出的便是最后的物理地址了 。当CPU要执行一条 引用了内存地址的指令时,转换过程就开始了 。第一步是把逻辑地址转换成线性地址 。但是,为什么不跳过这一步,而让软件直接 使用线性地址(或物理地址呢?)其理由与:"人类为什么要长有阑尾?它的重要作用仅仅是被 感化发炎而已" 大体 雷同 。这是进化过程中产生的神奇 构造 。要真正 了解x86分段 性能的设计,我们就必须回溯到1978年 。 最初的8086 解决器的 存放器是16位的,其指令集大多 使用8位或16位的操作数 。这使得代码 能够操纵216个字节(或64KB)的内存 。但是Intel的工程师们想要让CPU 能够 使用更多的内存,而又不用 扩充 存放器和指令的位宽 。于是他们引入了段 存放器(segment register),用来告诉CPU一条程序指令将操作哪一个64K的内存区块 。一个 正当的解决 方案是:你先加载段 存放器,相当于说"这儿!我打算操作开始于X处的内存区块";之后,再用16位的内存地址来 示意 有关于那个内存区块(或段)的偏移量 。总共有4个段 存放器:一个用于栈(ss),一个用于程序代码(cs),两个用于数据(ds,es) 。在那个年代,大 部分程序的栈、代码、数据都 能够塞进对应的段中,每段64KB长,所以分段 性能 时常是透明的 。 现今,分段 性能依旧存在,向来被x86 解决器所 使用着 。每一条会 拜访内存的指令都隐式的 使用了段 存放器 。 比方,一条跳转指令会用到代码段 存放器(cs),一条压栈指令(stack push instruction)会 使用到堆栈段 存放器(ss) 。在大 部分状况下你 能够 使用指令明确的改写段 存放器的值 。段 存放器存储了一个16位的段 取舍符(segment selector);它们 能够经由机器指令( 比方MOV)被直接加载 。唯一的例外是代码段 存放器(cs),它不得不被影响程序执行顺序的指令所转变, 比方CALL或JMP指令 。 固然分段 性能向来是开启的,但其在实模式与 掩护模式下的运作 模式并不 雷同的 。 在实模式下, 比方在 疏导启动的初期,段 取舍符是一个16位的数值, 批示出一个段的开始处的物理内存地址 。这个数值必须被以某种 模式放大,不然它也会受限于64K当中,分段就没故 意思了 。 比方,CPU可能会把这个段 取舍符当作物理内存地址的高16位( 只有将之左移16位,也便是乘以216) 。这个 容易的 规定使得: 能够按64K的段为单位,一块块的将4GB的内存都寻址到 。遗憾的是,Intel做了一个很诡异的设计,让段 取舍符仅仅乘以24(或16),一举将寻址 规模 制约在了1MB,还引入了 适度复杂的转换过程 。下述图例显示了一条跳转指令,cs的值是0x1000:
实模式分段 性能
实模式的段地址以16个字节为步长,从0开始编号向来到0xFFFF0(即1MB) 。你 能够将一个从0到0xFFFF的16位偏移量(逻辑地址)加在段地址上 。在这个 规定下,关于同一个内存地址,会有多个段地址/偏移量的组合与之对应,并且物理地址 能够超过1MB的边界, 只有你的段地址足够高(参见 臭名远扬的A20线) 。同样的,在实模式的C语言代码中,一个远指针(far pointer)既包含了段 取舍符又包含了逻辑地址,用于寻址1MB的内存 规模 。真够"远"的啊 。随着程序变得越来越大,超出了64K的段,分段 性能以及它 怪僻的 解决 模式,使得x86平台的软件开发变得十分复杂 。这种设定可能听起来有些诡异,但它却把当时的程序员推动了令人 瓦解的深渊 。 在32位 掩护模式下,段 取舍符不再是一个单纯的数值,取而代之的是一个索引编号,用于 引用段 形容符表中的表项 。这个表为一个 容易的数组,元素长度为8字节,每个元素 形容一个段 。看起来如下:
段 形容符
有三 品种型的段:代码,数据,系统 。为了简洁明了,惟独 形容符的共有 特色被绘制出来 。基地址(base address)是一个32位的线性地址,指向段的开始;段 界线(limit)指出这个段有多大 。将基地址加到逻辑地址上就 构成了线性地址 。DPL是 形容符的特权级(privilege level),其值从0(最高特权,内核模式)到3(最低特权,消费者模式),用于操纵对段的 拜访 。 这些段 形容符被 保留在两个表中:全局 形容符表(GDT)和 部分 形容符表(LDT) 。电脑中的每一个CPU(或一个 解决核心)都含有一个叫做gdtr的 存放器,用于 保留GDT的首个字节所在的线性内存地址 。为了选出一个段,你必须向段 存放器加载 相符以下 格局的段 取舍符:
段 取舍符 对GDT,TI位为0;对LDT,TI位为1;index指出想要表中哪一个段 形容符(译注:原文是段 取舍符,应该是笔误) 。关于RPL, 申请特权级(Requested Privilege Level),以后我们还会 详尽 探讨 。现在,需求好好想想了 。当CPU运行于32位模式时, 无论 怎么, 存放器和指令都 能够寻址整个线性地址空间,所以 根本就不需求再去 使用基地址或 其余什么鬼东西 。那为什么不 索性将基地址设成0,好让逻辑地址与线性地址 统一呢?Intel的文档将之称为"扁平模型"(flat model),并且在现代的x86系统内核中便是这么做的(特殊指出,它们 使用的是 根本扁平模型) 。 根本扁平模型(basic flat model)等价于在转换地址时关闭了分段 性能 。如此一来 如许 好看啊 。就让我们来看看32位 掩护模式下执行一个跳转指令的例子,其中的数值来自一个实际的Linux消费者模式 利用程序:
掩护模式的分段
段 形容符的内容一旦被 拜访,就会被cache(缓存),所以在随后的 拜访中,就不再需求去实际读取GDT了,不然会有损性能 。每个段 存放器都有一个 潜藏 部分用于缓存段 取舍符所对应的那个段 形容符 。假如你想了解更多细节,包含关于LDT的更多信息,请参阅《Intel System Programming Guide》3A卷的第三章 。2A和2B卷讲述了每一个x86指令,同时也指明了x86寻址时所 使用的各 品种型的操作数:16位,16位加段 形容符(可被用于实现远指针),32位,等等 。 在Linux上,惟独3个段 形容符在 疏导启动过程被 使用 。他们 使用GDT_ENTRY宏来定义并存储在boot_gdt数组中 。其中两个段是扁平的,可对整个32位空间寻址:一个是代码段,加载到cs中,一个是数据段,加载到 其余段 存放器中 。第三个段是系统段,称为 使命状态段(Task State Segment) 。在 实现 疏导启动以后,每一个CPU都 占有一份属于自己的GDT 。其中大 部分内容是 雷同的,惟独少数表项依赖于正在运行的 历程 。你 能够从segment.h看到Linux GDT的布局以及其实际的样子 。这里有4个重要的GDT表项:2个是扁平的,用于内核模式的代码和数据,另两个用于消费者模式 。在看这个Linux GDT时,请 留神那些用于确保数据与CPU缓存线对齐的填充字节—— 目标是克服冯·诺依曼瓶颈 。最后要说说,那个经典的Unix 舛误信息"Segmentation fault"(分段 舛误)并不是由x86 格调的段所引起的,而是因为分页单元检测到了非法的内存地址 。唉呀,下次再 探讨这个话题吧 。 Intel 奇妙的绕过了他们原先设计的那个拼拼凑凑的分段 步骤,而是提供了一种富于弹性的 模式来让我们 取舍是 使用段还是 使用扁平模型 。因为很方便将逻辑地址与线性地址合二为一,于是这成为了 标准, 比方现在在64位模式中就强制 使用扁平的线性地址空间了 。但是 即便是在扁平模型中,段关于x86的 掩护机制也十分重要 。 掩护机制用于抵制消费者模式 历程对系统内核的非法内存 拜访,或各个 历程中间的非法内存 拜访,不然系统将会进入一个狗咬狗的世界!在下一篇文章中,我们将窥视 掩护级别以及如何用段来实现这些 掩护 性能 。 |
![]() |
百度中 计算机内存地址转换与分段 相关内容 |
![]() |
Google搜索中 计算机内存地址转换与分段 相关内容 |