从实模式到保护模式跳转的详解

从实模式到保护模式跳转的详解

三年前拿到《自己动手写操作系统》第一版的时候,虽然很有兴趣,但是没有时间详细地
看进去,直到前段时间又看到它的第二版出来,兴趣又提上来了。正好此时的工作之余可以
深入地研究一下。

 第一遍看这段代码,没懂(我承认是个很不聪明的人),只看懂了下面中文描述,说是
从实模式跳到保护模式的。
 第二遍看这段代码,似乎懂了一些,大约20%吧。
 第三遍,第四遍,第五遍...................第n遍,我懂了,我承认古代的大哥们是不骗
人的,他们都说书读百遍,其义自见。
 第n+1遍,我发现作者自己似乎没有懂。我用似乎是说作者也许懂了,但他的描述让别人
不好懂。

 于是我这个觉得自己懂的人来做一个详细的解释。


Descriptor是一个宏,定义一个8字节大小的数据结构。然后把三个参数%1,%2,%3按一定规则
自动填充到这个结构中。这个比较简单,没有什么好说的。

对于[SECTION .gdt],其中第一个Descriptor是一个占位符,科学的说法是数组的哑元(dummy ),用来作

为下面计算Selector的基地址,下面会详细说明。

 

很多个段描述符定义在一起,是否可以不连续呢?
当然可以,比如:

LABEL_GDT:   Descriptor        0,                0, 0       ; 空描述符
LABEL_DESC_CODE32: Descriptor        0, SegCode32Len - 1, DA_C + DA_32 ; 非一致代码段, 32
OTHER_LABEL:  dw       0        ;
LABEL_DESC_VIDEO: Descriptor  0B8000h,           0ffffh, DA_DRW  ; 显存首地址

但是,这样你就要通过一种复杂的(心算?)方式计算出每个描述符在GDT表中的索引,然后很难用
一种自动的方式计算每个选择子中的索引值,你要为个每个选择子中索引位置手工填充它们的值。

选择子到底是什么?这一点作者说的一点也不清楚。

“在保护模式下,地址仍然用SEG:OFFSET这样的形式来表示,但这时的段值是对段描述符的索引”,
那么这个索引如何放到cs和ds中呢?在没有进行页管理之前,段基址的物理地址如何传给ds,cs?
当然我们手工(心算)是可以的,GDT表中0是空着的,第一个描述符LABEL_DESC_CODE32的索引是8
所以初始化cs时可以直接传8作为索引部分(注意属性部分没有谈),第二个描述符是8+2+8,因为中
间我们多了一个OTHER_LABEL.所以我们可以直接把0x12(18)作为索引值传给gs.但这是你在心算,
在复杂情况下,并不是每个人都有这样心算的能力的。

选择子其实就是表示对应的描述符在GDT表中的索引和属性的一个结构,或者说是一个指针,指示它对
对应的描述符在GDT表中的位置。一共有两个字节,16位。

如果我们把描述符连续地排列,第0个描述符是一个空的(浪费了8字节)作为GDT基地址。那么下面的
每个描述符对于GDT基地址来说就是8,16,24,32.......这样的倍数。所以用
LABEL_DESC_CODE32-LABEL_GDT得到的就是一个偏移(作者非说它不是偏移),这个偏移值就是每个
描述符对于GDT基地址(LABEL_GDT)的偏移,也就是选择子中索引的值。

因为连续的描述符对于GDT基地址的偏移至少是8的倍数,所以后面3位肯定是0,即

00000000,00001000 ->8
00000000,00010000 ->16
00000000,00011000 ->24
00000000,00100000 ->32
所以后面三位用不着,我们只需要前面的13位就行了。计算的时候内部左移3就可以把后面的3个0
补上来,这样在存储表示的时候就可以省出三位来,简单说是前13位表示的数字乘8表示描述符相对
GDT基地址的索引(8,16,24,32......),而前13位本身(不乘8)就是描述符在表中第几位(1,2,3,4...),
而节省下来的3位正好用于表示TI和RPL。

 

下面GdtPtr的定义:

GdtPtr        dw    GdtLen - 1    ; GDT界限

dd    0        ; GDT基地址

 

GdtPtr其实是一个伪描述符,它的结构是:

DW16位界限

DD32位基地址

