从计算机的历史谈起
以前的程序开发是
直接操作物理内存的
CPU指令的操作数
直接使用实地址(实际内存地址)
任何一个程序员都拥有全部内存操作权力
拥有全部内存操作权力带来的问题
难以重定位
程序必须固定在内存的某个位置,因为加载地址必须明确指定
给多个程序设计带来问题
只要两个程序碰到内存重叠,程序就不能运行
CPU历史的里程碑-8086
地址线宽度为
20位,可访问
1M内存空间
引入[
段地址:
偏移地址]的内存访问形式
8086的段寄存器和通用寄存器为
16位
单个寄存器寻址最多访问
64k的内存空间
需要
两个寄存器配合来完成所有内存空间的访问
深入解析[段地址:偏移地址]
硬件所做的工作
段地址左移4位,构成20位的基地址(起始地址)
基地址+偏移地址=实地址
对于开发者的意义
更有效的划分内存的功能(数据段,代码段,等)
当出现程序地址冲突时,通过修改段地址解决冲突
示例
mov ax, [0x1234] ; 实地址 ==> (ds << 4) + 0x1234
mov ax, [es:0x1234] ; 实地址 ==> (es << 4) + 0x1234
有趣的问题
[段地址:偏移地址]能访问的最大地址为
0xFFFF:0xFFFF,
即:
0x10FFEF;
超过了1MB(0xFFFFF)的空间,CPU如何处理?
8086中的高端地址区(High Memory Area)
0xFFFF:0xFFFF
0xFFFF0+0xFFFF
0xFFFF0+(0xF+0xFFF0)
(0xFFFF0+0xF)+0xFFF0
0xFFFFF+
0xFFF0(HMA)
8086的处理方式
由于8086只有20位地址线,因此
最高位被丢弃(溢出),对内存地址的访问相当于取模1MB
0xFFFF:0xFFFF
100001111111111101111(0x10FFEF)
回卷0xFFEF(舍弃最前面的1)
再谈8086历史
8086在当时是非常成功的一款产品
因此拥有一大批的开发者和应用程序
各种基于8086程序设计的技术得到了发展
不幸的是,各种奇技淫巧也应运而生
8086时期应用程序中的问题
1M内存完全不够用
开发者在程序中大量使用内存回卷技术
应用程序之间没有界限,相互之间随意干扰
A程序可以随意访问B程序的数据
C程序可以修改系统调度程序的指令
80286的登场
8086已经有那么多应用程序了,所以新CPU必须向下兼容
加大内存容量,增加地址线数量(24位,16M ==> 0xFFFFFF)
[段地址:偏移地址]的方式可以强化一下
为每个段提供更多属性(如:范围,特权级,等)
为每个段的定义提供固定方式
80286的兼容模式
默认情况下完全兼容8086的运行模式(实模式)
默认可直接访问1MB的内存空间
通过特殊的方式访问1MB+的内存空间
80286之后的工作模式
初识保护模式
每一段内存都拥有一个属性定义(
描述符Descriptor)
所有段的属性定义构成一张表(
描述符表Descriptor Table)
段寄存器保存的是属性定义在表中的索引(
选择子Selector)
描述符(Descriptor)的内存结构
因为一些历史原因,段基址被分成了几段
GDT每个位置的意义如下:(引用于:https://www.cnblogs.com/Philip-Tell-Truth/p/5211248.html)
G位(Granularity,粒度)
这个位用于解释界限的含义,当G=0,段界限是以字节为单位的,这个时候,段的拓展范围是1B-1MB(段描述符的段界限是20位的);如果G=1,则段界限是以4KB为单位的,范围4KB-4GB。
D/B位(Default Operation Size,默认的操作数大小)
对于代码段,当D=0表示指令的偏移地址或者操作数是16位的;D=1表示偏移地址或者操作数是32位的.
对于栈段,当D=0,表示使用SP寄存器,栈段上界是0xFFFF;当D=1,表示使用ESP寄存器,栈段上界是0xFFFFFFFF。
L位(64-bit Code Segement)
这个位用于保留给64位处理器使用,当L=0,表示是32位处理器,当L=1,表示是64位处理器。
AVL位(Available)
通常由操作系统用,处理器并不会使用它。
P位(Segement Present,存在位)
P位用于描述描述符对应的段是否存在,一般来说,描述符所指示的段都是存在于内存中的,但是,当内存空间紧张的时候,有可能只是建立了描述符,对应的内存空间并不存在,这个时候就应该把P位清零。表示段并不 存在,另外,同样是在内从空间紧张的情况下,会把很少用到的段换出到硬盘中,腾出空间给当前急需内存的程序使用(当前正在执行的),这时,同样要把段的描述符P位清零,当再次轮到它执行时,P=1.
P位是处理器负责检查的,每当通过描述符访问内存的段中时,如果P位是0,则处理器会产生一个异常中断,通常,这个中断处理过程是操作系统提供的。该处理过程的任务是负责将该段从硬盘中换回内存,并将P=1。在多用户,多任务的系统中,这是一种常用的虚拟内存调度策略。
DPL位(Descriptor Privilege Level,DPL特权级)
32位处理器的DPL位有四种,分别是0,1,2,3(就是特权级0123),不同特权级的程序是相互隔离的,其访问是严格限制的,而且有些处理器指令(特权指令)只能由0特权级的程序来执行。在这里,DPL指的是访问该段所必须拥有的最低特权级,如果这里的数值是2,那么特权级0,1,2可以访问这个段,特权级3访问这个段会被处理器阻止。
S位(Descriptor Type,描述符类型)
当S=0,说明这是一个系统段,当S=1,说明这是一个代码段或者是数据段(栈段是特殊的数据段)。
TYPE位(描述符类别)
X表示是否可执行(eXecutable)。数据段总是不可执行的,X=0,代码段可执行,X=1。
E是对数据段而言,指示段的扩展方向,E=0表示向上拓展,E=1表示向下拓展。
W段指示读写属性,W=0指示不可写入,否则会引发处理器异常中断,W=1表示允许写入。
对代码段而言,
C表示是否特权级依从(Conforming),C=0表示非依从的代码段,这样的代码段可以和他特权级相同的代码段调用。或者通过们调用,C=1表示允许从低特权级的程序转移到该段执行。代码段总是可以 执行,但是总是不允许被修改(如果要修改代码段,可以指定一个可以读写的数据段指向这个代码段)。至于能不能读出,由
R位决定,R=0表示不能读出,R=1表示可以读出。(相当于一个ROM)(R位不是指示处理器能否读取指令的,而是限制程序和指令的行为,比如使用超越前缀CS:访问代码段的内容)。
A位是已访问位(Assessed),指示这个段最近有没有被访问过,在描述符创建后,这个位置应该被置零。之后,每当这个段被访问的时候,处理器都会将这个位变成1,对这个位清零是操作系统做的,通过定期监视该段 的位置,可以统计出该段的使用频率,当内存空间紧张的时候,可以不经常用的段退到硬盘上,从而实现虚拟内存管理。
描述符表(Descriptor Table)
选择子(Selector)的结构
进入保护模式的方法
定义描述符表
打开A20地址线
加载描述符表
通知CPU进入保护模式
小结
[段地址:偏移地址]的寻址方式解决了早期程序重定位的难题
8086实模式下的程序
无法保证安全性
80286提出了
保护模式的概念,加强了内存段的安全性
处于兼容性的考虑,80286之后的处理器都有2中保护模式
处理器需要
特定的设置步骤才能进入保护模式,默认为
实模式