``本文的一些截图来自于<IntelDEV卷3>和<x86汇编从实模式到保护模式>`
最近复习一些操作系统的知识,首先遇到了个坑便是计算机寻址问题.
本文是一些偏理论的东西(汇编可能在工作中用不到,需要的时候再深入研究吧- -!)
本文参考的一些博客和书本:
<a href=http://blog.csdn.net/trochiluses/article/details/8954527>[实模式与保护模式解惑之(一)——二者的起源与区别]</a>
汇编相关-从汇编研究局部变量机制
我看保护模式
x86汇编语言:从实模式到保护模式
实模式
- 是什么
INTEL 8086
CPU的寻址方式.
具体利用分段机制访问内存,访问时给出一个段基地址和一个段内偏移;(如DS:AX,其中DS和AX在这里的字长为16),访问的地址为实实在在的物理地址.
-
为什么
一个原因是
8086
特点 - 数据总线16位(字长为16)、地址总线却有20位(准确地说是CPU的寻址能力需要被设计成20位).这样程序员在没有特殊机制的引入时,只能访存
2^16 B = 64 KB
的空间(16位字长),达不到产品经理的需求(20位地址空间 = 2^20 B = 1 MB
)于是,这里设计成了使用
段基地址 + 段内偏移
的方式:物理地址 = 段基地址<<16 + 段内偏移
有个小问题就是这种方式实际上可访问的空间大于1M,即最大可以访问到0xFFFF*16 + 0xFFFF = 0x10FFEF > 0xFFFFF
,多出的这些地址将从0开始(即对1M取模). -
实模式的段的特点 -
每个段基地址都是16的倍数;
-
每个段的最小长度是16 Bytes,最大是64 KB;<----------更新: 段的大小从1B到64KB都可以.见wiki:
访问的每个地址都是实际的物理地址.
遗留的问题
系统程序与用户程序访问的地址共存于同一地址空间,并且一视同仁:
- 没有权限保护(用户可以随意访问1M的全部内存地址空间)
- 不支持多任务.
保护模式
保护模式之所以叫“保护模式”,因为他对多任务提供了保护,并加入了权限.
玛德我想起来了大二汇编时保护模式这一章跳过了因为期末不考沃日.
是什么
80386
及后续系列CPU所资磁的内存寻址方式.-
为什么
引入保护模式的目的有:- 保护 - 权限管理
- 多任务隔离
向下兼容 - ...
-
怎么做
这里简要介绍一下保护模式.寻址方式的改变:
实模式下 - 可以直接访问
段基地址(段寄存器16_bit)<<16 + 段内偏移
并做一些单字长操作(访存/赋值) .-
保护模式下 -
先来点废话:
由于需要引入权限管理, 需要:访存时指出当前执行指令的人有没有权限访问这个地址;
这句话至少包括了2点:- 当前指令的权限描述;
- 目标地址的权限描述.
所以当前的解决方案已经呼之欲出了:需要一个数据结构来描述内存地址.
保护模式下的寻址_part1段式(1MB --> 4GB 32Bit)
机器启动流程 - 一个PC启动时都会先进入实模式,接着由某指令转入保护模式.
-
段描述符表
- 是一堆段描述符的集合,有GDT和LDT.其中GDT是全局描述符表Global Descriptor Table
,该表是为整个OS服务的,在进入保护模式之前定义好(由bootloader). LDT,相对应地,则是局部段描述表,是每个进程自己独有的?.GDTR寄存器存放着GDT的地址.
理论上说GDT可以位于4GB中的任何一个位置,但因为需要从实模式转保护模式,所以一般只在1M以内的位置...(参见<x86从实模式到保护模式>)
-
描述符表项
- 无论是全局还是局部的描述符表,表项字段都如下图所示:(it‘s a fucking Data Structure!)
里面记录了这个段的:
- 段基地址BASE(32位一共,被分割成了3部分)
- 段界限LIMIT(20位)
- 段界限Granularity (Seg.LIMIT字段)的单位(0 - 1B/1 - 4KB).
段界限及其单位
表示 段内偏移的最值
(对于向上增长的段而言表示了最大值,而向下增长的段如堆栈段则是表示段内偏移的最小值哦)<------why?举个例子如下:(如有错误请指出)
0xFFFFFFFF |********|
...
0x00A01001 |********|<-----SS:EBP (指向栈底) }
0x00A00FFF |********|<-----SS:ESP (指向栈顶) }EBP,ESP两个的值可能都是段内偏移
...
0x00A00002 | |
0x00A00000 |********|<-----SS:<段界限*粒度+1> Suppose a MIN_VAL's linear addr
...
|
0x0000FF00 |********|
...
0x00000002 | |(每次存进来一个字2 bytes,所以地址上相差2)
0x00000000 |********|<-----SS:0
0.假设某程序运行时调用了一个函数,这时该程序的动作包括`保存现场`,`声明堆栈段`;我们关心的是声明堆栈段(用来存储函数参数、局部变量等).
1.这个段也有基址(我们假设为0x0,后面你就知道其实这个假设是ok的)
2.声明时要做的事是指定EBP、ESP(有点钦定的感觉),接着ESP减2,push进来函数参数之类的,每次push,ESP会自动减2(别问我为什么自动),每次pop,ESP会自动加2
3.上面所说的段内偏移的最小值,指的就是当ESP一直减,最多只能减到<段界限*粒度> ,再减就提示堆栈溢出了.
<a href=http://www.360doc.com/content/16/0223/12/28062682_536641957.shtml>汇编相关-从汇编研究局部变量机制</a>
所以现在一个段最大可以是4G,最小可以是1B.
理论上访问4G内存不再需要什么段啥的,一个段足矣(这种情况便是所谓的平坦模式):
-
段选择子
- 段寄存器CS/DS/SS/etc...此时不再是被用来左移并相加的对象,而是存放一个索引+标记+权限,这个索引指向了需要访问的段所在描述符.这些存放的内容被称为段选择子
.
由段选择子的索引量可以看出一个表最多有2^13=8192
个表项
(实际上32位处理器如80386,他们的段寄存器的长度为32位,只不过后面16位我们不可见,是处理器用作描述符高速缓存的) -
整体描述 (from high level)
-
权限 :
权限字 0 - 3(高 - 低),如下:
-
权限的判断机制:
- 代码段CS寄存器里的CPL为当前执行权限,称其为CPL(current);
- 数据段DS(或SS等,发出寻址请求指令所在寄存器)里的CPL为请求权限,称其为RPL(request);
- 段描述符中的段权限DPL;
3者进行一个判断,如果合法(比较复杂的判断,例如满足DPL>=CPL,DPL >= RPL,且CPL <= RPL等),则进行地址转换(此时得出的是虚拟地址,需要转为物理地址,转换又跟此时的另一些东西相关,见part2
),转换后最终寻址到所需地址.
总之,在保护模式下做什么事都得先进行权限检查.
PS: 这里的ring 0也就是所谓的内核态,而linux中的用户态是ring 3.
参考:<a href=http://blog.csdn.net/xiao_0429/article/details/47165169>我看保护模式</a>
(具体如何做的不深入了,涉及太多汇编相关的东西,知道就好)<-------fuck that!既然有兴趣读到这些知识,最好搞懂, 不然辜负这些知识.
保护模式下的寻址_part2段页式(更复杂的机制来了)
- 整理一下目前为止接触到的东西:
- 机器启动阶段先进入的是实模式(1M内存寻址空间,20Bit地址 + 16Bit数据), 然后由某指令转入保护模式.
-
保护模式_段式
- 最主要是为了解决权限问题,比如数据段不能被拿来当代码段运行,当前权限低的不能访问权限高的段等.
2.1 保护模式_段式 - GDT 和LDT(本来该设计是整个OS有一个GDT,每个进程有自己的LDT,而linux的进程极少使用LDT,基本上GDT和LDT起始为止都是0x00000000
)
2.2 GDT的地址和长度存放于GDTR(Global Descriptor Table Register),其中0~15
为GDT长度,16~47
为GDT所在地址.
- 保护模式_段式 - the entry of GDT(or LDT),每个表项描述了一个段.
- 段选择子 - 段寄存器(如CS,SS等)存放的内容包括INDEX、TI、RPL.
- 权限4个等级(特权0 - 内核态, 特权3 - 用户态)- also known as
ring 0 ~ 3
.不管做什么都需要进行权限判断。
- 目前为止,访问内存是这样访问的:
用户提供段选择子(Index,TI,RPL)和段内偏移(offset) |
GDTR/LDTR提供了GDT和LDT的地址和长度 |-->权限检查(结合代码段CS中的权限字,段选择子中的权限字和GDT/LDT表项中的权限字DPL进行检查)
-->找到Index所在段,取出其段基地址
-->把用户提供的段内偏移与段基地址结合构成一个完整的地址(这里称为**线性地址**)
也就是上面的图6表示的.
在单纯只有分段的情形下,这个线性地址就是物理地址.
- 目前为止,基于段的内存替换是这样进行的:
- 分段的缺点 - 内存外部碎片.(段大小不确定,使用一段时间之后,内存可能会有很多微小的空洞,不足以提供分配)===>因此
分页机制
就来了.
分页机制
- 大致描述:
- 4GB物理内存以4KB大小分为
4GB / 4KB = 1048576(1M)
个页,页也称为页框(page frame). - 引入
虚拟内存
的概念.对于每个进程而言,大家都有自己的4GB内存空间
,并且通过一定的手段(后面解释), 按页为单位映射到实际的物理内存上. - 映射表,上一点提到的虚拟内存中的页面与实际内存物理页面间通过映射表来联系,甚至每个进程都有自己的映射表, 另外这个映射表也很可能是分级的以解决空间利用效率.
- 页面的管理和页面的分配没有关系,线性地址(也就是段管理单元得到的地址)也与页面的分配没有关系.
- 4GB物理内存以4KB大小分为
- 详细深入:
- MMU(Memory Management Unit)是CPU中的内存管理单元.
- 页目录:分页机制实际上更复杂一些,上面说的"页表"一共有1M个,每个表项有4Bytes,那么整个表有4MB这么大. 每个进程有自己的页表,并且一般不会用到这么大,每次换入换出RAM是不是TM很烦?页目录就是解决这个的.(也就是上文里提到的页映射表分级问题)
- 4GB内存中一共有1M个页 --> 现在把这1M分成2个层次,即 1K * 1K,给第一个1K一个新的名字——页目录表(Page Dir. Tbl.),第二个1K是真正的页表. 页表的规模变小了但相应地数目变为1K个,所谓页目录表,就是存放这1K个页表的页.这里需要注意两种表的表项大小都是4B,所以两种表的大小正好都是4K即一个页的大小.
还是有点乱,见下面一系列的图.(退后 我要开始装逼了!)
Powered By Processon.com -
Naive的页表:
-
分层次的页表:
-
分段/分页的关系:
-
段页式物理地址的获取:
一些其他问题:
-
吐槽 - 为什么经常弄不懂呢?一部分原因是自己太懒,另一部分书确实也有些没讲清楚或者考试不考尼玛就跳过了.(归根到底还是自己的问题,别怪别人...)
- 目前关注的这些应该还算是寻址方式的一些东西,而上面有提到需要给进程分配页面,以及在该页面不怎么使用时换出,那么这些动作是怎么做的呢?
上面最后几个图如果看不清请猛戳<a href = https://www.processon.com/view/link/578a274ce4b0701cc02852c6> 我的文件</a>
欢迎大家纠错,共同进步.
写这篇的时候联想到的一些问题:(亟待深入)
快表? - ok.
缺页中断 ?
dirty ?
伙伴系统 ?
slab/slub ?
malloc ?
page cache / buffer cache 又是什么呢 ?