现在的位置: 首页 > 技术文章 > 操作系统 > 正文

《自己动手写操作系统》学习笔记(四)

2014年03月04日 操作系统 ⁄ 共 7081字 ⁄ 字号 《自己动手写操作系统》学习笔记(四)已关闭评论 ⁄ 阅读 1,916 次

《自己动手写操作系统》学习笔记目录(持续更新)  http://www.techbulo.com/832.html

实模式跳转到保护模式

实模式跳转到保护模式

实模式------>保护模式

有了上一节的基础,那我们开始编码,看看如何实现先前描述的内容

首先,既然我们需要一个数组,全局描述符表,那我们就定义一块连续的结构体:

[SECTION .gdt]    ;为了代码可读性,我们将这个数组放到一个节(段)中

;由一块连续的地址组成的,不就是一个数组吗?看下面代码,^_^

                                     段基地址    段界限 段属性

GDT_BEGIN: Descriptor 0,       0,    0  

GDT_CODE32: Descriptor 0,    0,    DA_C

;上面,我定义了二个连续地址的结构体,大家先认为Descriptor就是一个结构体类型,我们会在以后详细讲述

;第一个结构体,全部是0,是为了遵循Interl规范,先记得就OK

;第二个定义了一个代码段,段基地址和段界限我们暂且还不知道,先初始化为0,但是因为是个代码段,代码段具备执行的属性,那么DA_C就代表是一个可执行代码段,DA_C是一个预先定义好的常量,我们会在详细讲解段描述符中讲解。

我们继续来实现,那么下面,我们就需要设计段选择子了,因为上面代码已经包含了段描述符和全局描述符表
还记得选择子是个什么东西吗 ?

段选择子:      也就是数组的索引,但这时候的索引不在是高级语言中数组的下标,而是我们将要找的那个段描述符相对于数组首地址(也就是全局描述表的首地址)偏移位置。

看我代码怎么实现,包含以上代码不再说明:

[SECTION .gdt]

GDT_BEGIN: Descriptor 0, 0, 0

GDT_CODE32: Descriptor 0, 0, DA_C

;下面是定义代码段选择子,它就是相对数组首地址的偏移量

SelectorCode32 equ    GDT_CODE32 - GDT_BEGIN

;因为第一个段描述符,不被使用,所以就不比设置段选择子了。

=================================

偏移地址:

注意一点,我们在程序中使用的都是偏移地址,相对于段的偏移地址,用上面的例子来说,象 GDT_CODE32 GDT_BEGIN 这些结构体的首地址都是相对于数据段的偏移量。什么意思呢 ?

因为我们的程序到底加载到内存的哪个地方是不固定,不知道的,只需使用偏移地址操作就行了,如:
SelectorCode32 ,它本身就是一个偏移地址

但是SelectorCode32    equ GDT_CODE32 - GDT_BEGIN

怎么解释呢 ?

GDT_CODE32是相对于数据段的偏移量,

GDT_BEGIN也是相对于数据段的偏移量,虽然它是数组的首地址,说的罗索一些,GDT_BEGIN是数组的首地址,但是它是相对于数据段的偏移量

那么两个偏移量相减就是GDT_CODE32 相对于GDT_BEGIN的偏移量

所以,我们要时时刻刻记得,在程序中,我们永远使用的是偏移量,因为我们不知道程序将要被加载内存那块地方。

好了,基础也学的差不多了,下面我们要自己动手写一段程序,实现实模式到保护模式之间的跳转

===================================================================

;参考:《自己动手写操作系统》

----------------------------------------------------------------------

%include "pm.inc"
org 0100h
jmp LABEL_BEGIN

[SECTION .gdt]
GDT_BEGIN: Descriptor 0, 0, 0
GDT_CODE32: Descriptor 0, LenOfCode32 - 1, DA_C + DA_32
GDT_VIDEO: Descriptor 0B8000H, 0FFFFH, DA_DRW

GdtLen equ $ - GDT_BEGIN
GdtPtr dw GdtLen - 1
 dd 0

