Linux内核笔记001 - Intel X86 CPU 系列的寻址方式
1. 段寄存器
上图是Intel X86系列的CPU,在Pentium之前的发展过程:
- 4004:8位CPU
- 8086~80286:16位CPU,即逻辑运算单元(ALU)的位数为16,但地址总线为20位,另外80286开始支持保护模式,但只支持实模式到保护模式的单向切换
- 80386:32位CPU, ALU为32位,地址总线也设计成32位了
我有个自我认识,就是在学习的时候,比较喜欢钻牛角尖,比如我就为这些问题纠结过:
- 8086为什么要把地址总线设计成20位?
- 地址总线既然可以设计成20位,为什么不设计成32位?
- 地址总线可以设计成20位,为什么不把ALU也设计成20位?
- 等等..
导致本来的目的是学习内核,结果差点被这些不相关的问题吓跑了。不过应该会有人比我更有这种性格:在第一章即使被不相关的问题卡住,也绝不继续学第二章!确实,自学一样很难的东西时,需要有这种性格,有时候只有逼着自己更加深入的去体会,才能推翻之前的错觉,然后很多东西都也随之豁然开朗了;但如果过于执着,就变成钻牛角尖了,内核中凝聚了非常多抽象和复杂的设计,很多时候不是按照顺序学习一遍,就能明白ABC,而是要一知半解的重复着ABC、ACB、BCA、..,这样去学习。
特别是有些明显偏离学习目标的问题,要懂得放下,或者猜个差不多的答案,安抚下自己的牛脾气就可以了。比如以上问题我猜测的原因是:
- 地址总线设计成16位,只能支持64KB的内存,已经不满足当时计算机用户的需求了
- 寻址能力大了,主板上实际用于存储的晶体管也要相应多才行,但当时的CPU制作工艺水平还有限
- ALU涉及到的电路复杂,不能像地址总线一样扩展成20位
以上是我学习内核的一点感受,接下来介绍正题:
1.1. 段寄存器诞生的原因
地址值经常要参与运算,比如指令跳转时,要加偏移值,而8086 CPU中,ALU是16位,没办法一次性运算20位的地址值,所以设计了段寄存器,20位地址由两个16位的存储单元保存,并且通过"段寄存器*16+段偏移",可以计算得到20位的实际地址。
1.2. 段寄存器为什么不只设计成4位
4位只能指定2^4个段基址,1MB空间只能被划分为0-64KB、64KB-128KB、..、960KB-1MB这样的16个段,每个段64KB,而16位可以指定2^16个段基址,从离每个段开始很近的地方,就可以指定为另外一个段,可以为每个程序安排自己独立的段,内存布局整体上会更灵活和更清晰。
其实,以上所有乱七八糟的问题和理解都不重要,重要的只有一个:段寄存器本质上是为了解决ALU和地址总线位数不一致的问题。
2. 80386 CPU
2.1. 实模式、保护模式
实模式和保护模式,指的是CPU硬件的不同状态,就像《秦时明月》中天明拿的"非攻"一样,它是墨家打造的一件多功能兵器,可以变换成不同的形状,同样一块黑铁,在不同形状下,用途也不同。同样的,CPU内部的一些元件,在实模式和保护模式下,作用可能也不相同。
2.2. 80386的选择
- 保留段寄存器
80386的ALU和地址总线都是32位,按道理段寄存器在80386中就没有存在的意义了,但是为了兼容之前的CPU,必须保留段寄存器,否则可以在之前的CPU运行的程序,到80386上就运行不了了,因为它连段寄存器都没有。
- 基于段寄存器实现保护模式
80386除了考虑兼容,还要扩展一个重要的特性,就是支持保护模式。还是拿"非攻"举例, "非攻"中的所有器材,在各种形态下,都会发挥作用,这叫物尽其用,特别对于CPU来说,如果切换到保护模式,段寄存器就空闲在一旁不起任何作用,一方面从制作工艺的角度不允许,因为CPU中每一寸空间都很宝贵,另一方面从生产成本上也不允许,资产家希望花了成本做出来的东西,要竭其所能的工作,不能闲着。
结论:Intel工程师不能在80386中丢弃段寄存器,同时对保护模式的设计思路,还得想办法把段寄存器用上。
3. 保护模式下的段式管理
3.1. 特权指令
Linux操作系统以及系统中运行的一些管理平台程序,都有用户管理的功能,并且有root用户、普通用户之分,这些都是通过纯软件的设计实现的。特权指令、非特权指令针对的是硬件层面,特权指令就是指,在保护模式下,标志寄存器中的某些位是1,才能执行,否则就让执行这条指令的程序异常退出。
比如我记得很小的时候,有些自行车刚买回来,就自带一把锁,同样是"骑"这个动作,如果锁是开的,就能骑走,如果锁是关的,就骑不走,这就是通过硬件的设计,可以导致同一条指令执行结果不同的道理。
为了新增保护模式,80386相应的新增了一些寄存器和特权指令,由于是新增的,以及"权"的信息怎么存、怎么使用,都不用考虑对低型号CPU的兼容性问题。
3.2. 保护模式下段寄存器含义
类似于"非攻"中元件,在不同形态中可以设计为完全不同的作用,段寄存器在保护模式中,也已经不再跟实模式一样纯粹指向一个段基址了,而是设计成上图中的结构:
- 高13位:描述符表索引/数组下标(详见笔记3.4节)
- TI位:标识使用GDTR指向的描述符表,还是LDTR指向的描述符表(详见笔记3.4节)
- 最低2位:RPL(Request Privilege Level) ,相关概念还有DPL(Descriptor Privilege Level) 、CPL(Current Privilege Level),含义都由硬件设计定义,用于实现特权指令,软件设计要遵守硬件的规范(详见笔记3.4节)
3.3. 段选择子
GDTR和LDTR是80386新增的寄存器,分别用于指向全局描述符表和局部描述符表,这样,段寄存器的TI位用于表明使用哪个表,再配合高13位的索引,就可以指定一个描述符。
描述符的结构为:
- B31~B24 + B23~B16 + B15~B0:段基址,共32位
- L19~L16、L15~L0:段长度,共20位
- G:0=段长度单位为1,1=段长度单位为4KB
- D:存取内存默认大小,0=16位,1=32位(暂时不用理解,opcode 66H前缀可以切换指令存取内存的默认大小)
- A:是否可供系统软件使用(暂时不用理解)
- P + DPL + S + type:
- P:是否在内存中(暂时不用理解,用于实现"交换分区"功能)
- DPL:本段特权(详见笔记3.4节)
- S:0=系统描述符(比如指向任务状态段TTS),1=代码或数据段描述符(暂时不用理解)
- E:0=数据段,1=代码段(暂时不用理解)
- D(E=0):0=堆区,1=栈区;C(E=1):0=忽视特权级,1=遵守特权级(暂时不用理解)
- W(E=0):0=不可写,1=可写;R(E=1):0=不可读,1=可读(暂时不用理解)
- A:CPU任何一次访问该描述符指向的段,都会通过硬件层的逻辑,将A设置为1(暂时不用理解,用于选择换出到交换分区的段)
这个结构中的大部分成员,随着后面的学习,都会逐个接触到,那么除了暂时不用理解的,就只剩下段基址、段长度了。很多人都会有疑问,为什么它们的位都不连续?那是因为Intel最初设计的80386只支持24位寻址(B23~B16 + B15~B0),后来很快又不满足需求了,又将寻址位数扩展到了32位。
3.4. 段式寻址过程
CPU内部是非常复杂的电路逻辑,如果按照软件的思维,把段式寻址这套电路逻辑理解成一个接口,那么段寄存器、GDTR/LDTR寄存器就是参数。
当软件设置好这些参数后,CPU的硬件执行过程是这样:
① 对比CPL和段寄存器中的"RPL",判断是否越权;
② 根据段寄存器中的TI位,加载全局或局部描述符表;
③ 根据段寄存器中的描述符表索引,加载指定的描述符;
④ 对比描述符中的"段长度"和指令中的"逻辑地址",判断是否越界;
⑤ 对比描述符中的"DPL"和段寄存器中的"RPL",判断是否越权;
⑥ 访问描述符中"段基址"+指令中"逻辑地址"得到的物理地址。
3.5. 平坦(Flat)地址
其实,80386后来又支持了另外一种更先进的内存管理:页式内存管理。而且类似于不能丢弃段寄存器,还要基于段寄存器设计保护模式,它的页式内存管理,又是基于段式内存管理设计的,从而导致很多学习这款CPU的人很讨厌它,但是如果完整的了解过Intel CPU的发展过程后,自然就认为情有可原了。
页式内存管理,和段式内存管理,本来是两个相互独立的管理方式,但是Intel新增页式内存管理的时候,80386已经发布过一段时间了,仍然是考虑到兼容性等问题,使得它必须先执行完段式寻址,才能进行页式寻址,目前,学习还只是刚开始,只了解了80386寻址相关的硬件特性,可能还理解不了这个特点意味着什么,后期学习到书本的第2.1节"Linux内存管理的基本框架"时,自然就能体会到了。
总结
本篇笔记,记录了段式寻址过程,过程本身很简单,但是涉及到的一些设计思维很抽象。 我刚接触计算机时,觉得它很神秘,像人一样,能"播放"音乐给我听,能"回答"我搜索的问题,导致我差点都不敢去探索它,不过我想,天才都已经把它从无到有制造出来了,我只是去学习一个已经存在的东西,还要畏惧吗?打开开关,灯就亮了,踩下自行车,*就转了,这些简单的物理反应,我习以为常,点击"播放",电脑发出声音,难道不就是一个更复杂的物理反应而已嘛。