所以下面的dd 0其实是GdtPtr的后四个字节。



开始进入代码:
 mov ax,   cs  
 mov ds,   ax  
 mov es,   ax  
 mov ss,   ax  
 mov sp,   0100h  
因为这时刚开始执行,在实模式下为使各个段共用一个段基址,所以把cs传给了ds,es,ss。
sp的初值赋0x100是256,即栈顶的指针。

 

 

 ; 初始化 32 位代码段描述符
 xor eax, eax
 mov ax, cs
 shl eax, 4
 add eax, LABEL_SEG_CODE32
 mov word [LABEL_DESC_CODE32 + 2], ax
 shr eax, 16
 mov byte [LABEL_DESC_CODE32 + 4], al
 mov byte [LABEL_DESC_CODE32 + 7], ah
 
第2,3,4行是根据实模式下cs的值计算段首址(左移4就是乘16),加上描述符的相对地址初始化成保护模式下的

段基址。然后把这个地址拆开存几个部分存到LABEL_DESC_CODE32描述符的对应位置。

 xor eax, eax
 mov ax, ds
 shl eax, 4
 add eax, LABEL_GDT  ; eax <- gdt 基地址
 mov dword [GdtPtr + 2], eax ; [GdtPtr + 2] <- gdt 基地址

这段作用和上面初始化LABEL_DESC_CODE32一样,先根据实模式下的ds值计算保护模式下对应的数据段基地址。
然后将GDT基地址传给GdtPtr+2.以供下面的lgdt调用,也就是那个无名的dd那个位置。

GdtPtr一共有6个字节,前两个字节存的是GDT界限(len-1)后4个字节就是存的GDT的基址。所以要把这个地址放到

GdtPtr + 2的位置。

 

 

 

pmtest2.asm:

 

[SECTION .s16code]
ALIGN    32
[BITS    16]
LABEL_SEG_CODE16:
    ; 跳回实模式:
    mov    ax, SelectorNormal
    mov    ds, ax
    mov    es, ax
    mov    fs, ax
    mov    gs, ax
    mov    ss, ax

    mov    eax, cr0
    and    al, 11111110b
    mov    cr0, eax

LABEL_GO_BACK_TO_REAL:
    jmp    0:LABEL_REAL_ENTRY    ; 段地址会在程序开始处被设置成正确的值

Code16Len    equ    $ - LABEL_SEG_CODE16

 

这段代码和在实模式下构造保护模式的段一样,要在保护模式下构造实模式的段以便跳回实模式.

在由保护模式向实模式跳转时候,由于每个段寄存器都配有段描述符高速缓冲寄存器,其内容会由保护模式状态下带到实模式下,

但其中内容的"格式(其实就是实模式下的段属性,在实模式下不能手工修改)"是保护模式下的格式,与实模式不匹配,所以要做两件事:

 

1.加载一个16位代码的选择子,即SelectorNormal,并将这个段的这个段描述符向ds,es,fs,gs,ss复制,其实并在用到这些寄存

器的值,只是为了把这些寄存器的高速缓冲寄存器中的内容刷新成16位的实模式下的"格式".

因为CS寄存器不能直接填充,所以只能从保护模式的32位代码跳转到16位代码由CPU自动去刷新CS寄存器的高速缓冲寄存器.

jmp    SelectorCode16:0的作用是jmp [cs:ip],所以SelectorCode16描述符被加载到CS段,完成对高速缓冲寄存器的刷新.

 

2.因为CS段属性已经正确,而开始时实模式下CS段的基址被保存在LABEL_GO_BACK_TO_REAL+3处,

即jmp    0:LABEL_REAL_ENTRY这条指令的段地址分部,所以jmp    0:LABEL_REAL_ENTRY 可以正确地跳到实模式的LABEL_REAL_ENTRY 处,并且将原来保存的实模式下的段地址带到LABEL_REAL_ENTRY 中.

 

 

这样完成了所有的寄存器的高速缓冲寄存器的内容的刷新成实模式的"格式"后,再跳入实模式的代码.所以从保护模式跳回实模式时

一定会借助一个Norma描述符和一个带有实模式的CS段地址的jmp指令.

 

其它的代码很直观,没有什么特别绕人的地方。

 

你可能感兴趣的:(descriptor,数据结构,video,go,存储,工作)