;定义段选择子
SelectorCode32 equ GDT_CODE32 - GDT_BEGIN
SelectorVideo equ GDT_VIDEO - GDT_BEGIN

[SECTION .main]
[BITS 16]
LABEL_BEGIN:
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax

;初始化32位代码段选择子
;我们可以在实模式下通过段寄存器×16 + 偏移两 得到物理地址,
;那么,我们就可以将这个物理地址放到段描述符中,以供保护模式下使用,
;因为保护模式下只能通过段选择子 + 偏移量

xor eax, eax
mov ax, cs
shl eax, 4
add eax, LABEL_CODE32
mov word [GDT_CODE32 + 2],ax
shr eax, 16
mov byte [GDT_CODE32 + 4],al
mov byte [GDT_CODE32 + 7],ah

;得到段描述符表的物理地址,并将其放到GdtPtr中
xor eax, eax
mov ax, ds
shl eax, 4
add eax, GDT_BEGIN
mov dword [GdtPtr + 2],eax

;加载到gdtr,因为现在段描述符表在内存中,我们必须要让CPU知道段描述符 表在哪个位置
;通过使用lgdtr就可以将源加载到gdtr寄存器中
lgdt [GdtPtr]

;关中断
cli

;打开A20线
in al, 92h
or al, 00000010b
out 92h, al

;准备切换到保护模式,设置PE为1
mov eax, cr0
or eax, 1
mov cr0, eax

;现在已经处在保护模式分段机制下,所以寻址必须使用段选择子:偏移量来 寻址

;跳转到32位代码段中
;因为此时偏移量位32位,所以必须dword告诉编译器,不然,编译器将阶段 成16位
jmp dword SelectorCode32:0;跳转到32位代码段第一条指令开始执行
[SECTION .code32]
[BITS 32]
LABEL_CODE32:
mov ax, SelectorVideo
mov es, ax

xor edi, edi
mov edi, (80 * 10 + 10)

mov ah, 0ch
mov al, 'G'

mov [es:edi],ax

jmp $
LenOfCode32 equ $ - LABEL_CODE32

这段代码的大概意思是:

先在16位代码段,实模式下运行,在实模式下,通过段寄存器×16+偏移量得到32位代码的真正物理首地址,并将放入到段描述符表中,以供在保护模式下使用,上面说过了,保护模式下寻址,是通过段选择子,段描述符表,段描述符一起工作寻址的。所以在实模式下所做的工作就是初始化段描述符表里的所有段描述符。

我们来看一下段描述符表,它有3个段:

GDT_BEGIN

GDT_CODE32

GDT_VIDEO

GDT_BEGIN,遵循Intel公司规定,全部置0

GDT_CODE32,32位代码段描述符,供保护模式下使用

GDT_VIDEO,显存段首地址,我们知道,显存首地址是0B8000H.

回想一下,我们在实模式下往显示器上输出文字时,我们设置段寄存器为

0B800h,(注意后面比真正物理地址少一个0)。

而我们现在在保护模式下访问显存,那么0B8000h就可以直接放到段描述符中即可。因为段描述符中存放的是段的真正的物理地址。

下面我们来逐行分析该代码

org    0100h

这句话告诉加载器,将这段程序加载到偏移段首地址0100h处,即:偏移256字节处,为什么要加载到偏移256个字节处呢 ?这是因为,在DOS中,需要留下256个字节和DOS系统进行通信。

jmp    LABEL_BEGIN

执行这句话就跳转到LABEL_BEGIN处开始执行。好,我们看一下LABEL_BEGIN在那块,也就是16位代码段

[SECTION .main]

[BITS 16]

LABEL_BEGIN: 

这样程序就从.main节的第一段代码开始执行。我们看一下上面的代码,[BITS 16]告诉编译器,这是一个16位代码段,所使用的寄存器都是16位寄存器。该代码段初始化所有段描述符表中的段物理首地址

首先在实模式下计算出32位代码段的物理首地址

对照    段值 × 16 + 偏移量    = 物理地址

