大学也上过微机原理,但那个时候整天玩PHP,VC++,C# .net...数据库管理系统...没有意识到她是一门非常重要的课。于是上这些基础课的时候都在下面看那些xxx从入门到精通之类的书了或者干脆翘了去图书馆看(当然我不会否认正是因为这些书让我玩计算机的兴趣坚持了下去)。 大学毕业同学都把书卖了。我当时感到像微机原理组成原理之类的书可能以后有用,于是没有卖(因为我知道自己的兴趣所在)。 毕业后感觉那些xxx从入门到精通之类的书太无聊也太多。更多的是兴趣吧,于是慢慢的啃起微机原理,操作系统来了。 废话还是少说,总结下80386保护模式内存寻址。防止以后忘了可以看看,再者希望有相同兴趣的人看到了减少他们的弯路和指出我理解的错误。
一、概括对比下8086和80386的寻址
8086的逻辑地址由16位的段地址和16位的段内偏移组成。 寻址方式: 段地址由用户程序指定,保存在段寄存器中。以后程序直接使用段内偏移地址(就是段内的偏移量)就行了。内存寻址时(如何得到物理内存地址): 1.取得相应的段地址(保存段寄存器中)。 2.取得偏移地址(这个一般都是指令中给出的)。 3.经过一个运算规则A 4.得到物理内存地址
80386的逻辑地址是也分两部分:16位段选择子+32位段内偏移。 寻址方式: 逻辑地址(这里用虚拟地址合理)在程序编译时候决定(可能是操作系统,编译器,连接器给出的)。 内存寻址时: 1.取得相应的段选择子(保存段寄存器中)。(段选择子其实还是在16位段寄存器中。只是名字改了而已。后面会说明为什么) 2.取得偏移地址(这个一般都是指令中给出的)。 3.经过一个运算规则B 4.得到物理内存地址(如果没有分页)
从上面可以看出从8086到80386段内存管理是本质上是一样的。都是通过段寄存器和偏移地址来寻址: 寻址方式可表示成 段寄存器:[段内偏移] 不同的地方是从8086到80386上面的步骤3中的运算规则和偏移地址长度改变了而已。
现在具体写下步骤3中的地址运算规则: 运算规则A(8086): 20位的物理地址 = 段地址(16位)*10h + 段内偏移(16位) 这就是传说中的段地址左移四位+偏移地址形成20位物理地址。 运算规则B(8386): 1.根据段选择子找到段基地址(32位) 2.32位物理地址 = 段基地址(32位) + 段内偏移(32位) 之所以叫80386改名叫选择子时因为段基地址是由段寄存器中的内容选择出来的。其实这一切还是很简单。要说复杂那就是80386多了些计算步骤而已。但是不用担心。这些寻址由硬件完成不用你自己计算。
最后要说明的是80386还多了一个分页的功能,这个功能是可选的。要是启用分页,上面得到地址其实是线性地址,还要变换才能形成物理地址。
二、相关寄存器和数据结构
这个地方可以先随便看看(但是很重要的其实),不懂的绕过。等看到看下面可以返回来查阅。
Descriptor 描述符 一个固定结构的结构体(struct)长度是8字节也就是64位,那么这个结构体是干嘛的呢?用来描述一个段基地址。这个结构体内容包括段基地址,段长度,段属性。这个被描述的段基地址就是上面我们说的 32位物理地址 = 段基地址(32位) + 段内偏移(32位) 中的段基地址。 描述符有两种,也就是说这种结构体有两种不同的结构(其实其中包含的内容都几乎一样只有一些细小的差别)。一种用来描述数据段,代码段和堆栈段的,称为非系统描述符,另一种就是用来描述LDT和TSS的,(LDT和TSS在后面有说明)称为系统段描述符。
GDT 全局描述符表(Global Descripter Table) 顾名思义,就是一个表而已。这个表存储在内存中,相当于一个结构体数组。数组的每一项就是上面所说的描述符了。这个表中可以包含四种信息的描述符。 1.全局的数据段代码段堆栈段,这些段一般操作系统内核使用。(每个单独的用户任务由LDT描述,LDT后面有说明) 2.对LDT的描述 这个描述符的基址就是是LDT所在内存中的起始地址 3.对TSS的描述 这个描述符的基址是TSS所在内存中的起始地址 4.一些门描述符(调用门,中断门...) 其中#1用的是上面说的非系统段描述符,#2,#3,#4用的是系统段描述符。不同门买描述符,LDT,TSS虽然都用系统段描述符但是其中一些属性值用拉区分不同的描述符。 BTW:LDT,TSS都是属于每个任务的所以一般成对出现在GDT中。
GDTR 全局描述符表寄存器(Global Descripter Table Register) GDTR就是一个寄存器,和AX BX.. CS DS...一个概念。这个寄存器共有48位。其中32位地址指出GDT在内存中的位置(可以说这个是一个32位的物理地址,如果没有分页开启),另外16位为GDT的大小(GDT最多可以有多少项)。
LDT 局部描述符表(Local Descripter Table) 和GDT的结构差不多,所不同的是LDT只是包含对具体每个用户任务的代码段堆栈段数据段描述(当然就没有包含对TSS,和LDT的描述项了).
LDTR 局部描述符表寄存器(Global Descripter Table Register) LDTR和GDTR一样也是寄存器,不同的是它只有16位大小。为什么不和GDTR不是48位呢?原因如下: 1.GDT在一个多任务系统中一般只有一个,所以基址由一个GDTR确定,之后几乎不会改变。但是LDT的数量是可以和用户任务相等的,就是每个用户任务都可能拥有自己的LDT,这里LDTR不方便和GDTR一样使用一个48位的寄存器给出基址,因为任务的切换这个之前基址就丢失了。任务就切换不回来了。或者任务切换回来重建自己的LDT,效率很低。 2.LDTR中存放的是一个16位选择子,根据这个选择子去在GDT中寻址找到LDT的基址。换句话说:全局描述符表(GDT)的基址是存在GDTR中的(就是说GDT靠GDTR定位),而局部描述符表(LDT)的基址是存在GDT中的,作为GDT这张表中的一项描述的(注意,前面我们说过GDT中描述符项包括对LDT段的描述)。 LDTR当作为选择子,LDT作为一项在GDT中描述好处在于:每个用户任务的局部描述符表(LDT)的起始内存地址放在GDT中,这样切换任务的时候只要改变选择子就能实现LDT的切换,而且由于每个LDT在GDT中都有描述,不会丢失,方便任务切换回来。
TR 任务寄存器(Task Register) 是一个16为的选择子,作用和LDTR类似,都是用来索引全局描述符表(GDT)中的一项。所不同的是TR选择的描述符描述的不再是LDT的段了。TR选择的描述符描述的一个任务状态段(TSS:Task Status Segment)。
TSS 任务状态段(Task State Segment) 正如前面所说,任务状态段(TSS)是在GDT中描述的。任务状态段是做什么的呢?任务状态段就是内存中的一个数据结构。这个结构中保存着和任务相关的信息。当发生任务切换的时候会把当前任务用到的寄存器内容(CS EIP DS SS...)包括LDT的选择子等保存在TSS中以便任务切换回来时候继续使用。
其实除了上述寄存器以外,还有一些不可访问的隐藏寄存器,这些隐藏寄存器其实是高速缓冲寄存器,可以忽略它的存在。为了降低理解复杂度上面没有写出。将在下面部分给出解释。
Selector 选择子 按照用途共有三种(其实就是一种,因为格式完全一样), 1.TR中的选择子,用来从GDT中选择一个TSS的描述符。 2.LDTR中的选择子,用来从GDT中选择一个LDT的描述符。 3.用户程序中的逻辑地址(虚拟地址,这个地址48位 = 16位选择子+32位偏移地址)中包含的选择子。用来选择程序用到的段在LDT(或者GDT)中的描述符(数据段,代码段...)。这个选择子由编译或者连接或者操作系统决定的.
三、分页
CR0中保存一个页基址A,线性地址经两级页变换成物理地址:具体见下图(摘自80386 manual).线性地址从高到底被分成三个部分高10位B,中间10位C,末12位D。变换过程不再描述(语言描述可能看上去复杂,其实很简单)。这里给出一个变换公式比较明了: 页基址 = A; 页基址 = 页基址 + B * 4; /* 查一级目录页表,在(页基址+B*4)处取得二级页表的的基址,这里等号的意思是查表 */ 页基址 = 页基址 + C * 4 ; /* 查二级目录页表,在(页基址+B*4)处取得物理基址,这里等号的意思是查表 */ 物理地址 = 物理基址 + D; 为什么要X4?因为页表按4位对齐的。页目录项后四位不用。 顺便说下选择子去GDT/LDT中索引是: 描述符地址 = Base+Selector*8.为什么要X8?道理类似,GDT/LDT中一项8字节。
四、80386内存管理和任务切换(具体过程)
一个80386操作系系统中运行两个用户任务A和B 具体如下图:
这个过程没有画出包括上门说的不可访问的隐藏寄存器。实际上,TR,LDTR之后都有一个64位(内容和描述符一样)外部不可访问的高速缓冲寄存器。内容为当前选择子对应的描述符。这样只有在切换任务的时候才去内存中GDT索引。具体任务在执行代码或者数据寻址时CPU直接读取高速缓冲寄存器中保存的某个和TR或者LDTR相对应的描述符。