1    mov ax,    cs

2    shl eax, 4    ;向左移动4位,不就是×16吗?呵呵

;到现在为止,eax就是代码段的物理首地址了,那么。。。看

3    add eax, LABEL_CODE32

;为eax (代码段首地址)加上 LABEL_CODE32偏移量,得到的不就是LABEL_CODE32的真正物理地址了吗 ?

LABEL_CODE32在程序中,不就是32位代码段的首地址吗 ?

上面说过,代码中,使用的变量,或者标签 都是相对程序物理首地址的偏移量

OK,现在我们已经知道了32位代码段的物理首地址,那么将eax放入到段描述符中就行了

我们先假设Descriptor就是一个结构体类型,(实际它是一个宏定义的数据结构,为了不影响整体思路,我们放到以后讲)

看一下这个Descriptor段描述符的内存模型:

; 高地址………………………………………………………………………低地址

; |     7     |     6     |     5     |     4     |     3     |     2     |     1     |     0      |

共 8 字节

; |--------========--------========--------========--------========|

; ┏━━━┳━━━━━━━┳━━━━━━━━━━━┳━━━━━━━┓

; ┃31..24  ┃     段属性            ┃       段基址(23..0)                ┃ 段界限(15..0)     ┃

; ┃             ┃                              ┃                                                ┃                               ┃

; ┃ 基址2 ┃                              ┃基址1b│     基址1a              ┃       段界限1        ┃

; ┣━━━╋━━━┳━━━╋━━━━━━━━━━━╋━━━━━━━┫

; ┃      %6 ┃    %5    ┃    %4    ┃    %3    ┃       %2                  ┃         %1                ┃

; ┗━━━┻━━━┻━━━┻━━━┻━━━━━━━┻━━━━━━━┛

(这幅图调整了很多次还是乱的,麻烦各位看官直接参照《自己动手写操作系统》P43 图3-2 )

由于历史原因,段描述符的内存排列不是按照 段基地址 段界限 段属性 这样的来排列的,所以我们现在要想一种办法,把eax里所存放的物理首地址拆开,分别放到2,3,4,7字节处

那么很显然,我们可以将eax寄存器中的ax先放到2,3字节处

mov    word [GDT_CODE32 + 2],ax

因为在偏移2个字节处,所以,首地址 + 2,才能定位到下标为2的字节开头处

而,word 告诉编译器,我要一次访问2个字节的内存

好,简单的搞定了,那么再看,我们现在要将eax高16字节分别放到下标为4,7字节处。

虽然eax的ax代表低16位,但是Intel并没有给高位一个名字定义,(不会是high ax,呵呵),所以,我们没有办法去访问高位。但是我们可以将高16位放到低16位中,因为这时,低16位我们已经不关心它的值了。

好,看代码

shr    eax,    16

这句代码就将eax向右移动16位,低位被抛弃,高位变成了低位。呵呵。。。

现在好办了,低16位又可以分为al,和 ah,那么现在我们就将al放到4位置,ah放到7位置吧

mov    byte [GDT_CODE32 + 4], AL

mov    byte [GDT_CODE32 + 7], AH

不用我再解释这段代码了,自己去分析为什么吧。。。。

好了,32位代码段描述符设置好了,其界限设置看代码吧,为什么要那样设置,很简单的,界限 = 长度 - 1,段属性:

DA_C: 98h      可执行

DA_32: 4000h 32位代码段

是个常量,换算成二进制位,对照段描述符属性位置去看吧,参考任意一本保护模式书。

段描述符设置好了,但是,先段描述符表,还在内存中,我们必须想办法放到寄存器中,这时,就用到了

gdtr(Golbal Descriptor Table Register),使用一条指令

lgdtr [GdtPtr]

就可以将GdtPtr加载到gdtr中

而gdtr的内存模型是:

-------------------------------------------------------

高字节                                   低字节

-------------------------------------------------------

但GdtPtr是什么呢 ?

就是我们定义的和这个寄存器内存模型一摸一样的结构体:

GdtLen    equ    $ - LABEL_BEGIN

GdtPtr    dw    GdtLen - 1     ;界限

dd    0       ;真正物理地址

那现在我们就要计算GdtPtr第二个字节 也就是真正物理地址了

xor eax, eax

mov ax,    ds

shl eax, 4

add eax, GDT_BEGIN

mov dword [GdtPtr + 2],eax

自己分析吧,和计算32位段首地址基本一样的,

搞定后,使用lgdt [GdtPtr]就将此加载到寄存器GDTR中了

然后关中断

cli    实模式下的中断和保护模式下的中断处理不一样,那就关吧,规矩

开启A20线

in al, 92h

or al, 00000010b

out 92h, al

如果不开启A20线,就无办法访问1M之上的内存,没办法,开启吧,规矩,想知道历史了,去查吧

然后设置CR0的PE位

mov eax, cr0

or eax, 1

mov cr0, eax

这个简单说一下,以后再详细

CR0也是一个寄存器,其中有个PE位,如果为0,就说明为实模式,

如果置1,说明为保护模式。现在我们要进入保护模式下工作,那么就要设置PE为1。

好了,看一下这个main节中的最后一个代码

jmp    dword SelectorCode32 : 0

哈哈,现在已经再保护模式下了,当然要使用段选择子 + 偏移量来寻址啊,这样不就是寻址到了32位代码段中去了吗,偏移量为0不就说明从第一个代码开始执行。

不是吗 ?呵呵,那dword了?

因为现在的代码段是16位,编译器只能将它编译位16位,但处于保护模式下,它的偏移量应该是32位,所以,要显示告诉编译器,我这里使用的是32位,把我这块给编译成32位的!!!

如果不加dword,

jmp    SelectorCode32:0

这句话不会出什么问题,16位的0是0,32位的0还是0,但如果这样呢?:

jmp    SelectorCode32:0x12345678

跳转到偏移0x12345678中,这时就错了

如果不将dword,编译器就将该地址截断成16位,取低位,变成了0x5678

所以我们必须这样做:

jmp    dword SelectorCodde32:0x12345678

OKEY,我们继续追击,执行完上面那个跳转后,

代码就跳到了32位代码段的中,开始执行第一条指令

mov ax, SelectorVideo

再看

mov es,ax

呵呵,实模式下,放的是16位的段值,而现在呢,不就是要将段选择子放到段寄存器里吗 ?然后通过段选择子(偏移量)找到描述符表中对应的段描述符的吗 !!!!

继续看下面代码

xor edi, edi

mov edi, (80 * 10 + 10)

mov ah, 0ch

mov al, 'G'

跟实模式下差不多,设置目标10行10列

设置现实字符:G

mov [es:edi],ax

也和实模式下一样,

只不过实模式是这样来寻址 :

es×16 + edi

而保护模式下呢

es是一个偏移,根据这个偏移找到段描述符表中的对应显存段,然后这个显存段里存放的就是0B8000h,然后在加上偏移 不就的了吗!!

注意:

1. 注意程序中使用的全部是偏移地址。注意两种偏移地址

A 对于程序的起始地址来说,所有变量和标签都是相对于整个程序的偏移量

B 对于段中定义的代码,有两种偏移:

相对于程序起始地址的偏移

相对于段标签的偏移。

2.不管是实模式下的物理地址,还是保护模式下的物理地址,反正他们都是物理地址,呵呵,实模式下求的物理地址,也能在保护模式下使用,只是他们不同的是,如何寻址的方式不一样。

3.一个程序中可以包含多个不同位的段,32位或者16位,他们之间也可以互相跳转,只是32位段用的是32位寄存器,16位代码段用的是16位寄存器,如果要在16位段下使用32位寄存器,必须象高级语言中强制类型转换一样,显示的定义 dword

下面总结一下进入保护模式的主要步骤:

(1)准备GDT

(2)用lgdt加载gdtr

(3)打开A20

(4)置cr0的PE位

(5)跳转,进入保护模式

×