实模式、保护模式、长模式

来源:互联网 发布:淘宝店铺找不到了 编辑:程序博客网 时间:2024/06/02 12:48

转自:http://blog.csdn.net/hitop0609/article/details/4527246

内存地址 实模式

2的10次方是1K,20次方就是1M。如果一个变量只有16 bit,(即只有16个表示二进制的位),那么只能表示 2的16次方种情况。如果一个变量只有1 bit,那么只能表示两种情况,即0和1。
        要表示地址(十进制)1234,(对应的十六进制为:4D2)只要12 bit就可以了。要表示12345678 这个地址,(BC614E)需要 24 bit。

在实模式下,只有16位(bit,下同)的寄存器,也就是只能表示的最大数是FFFFH(H表示十六进制,下同),所以表示不了BC614EH。

只有16位的寄存器,如果要表示 10001H,怎么办呢?因为一个寄存器能表示的最大数是FFFFH,所以用两个寄存器可以表示10001H,比如用DS和BX来表示吧,令DS=FFFFH,BX=2即可,这样DS+AX=10001H。

两个16位的寄存器相加能表示的最大数为10001H,如果要表示10002H,就得用3个寄存器相加了。即DS=FFFFH,BX=FFFFH,CX=1H。这是一种表示方法。

还有N种表示方法。在实模式下,要表示10002H,计算机设计者采用的方法是:DS*10H+BX 就可以了,即令DS=1000H,BX=2H。上面用相加的方法是小学生想得出来的。

好了,在这种用乘法表示的方法下,要表示12345H,应该要这样设置DS和BX,先把12345H/10得到的商赋给DS,然后把5赋给BX就可以了。简单的说,要表示地址Addres,就是解方程 DS*10H+BX=Addres,这是一个二元一次方程。应该会有很多个解。这种方式起一个名字可以叫段氏方式。能表示的最大数是:十六进制
FFFF*10H+FFFF=10FFEF,能表示到一些六位数了。
而2的20次方表示的最大数是FFFFF。

1G的内存,其最大地址为 3FFFFFFF,显然用上面的 DS*10H+BX 也表示不了啦。

在保护模式下可以用32 bit的寄存器。那么,如果用32bit 的寄存器来表示呢?32 bit就是8位的十六进制,可以表示的最大数是:FFFF FFFF,显然要表示 3FFF FFFF 完全可以,还有剩余呢。

但(他妈的)计算机设计者不是这样表示内存地址,还非得用另外一种段氏表示法。

保护模式

本来使用EBX这样的32bit 寄存器就可以直接访问4G以内的内存了,但CPU设计者不采用这样天然的方法,而非得弄得很复杂。简单的说,要访问一个内存单元,你需要给两个参数,一个是段选择子、一个是偏移,然后,剩下的事情你不用管了,CPU通过一个函数的东西计算其实际的物理地址。这样看来,似乎速度要慢好多了。即 
物理地址=f(段选择子,偏移)            addres=f(sel,offset);

如果这个函数是透明的,那还好些,但是,这个函数不是透明的,这就意味着,我们还得去研究这个函数是如何工作的。看起来是一件很麻烦的事情。

保护模式也像实模式那样,把内存分成很多部分,即所谓的一段一段的。那么每个段自然也就有个基址和长度等属性了。Base Address 和 Limit 和段属性 Attributes,长度的单词本来是 Length,但他们叫做“界限”,所以用Limit ,实际上就是长度的意思了,搞得莫明其妙。即:
SEG{BaseAddres,Limit,Attributes}为了一眼看得明白,不如写成:

SEG 被表现为:{Base,Length,Attr}。

因为这些 SEG 很多,可能有上千上万个,为了管理方便,把它们记录在一个表里面,就像数据库那样了。嗯,这个表就是传说中的“描述符表”。要记录 Base 需要32bit,就是4个字节,而 Length 是20 bit,2.5个字节,总之最后这三项由8个字节搞定。每一个 {Base,Length,Attr} 就是表中的一个记录,他们叫做传说中的“描述符”。具体怎么记录这些东西,比如哪几位表示 base,哪几位表示 Length ,还是比较复杂的。看看,他们又不按自然的方式来,而非得把 base 拆成几个部分……具体情况就不描述了。

好了,现在要访问内存中某个地方 Addres,输入两个参数,SEG和offset,那么经过 f(seg,offset)的计算就可以得到地址了。这个过程怎么做呢?

1,sel (段选择子)里有表示描述符表中的第几个记录的数据,比如第一个记录,或第十个记录;所谓的“记录”就是上面说的“描述符”了。
2,好,继续计算。把这个描述符拿出来看看,里面有一个段基址,假设是 Base;
3,那么,就用加法搞定这个 Addres=Base+offset

和实模式下有什么不同?嗯,这个不是一般的 DS*10H+BX了,不能用一个公式来表达了。得用一个函数才能完成上面的三步,所以,速度应该是变得好慢了。现在用拟人化的方法描述一下:现在你要访问内存中的某个地方,但你不知道确切的地址,你手里就有它的两个参数,一个是 sel ,人们把这个参数叫做 段选择子,另外一个参数是段内偏移 offset 。你得先去问“描述符表”先生,“先生,请帮忙查找一下 sel 里面所说的段在哪?”“描述符表”先生接过参数 sel ,对照了一下,给你一个记录(描述符),里面有关于段的各个数据,好,你再从中找到段的基址,然后跑到那儿,再继续加上 offset,就可以找到确切的地址了。

又以一个看书的例子来说明:比如你看了一本书,已经看到第10章,而且这部分内容你已经看了前面的20页,现在你想接着看下去,但显然你平时没注意记下这个页码,所以,你现在有两个参数:一个是第“10”章,另外一个是看了20页,要接着往下看的第“21”页。好,现在你要找出这一页,你就得去查目录,查到第10章从第287页开始,然后再加上21页,得到要接着看的 287+21=308页。

为了简单直白,就把描述符表简称为表,把描述符简称为记录。在实模式中,段寄存器中的值乘以10H就知道段的基址了,而在保护模式下,段寄存器只告诉你该段的资料被放在表中的第几个记录,至于段的基址,你得去查表搞到记录的内容才知道。

 

 

从实模式到保护模式

如果你能正确的理解了保护模式下段寄存器中的值表示什么意思,那么就可以考虑从实模式跳到保护模式的问题了。不能直接跳到长模式,所以从实模式跳到保护模式是必经之路。

NASM中,跳转指令可以强制规定相关的操作数尺寸,比如:jmp 342H: dword 542397AEH    表示段值是342H,偏移是32位的542397AEH,这样就可以正常的从16位代码跳到32位代码了。如果使用其他汇编语言,可能需要手工写机器码。

在实模式中,要为保护模式准备一些数据,这些数据就是一些表。如果不准备这些,也是可以切换到保护模式的,只是接着就可能死机而已。

1,声明全局表中的记录——有关的描述符

;存储段描述符结构类型定义
;----------------------------------------------------------------------------
struc DES
LimitL:          resb      2 ;段界限(BIT0-15)
BaseL:           resb      2;段基地址(BIT0-15)
BaseM:           resb      1;段基地址(BIT16-23)
Attributes:      resb      1;段属性
LimitH:          resb      1;段界限(BIT16-19)(含段属性的高4位)
BaseH:           resb      1;段基地址(BIT24-31)
endstruc
;----------------------------------------------------------------------------

;存储段描述符类型值说明
;----------------------------------------------------------------------------
ATDR            EQU     90h ;存在的只读数据段类型值
ATDW            EQU     92h ;存在的可读写数据段属性值
ATDWA           EQU     93h ;存在的已访问可读写数据段类型值
ATCE            EQU     98h ;存在的只执行代码段属性值
ATCER           EQU     9ah ;存在的可执行可读代码段属性值
ATCCO           EQU     9ch ;存在的只执行一致代码段属性值
ATCCOR          EQU     9eh ;存在的可执行可读一致代码段属性值
;----------------------------------------------------------------------------

上面只是结构的定义(本文无特别说明,均为NASM汇编语言语法)。至少声明一个空描述符、一个代码段描述符、一个数据段描述符实例。一般在DATA段中定义。NASM的语法写起来有点繁琐

GDT:     ;这个标签表示全局表的偏移
NullRecord: istruc DES ;这个是空描述符
at LimitL, dw 0 
at BaseL, dw 0
at BaseM, db 0
at Attributes, db 0
at LimitH, db 0 ;
at BaseH, db 0 ;
iend

CodeSeg: istruc DES ;这个是代码段描述符
at LimitL, dw 0ffffh 
at BaseL, dw 0
at BaseM, db 0
at Attributes, db ATCE;存在、只执行
at LimitH, db 0 ;
at BaseH, db 0 ;
iend

DataSeg: istruc DES ;这个是数据段描述符,试验
at LimitL, dw 4096 
at BaseL, dw 0
at BaseM, db 0;
at Attributes, db ATDW;存在的可读写数据段属性值
at LimitH, db 0 ;
at BaseH, db 0 ;
iend

resb 8*100

Data2:;试验用的数据段
MSG db 'Now,it is Protected.',13,10,0;
resb 4096-($-Data2);

 

2,全局表、描述符初始化 GDT LGDT

在数据段中声明了描述符的实例之后,再在代码中进一步初始化——把它们的段基址计算出来填上。

GDT是全局表,LDT是局部表。这些表也是放在某个段中的,所以,如何知道这个段的地址?有专用的寄存器,就是GDTR和LDTR,不过对它们的操作也是用专门的指令如LGDT、LLDT。那么LGDT需要什么样子的参数呢?16位的界限+32位的基址。所谓界限就是以字节为单位的全局表所在的段的长度,等于全局表的长度。
假设代码如:

GDT:     ;这个标签表示全局表的偏移
空记录
第1个记录
第2个记录
……
第N个记录
……保留用于将来可能增加的记录项,
GDTLenth    EQU $-GDT;       总长度(单位为字节)是一个20 bit的数。最大值:FFFFF
VGDTR: ;                              这个就用来存参数
dw 0;                                  16位的长度
dd 0;                                   32位的段基址

到此,两个参数搞定了一半。下一步要从偏移加上段基址得到全局表所在的确切位置。段基址一般是DS*10H 即数据段的基址。
假设数据段偏移为 DATA

mov ax,DATA
mov bx,10H
mul bx;              bx是16位源操作数,与AX相乘,结果放在DX:AX中。这必然是2字节的数。高2位在DX
add ax,GDT;        注意这里是NASM的写法,标签就是偏移值。这样一加,有可能存在进位了。
adc dx,0;         ;偏移的高2位为0。高二位相加的结果放在DX中。     
mov word [VGDTR],GDTLenth;          全局表的长度。也可以设置为 FFFFF以下的数。因为以后可能增加记录项
mov word [VGDTR+2],ax                     ;低16位,全局表的基址
mov word[VGDTR+4],dx                     ;高16位   全局表的基址

全局表可以存放的内容包括其他很多个局部表的记录、某些全局段的记录。例如,可以安排第0项为空记录,第一项为一个代码段记录,第二项为一个数据段记录。

下面计算代码段和数据段的基址

;分别计算代码段和数据段的基址
mov     ax,cs
mul     bx
mov     word [CodeSeg+BaseL],ax ;代码段开始偏移为0
mov     byte [CodeSeg+BaseM],dl ;代码段界限已在定义时设置好
mov     byte [CodeSeg+BaseH],dh
;设置数据段描述符
mov     ax,ds
mul     bx                     ;计算并设置目标数据段基址
add     ax,Data2
adc     dx,0
mov     word [DataSeg+BaseL],ax
mov     BYTE [DataSeg+BaseM],dl
mov     BYTE [DataSeg+BaseH],dh

3,跳到保护模式

         lgdt [VGDTR]
         cli                            ;关中断
         call EnableA20                      ;打开地址线A20
       test eax,0
       jne Fail;                                         打开A20地址线失败
        ;切换到保护方式
                mov     eax,cr0
                or      eax,1
                mov     cr0,eax
;当执行到下面的 jmp 指令时,已经预取了再下一条指令.即EIP已经是 ProtectedMode.
;这是一个强制的远跳转,偏移值强制为一个32位数,
;段前辍是 Code_Sel,而不是实模式下意义的值,因为上面的MOV指令已经打开了保护模式.
   jmp Code_Sel:dword ProtectedMode
ProtectedMode:;下面是保护模式的代码
         mov     ax,Data_Sel
        mov     ds,ax                  ;加载数据段描述符

 

Fail:打开A20地址线失败

如果没有那个 jmp 会怎么样?执行 mov cr0,eax 的时候,会预先读取下一条指令: mov ax,Data_Sel,由于是在实模式方式下读取的,所以CS:IP 是正确的。执行 mov ax,Data_Sel 的时候,会去读取下一条指令,问题是,这时已经用保护模式的方式来寻址了,所以假设 CS=1015H之类的值,并不会做 CS*10H 之类的处理,而是去查全局表找到描述符……显然没有1015H 这个描述符,那么,将取不到正确的指令,如此就极有可能死机了。

非得用jmp吗?也许不一定吧。比如,CS的值正好是代码段选择子的值,这样就不用JMP了。

 

 

从实模式跳转到保护模式要注意的事情

汇编编程是一件极为麻烦的事情。只要有一个地方出错,那么全盘皆败——程序将不能达到预想的效果。本人总结一下要注意的事情。

1,描述符在数据结构上可以说是糟糕透顶的设计。竟然用到了“半字节”的属性字段和地址字段。这种数据结构极不符合自然思维,所以很容易错。我们知道描述的内容包括:段基址、段界限(就是长度)、段属性。你得注意它们的身体是被割裂开来放置在8个字节之中的。所以当你从别人的程序抄过来一个描述符时,一定要注意是否和你的描述符的定义相应。

如果要用到的代码段描述符的内容是错误的,那么,一旦 mov cr0,eax 就会导致系统崩溃,剩下的事情就是傻傻乎的看着自动重启,一脸茫然的样子。

所以,如果一转换到保护模式就系统崩溃重启,请先考虑描述符的内容是否正确。无论如何,在保护模式中,只要段寄存器被载入描述符,而描述符的内容又是荒唐的,那么系统就会无情的重启。所以要保证那些代码段、数据段的描述符一定要正确。

2,不要在保护模式中调用以前在实模式下用的BIOS中断和DOS中断,或者可能引用它们的任何函数。一旦这样做了,就会崩溃重启。保护模式下,你得自己设计中断,(汗一个)。比如,有的人习惯用显示类的中断或含有调用中断的显示函数来 debug,或磁盘读写一类的中断来读写磁盘,这些都会导致崩溃重启。除非你自己设计了这些中断。如果要想显示一些内容,请直接操作显存,或考虑自己写一些驱动程序吧。

3,TD或者其他一些调试器是不能适应从一个模式转换到另外一个模式的调试的。一到 CR0设置的地方大多会崩溃。可以考虑用 bochs 来调试。

4,不要直接从32位代码的保护模式跳回实模式。从16位代码的保护模式可以跳回实模式。跳转指令在保护模式下预取,用实模式的方式执行。无论如何,这些模式间跳转的关键意义是重新装载了段寄存器的值。所以选择能重新装载段寄存器的跳转指令——需要指定段值的跳转指令。比如从16位保护模式跳到实模式可以用 jmp word 段值:偏移值

5,避免被假错误困扰。程序一口气写完了,运行却不是预想的结果,然后尝试去找出错误修改,可能不止一处要修改,修改对了一个地方并不表示会得到正确的结果,却又以为这样修改还是不对的,实际上可能是对的……于是绕圈子,被假的错误困住了。

6,现在已经进入64位CPU的时代,可考虑64位的OS。由于64位长模式的工作机制和32位的保护模式并不怎么相同,所以,如果要考虑64位OS,反倒是不要太深入32位的保护模式。之所以学习保护模式,是因为要经过保护模式才能进入长模式,但也不需要学完全部的保护模式知识。

没有参考资料?资料资料是有的,不仅有,而且还太多了,你可以去看最权威的《AMD64架构程序员手册 卷二:系统程序》。你没有?这个不是问题,你可以去AMD的网站下载,或者在互联网上随便搜搜就可以了。

就当做同时学习英语吧。

描述符的结构

来看看一个多么垃圾的数据结构——描述符。按编程习惯,我们都是从上往下写,那么描述这个垃圾的结构也从上往下写。下面的每个 □表示 1 bit。

□□□□□□□□ □□□□□□□□ 20位段界限的前16位,LimitL dw 0,被分尸。

接着是段基址的开始三个字节,段基址被分尸了,最后面还有一个字节。

□□□□□□□□ □□□□□□□□ 因为NASM没有办法一次性表达三个字节的数据,所以要分成两个字段。
□□□□□□□□

接着是极为混乱的属性+4bit段界限+属性
□□□□ TYPE 
          DT
□□       DPL
          P

□□□□    4bit段界限,把这个接上前面的16bit才构成完整的Limit
          AVL
          0,保持是0
          D
         G

□□□□□□□□ 段基址的最后一个字节。

表示 16位代码段的描述符:(默认十六进制)
FF FF                              段的长度前16位,或者写其他数字也可以,不要小于实际代码段就可以了。

00 00                              在数据段中我们还不知道段基址,先赋0吧,后面再用代码去计算。
00

□□□□ TYPE        对于代码段,有代号8到F,8表示只执行。
          DT             DT=1表示存储段,DT=0表示系统段和门。这里用DT=1
□□       DPL          表示描述符特权级,这里用0表示内层
          P               P=1表示存在在内存中,这里就用这个。

□□□□    4bit段界限,这里根据实际代码段的长度来确定,
          AVL=0,不知道用做什么。        
          0,保持是0
          D       D=0 16位代码段。默认情况下,使用16位地址及16位或8位操作数。可以使用地址大小前缀和操作数大小前缀分别改变默认的地址或操作数的大小。
         G=0上面段界限的单位是字节,=1单位是4K,这里可以用0

□□□□□□□□ 段基址的最后一个字节。在16位代码中当然是0,不可能有这么长的段。


实模式->保护模式->实模式 的切换步骤

1、设置必要的实模式环境,如实模式下的堆栈等。 
2、初始化全局描述符表(GDT)、局布描述符表(LDT)及中断描述符表(IDT)等。 
3、保存实模式下的堆栈地址到某内存处,以便切换回实模式后恢复,如有必要也可保存DS、ES、FS、GS等数据段寄存器的值。 
4、加载全局描述符表至全局描述符表寄存器(GDTR),如果未定义中断描述符表,则关中断,然后打开地址线A20。 
5、修改cr0的PE位为1,切换到保护模式。 
6、使用段间跳转指令转到保护模式下的段,如果有局部描述符表,则应首先加载局部描述表段至局部描述符表寄存器(LDTR)。 
7、设置保护模式下的堆栈段SS及堆栈指令SP(ESP)。 
8、设置DS、ES、FS、GS指令某个数据库,防止无意中使用到未设置的数据段。 
9、准备切换回实模式,用于切换回实模式的段必须是16位段且其段描述符必须定义在GDT中,其段限制必须是0FFFFH。 
10、修改cr0的PE位为0切换回实模式。 
11、恢复堆栈段至切换到保护模式之前的状态,如有必要也可恢复DS、ES等数据段。 
12、关闭地址线A20。如中断为关闭状态,则打开中断。 
本文介绍两个实现实模式与保护模式切换的实例,通过他们说明如何实现实模式与保护模式的切换,也说明保护模式下的80386及其编程
<一>演示实模式和保护模式切换的实例(实例一)
     实例一的逻辑功能是,以十六进制数的形式显示从内存地址110000H开始的256个字节的值。本实例指定该内存区域的目的仅仅是想说明切换到保护模式的必要性,因为在实模式下不能访问该指定内存区域,只有在保护模式下才能访问到该指定区域。
     本实例的具体实现步骤是:(1)作切换到保护方式的准备;(2)切换到保护方式;(3)把指定内存区域的内容传送到位于常规内存的缓冲区中;(4)切换回实模式;(5)显示缓冲区内容。
1.包含文件
     386保护模式汇编语言程序用到的包含文件如下所示,该包含文件在后面的程序中还要用到。 
;名称:386SCD.INC
;功能:符号常量等的定义
;----------------------------------------------------------------------------
;IFNDEF __386SCD_INC
;__386SCD_INC EQU 1
;----------------------------------------------------------------------------
.386P
;----------------------------------------------------------------------------
;打开A20地址线
;----------------------------------------------------------------------------
EnableA20 MACRO
push ax
in al,92h
or al,00000010b
out 92h,al
pop ax
ENDM
;----------------------------------------------------------------------------
;关闭A20地址线
;----------------------------------------------------------------------------
DisableA20 MACRO
push ax
in al,92h
and al,11111101b
out 92h,al
pop ax
ENDM
;----------------------------------------------------------------------------
;16位偏移的段间直接转移指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
JUMP16 MACRO Selector,Offset
DB 0eah ;操作码
DW Offset ;16位偏移量
DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;32位偏移的段间直接转移指令的宏定义(在32位代码段中使用)
;----------------------------------------------------------------------------
COMMENT <JUMP32>
JUMP32 MACRO Selector,Offset
DB 0eah ;操作码
DD OFFSET
DW Selector ;段值或段选择子
ENDM
<JUMP32>
;-------------------------------------------------
JUMP32 MACRO Selector,Offset
DB 0eah ;操作码
DW OFFSET
DW 0
DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;16位偏移的段间调用指令的宏定义(在16位代码段中使用)
;----------------------------------------------------------------------------
CALL16 MACRO Selector,Offset
DB 9ah ;操作码
DW Offset ;16位偏移量
DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;32位偏移的段间调用指令的宏定义(在32位代码段中使用)
;----------------------------------------------------------------------------
COMMENT <CALL32>
CALL32 MACRO Selector,Offset
DB 9ah ;操作码
DD Offset
DW Selector ;段值或段选择子
ENDM
<CALL32>
;-------------------------------------------------
CALL32 MACRO Selector,Offset
DB 9ah ;操作码
DW Offset
DW 0
DW Selector ;段值或段选择子
ENDM
;----------------------------------------------------------------------------
;存储段描述符结构类型定义
;----------------------------------------------------------------------------
Desc STRUC
LimitL DW 0 ;段界限(BIT0-15)
BaseL DW 0 ;段基地址(BIT0-15)
BaseM DB 0 ;段基地址(BIT16-23)
Attributes DB 0 ;段属性
LimitH DB 0 ;段界限(BIT16-19)(含段属性的高4位)
BaseH DB 0 ;段基地址(BIT24-31)
Desc ENDS
;----------------------------------------------------------------------------
;门描述符结构类型定义
;----------------------------------------------------------------------------
Gate STRUC
OffsetL DW 0 ;32位偏移的低16位
Selector DW 0 ;选择子
DCount DB 0 ;双字计数
GType DB 0 ;类型
OffsetH DW 0 ;32位偏移的高16位
Gate ENDS
;----------------------------------------------------------------------------
;伪描述符结构类型定义(用于装入全局或中断描述符表寄存器)
;----------------------------------------------------------------------------
PDesc STRUC
Limit DW 0 ;16位界限
Base DD 0 ;32位基地址
PDesc ENDS
;----------------------------------------------------------------------------
;任务状态段结构类型定义
;----------------------------------------------------------------------------
TSS STRUC
TRLink DW 0 ;链接字段
DW 0 ;不使用,置为0
TRESP0 DD 0 ;0级堆栈指针
TRSS0 DW 0 ;0级堆栈段寄存器
DW 0 ;不使用,置为0
TRESP1 DD 0 ;1级堆栈指针
TRSS1 DW 0 ;1级堆栈段寄存器
DW 0 ;不使用,置为0
TRESP2 DD 0 ;2级堆栈指针
TRSS2 DW 0 ;2级堆栈段寄存器
DW 0 ;不使用,置为0
TRCR3 DD 0 ;CR3
TREIP DD 0 ;EIP
TREFlag DD 0 ;EFLAGS
TREAX DD 0 ;EAX
TRECX DD 0 ;ECX
TREDX DD 0 ;EDX
TREBX DD 0 ;EBX
TRESP DD 0 ;ESP
TREBP DD 0 ;EBP
TRESI DD 0 ;ESI
TREDI DD 0 ;EDI
TRES DW 0 ;ES
DW 0 ;不使用,置为0
TRCS DW 0 ;CS
DW 0 ;不使用,置为0
TRSS DW 0 ;SS
DW 0 ;不使用,置为0
TRDS DW 0 ;DS
DW 0 ;不使用,置为0
TRFS DW 0 ;FS
DW 0 ;不使用,置为0
TRGS DW 0 ;GS
DW 0 ;不使用,置为0
TRLDTR DW 0 ;LDTR
DW 0 ;不使用,置为0
TRTrip DW 0 ;调试陷阱标志(只用位0)
TRIOMap DW $+2 ;指向I/O许可位图区的段内偏移
TSS ENDS
;----------------------------------------------------------------------------
;存储段描述符类型值说明
;----------------------------------------------------------------------------
ATDR EQU 90h ;存在的只读数据段类型值
ATDW EQU 92h ;存在的可读写数据段属性值
ATDWA EQU 93h ;存在的已访问可读写数据段类型值
ATCE EQU 98h ;存在的只执行代码段属性值
ATCER EQU 9ah ;存在的可执行可读代码段属性值
ATCCO EQU 9ch ;存在的只执行一致代码段属性值
ATCCOR EQU 9eh ;存在的可执行可读一致代码段属性值
;----------------------------------------------------------------------------
;系统段描述符类型值说明
;----------------------------------------------------------------------------
ATLDT EQU 82h ;局部描述符表段类型值
ATTaskGate EQU 85h ;任务门类型值
AT386TSS EQU 89h ;可用386任务状态段类型值
AT386CGate EQU 8ch ;386调用门类型值
AT386IGate EQU 8eh ;386中断门类型值
AT386TGate EQU 8fh ;386陷阱门类型值
;----------------------------------------------------------------------------
;DPL值说明
;----------------------------------------------------------------------------
DPL0 EQU 00h ;DPL=0
DPL1 EQU 20h ;DPL=1
DPL2 EQU 40h ;DPL=2
DPL3 EQU 60h ;DPL=3
;----------------------------------------------------------------------------
;RPL值说明
;----------------------------------------------------------------------------
RPL0 EQU 00h ;RPL=0
RPL1 EQU 01h ;RPL=1
RPL2 EQU 02h ;RPL=2
RPL3 EQU 03h ;RPL=3
;----------------------------------------------------------------------------
;IOPL值说明
;----------------------------------------------------------------------------
IOPL0 EQU 0000h ;IOPL=0
IOPL1 EQU 1000h ;IOPL=1
IOPL2 EQU 2000h ;IOPL=2
IOPL3 EQU 3000h ;IOPL=3
;----------------------------------------------------------------------------
;其它常量值说明
;----------------------------------------------------------------------------
D32 EQU 40h ;32位代码段标志
GL EQU 80h ;段界限以4K为单位标志
TIL EQU 04h ;TI=1(局部描述符表标志)
VMFL EQU 00020000h ;VMF=1
VMFLW EQU 0002h
IFL EQU 00000200h ;IF=1
RFL EQU 00010000h ;RF=1(重启动标志,为1表示忽略调试故障)
RFLW EQU 0001h
NTL EQU 00004000h ;NT=1
;----------------------------------------------------------------------------
;分页机制使用的常量说明
;----------------------------------------------------------------------------
PL EQU 1 ;页存在属性位
RWR EQU 0 ;R/W属性位值,读/执行
RWW EQU 2 ;R/W属性位值,读/写/执行
USS EQU 0 ;U/S属性位值,系统级
USU EQU 4 ;U/S属性位值,用户级
;----------------------------------------------------------------------------
;ENDIF

2.实例源程序
     实例一的源程序如下所示:
;名称:ASM1.ASM
;功能:演示实方式和保护方式切换(切换到16位代码段)
;----------------------------------------------------------------------------
INCLUDE 386SCD.INC
;----------------------------------------------------------------------------
;字符显示宏指令的定义
;----------------------------------------------------------------------------
EchoCh MACRO ascii
mov ah,2
mov dl,ascii
int 21h
ENDM
;----------------------------------------------------------------------------
DSEG SEGMENT USE16 ;16位数据
;----------------------------------------------------------------------------
GDT LABEL BYTE ;全局描述符表
DUMMY Desc <> ;空描述符
Code Desc <0ffffh,,,ATCE,,> ;代码段描述符
DataS Desc <0ffffh,0,11h,ATDW,,> ;源数据段描述符
DataD Desc <0ffffh,,,ATDW,,> ;目标数据段描述符
;----------------------------------------------------------------------------
GDTLen = $-GDT ;全局描述符表长度
VGDTR PDesc <GDTLen-1,> ;伪描述符
;----------------------------------------------------------------------------
Code_Sel = Code-GDT ;代码段选择子
DataS_Sel = Datas-GDT ;源数据段选择子
DataD_Sel = DataD-GDT ;目标数据段选择子
;----------------------------------------------------------------------------
BufLen = 256 ;缓冲区字节长度
Buffer DB BufLen DUP(0) ;缓冲区
;----------------------------------------------------------------------------
DSEG ENDS ;数据段定义结束
;----------------------------------------------------------------------------
CSEG SEGMENT USE16 ;16位代码段
ASSUME CS:CSEG,DS:DSEG
;----------------------------------------------------------------------------
Start PROC
mov ax,DSEG
mov ds,ax
;准备要加载到GDTR的伪描述符
mov bx,16
mul bx
add ax,OFFSET GDT ;计算并设置基地址
adc dx,0 ;界限已在定义时设置好
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
;设置代码段描述符
mov ax,cs
mul bx
mov WORD PTR Code.BaseL,ax ;代码段开始偏移为0
mov BYTE PTR Code.BaseM,dl ;代码段界限已在定义时设置好
mov BYTE PTR Code.BaseH,dh
;设置目标数据段描述符
mov ax,ds
mul bx ;计算并设置目标数据段基址
add ax,OFFSET Buffer
adc dx,0
mov WORD PTR DataD.BaseL,ax
mov BYTE PTR DataD.BaseM,dl
mov BYTE PTR DataD.BaseH,dh
;加载GDTR
lgdt QWORD PTR VGDTR
cli ;关中断
EnableA20 ;打开地址线A20
;切换到保护方式
mov eax,cr0
or eax,1
mov cr0,eax
;清指令预取队列,并真正进入保护方式
JUMP16 Code_Sel,<OFFSET Virtual>
Virtual: ;现在开始在保护方式下运行
mov ax,DataS_Sel
mov ds,ax ;加载源数据段描述符
mov ax,DataD_Sel
mov es,ax ;加载目标数据段描述符
cld
xor si,si
xor di,di ;设置指针初值
mov cx,BufLen/4 ;设置4字节为单位的缓冲区长度
repz movsd ;传送
;切换回实模式
mov eax,cr0
and al,11111110b
mov cr0,eax
;清指令预取队列,进入实方式
JUMP16 <SEG Real>,<OFFSET Real>
Real: ;现在又回到实方式
DisableA20
sti
mov ax,DSEG
mov ds,ax
mov si,OFFSET Buffer
cld
mov bp,BufLen/16
NextLine: mov cx,16
NextCh: lodsb
push ax
shr al,1
call ToASCII
EchoCh al
pop ax
call ToASCII
EchoCh al
EchoCh ' '
loop NextCh
EchoCh 0dh
EchoCh 0ah
dec bp
jnz NextLine
mov ax,4c00h
int 21h
Start ENDP
;----------------------------------------------------------------------------
ToASCII PROC
and al,0fh
add al,90h
daa
adc al,40h
daa
ret
ToASCII ENDP
;----------------------------------------------------------------------------
CSEG ENDS ;代码段定义结束
;----------------------------------------------------------------------------
END Start

3.关于实例步骤的注释
     在源程序的开头首先包含了文件“386SCD.INC”,在此包含文件中定义了保护模式程序设计要用到的一些结构、宏及常量。下面对各实现步骤作些说明。
     (1)切换到保护方式的准备工作
     在从实模式切换到保护模式之前,必须作必要的准备。准备工作的内容根据实际而定。最起码的准备工作是建立合适的全局描述符表,并使用GDTR指向该GDT。因为在切换到保护方式时,至少要把代码段的选择子装载到CS,所以GDT中至少含有代码段的描述符。
     从本实例源程序可见,全局描述符表GDT仅有四个描述符:第一个是空描述符;第二个是代码段描述符;第三个和第四个分别为源数据段及目标数据段描述符。本实例各描述符中的段界限是在定义时设置的,并且除伪描述符VGDTR中的界限按GDT的实际长度设置外,各使用的存储段描述符的界限都规定为0FFFFH。另外,描述符中的段属性也根据所描述段的类型被预置,各属性的定义在包含文件386SCD.INC中均有说明。从属性值可知,这三个段都是16位段。
     由于在切换到保护方式后就要引用GDT,所以在切换到保护方式前必须装载GDTR。实例中使用如下指令装载GDTR:
     LGDT QWORD PTR VGDTR
     该指令的功能是把存储器中的伪描述符VGDTR装入到全局描述符表寄存器GDTR中。伪描述符VGDTR的结构如前所述结构类型PDESC所示,低字是以字节位单位的全局描述符表段的界限,高双字为描述符表段的线性基地址(本实例不启用分页机制,所以线性地址等同于物理地址)。本实例中未涉及到局部描述符表及中断描述符表,后面的文章将作详细说明。
     (2)由实模式切换到保护模式
     在做好准备后,从实模式切换到保护模式并不难。原则上只要把控制寄存器CR0中的PE位置1即可。本实例采用如下三条指令设置PE位: 
     mov eax,cr0
     or eax,1
     mov cr0,eax
     实际情况要比这复杂些。执行上面的三条指令后,处理器转入保护模式,但CS中的内容还是实模式下代码段的段值,而不是保护模式下代码段的选择子,所以在取指令之前得把代码段的选择子装入CS。为此,紧接着这三条指令,安排一条如下所示的段间转移指令:
     JUMP16 Code_Sel ,< OFFSET Virtual >
     这条段间转移指令 在实模式下被预取并在保护方式下被执行 。利用这条段间转移指令可把保护模式下代码段的选择子装入CS,同时也刷新指令预取队列。从此真正进入保护模式。
     (3)由保护模式切换到实模式 
     在80386上,从保护模式切换到实模式的过程类似于从实模式切换到保护模式。原则上只要把控制寄存器CR0中的PE位清0即可。实际上,在此之后也要安排一条段间转移指令,一方面清指令预取队列,另一方面把实模式下代码段的段值送CS。 这条段间转移指令在保护方式下被预取并在实模式下被执行 。
     (4)保护模式下的数据传送 
     首先,把源数据段和目标数据段的选择子装入DS和ES寄存器,这两个描述符已在实模式下设置好,把选择子装入段寄存器就意味着把包括基地址在内的段信息装入到了段描述符高速缓冲寄存器。然后设置指针寄存器SI和DI的初值,也设置计数器CX的初值。根据预置的段属性,在保护方式下,代码段也仅是16位段,串操作指令只使用16位的SI、DI和CX等寄存器。最后利用串操作指令实施传送。
     (5)显示缓冲区中的内容
     由于缓冲区在常规内存中,所以在实模式下根据要求按十六进制显示其内容是很容易理解的,这里就不再多说。
     4.内存映象 
     在源程序中没有把GDT作为一个单独的段对待,但在进入保护方式后,它是一个独立的段。从对代码段和源数据段描述符所赋的基地址和段界限值可见,代码段和数据段有部分覆盖。尽管这样做不利于代码和数据的安全,但如果需要,这样做是可行的。本实例运行时的内存映象如下图所示。
                             
      5.特别说明
      作为第一个实模式和保护模式切换的例子,本实例作了大量的简化处理。 
      通常,由实模式切换到保护模式的准备工作还应包含建立中断描述符表。但本实例没有建立中断描述符表。为此,要求整个过程在关中断的情况下进行;要求不使用软中断指令;假设不发生任何异常。否则会导致系统崩溃。 
      本实例未使用局部描述符表,所以在进入保护模式后没有设置局部描述符表寄存器LDTR。为此,在保护模式下使用的段选择子都指定GDT中的描述符。 
      本实例未定义保护模式下的堆栈段,GDT中没有堆栈段描述符,在保护模式下没有设置SS,所以在保护方式下没有涉及堆栈操作的指令。 
      本实例各描述符特权级DPL和各选择子的请求特权级RPL均为0,在保护方式下运行时的当前特权级CPL也是0。 
      本实例没有采用分页管理机制,也即CR0中的PG位为0,线性地址就是存储单元的物理地址。 
      6.打开和关闭地址线A20
      PC及其兼容机的第21根地址线(A20)较特殊,计算机系统中一般安排一个 “门”控制该地址线是否有效。为了访问地址在1M以上的存储单元,应先打开控制地址线A20的“门”。这种设置与实模式下只使用最低端的1M字节存储空间有关,与处理器是否工作在实模式或保护方式无关,即使在关闭地址线A20时,也可进入保护模式。 
      如何打开和关闭地址线A20与计算机系统的具体设置有关。在本文中介绍的包含文件386SCD.INC中定义了两个宏,打开地址线A20的宏EnableA20和关闭地址线A20的宏DisableA20,此两个宏指令在一般的PC兼容机上都是可行的。
<二>演示32位代码段和16位代码段切换的实例(实例二) 
     实例二的逻辑功能是,以十六进制数和ASCII字符两种形式显示从内存地址100000H开始的16个字节的内容。
     从功能上看,本实例类似于实例一,但在实现方法上却有了改变,它更能反映出实模式和保护模式切换的情况。具体实现步骤是:(1)作切换到保护方式的准备;(2)切换到保护方式的一个32位代码段;(3)把指定内存区域的内容以字节为单位,转换成对应的十六进制数的ASCII码,并直接填入显示缓冲区实现显示;(4)再变换到保护方式下的一个16位代码段;(5)把指定内存区域的内容直接作为ASCII码填入显示缓冲区中实现显示;(6)切换回实模式。
     1.实例二源程序
     实例二的源程序如下所示: 
;名称:ASM2.ASM
;功能:演示实方式和保护方式切换(切换到32位代码段)
;----------------------------------------------------------------------------
INCLUDE 386SCD.INC
;----------------------------------------------------------------------------
DSEG SEGMENT USE16 ;16位数据
;----------------------------------------------------------------------------
GDT LABEL BYTE ;全局描述符表
DUMMY Desc <> ;空描述符
Normal Desc <0ffffh,,,ATDW,,> ;规范段描述符
Code32 Desc <C32Len-1,,,ATCE,D32,> ;32位代码段描述符
Code16 Desc <0ffffh,,,ATCE,,> ;16位代码段描述符
DataS Desc <DataLen-1,0,10h,ATDR,,> ;源数据段描述符
DataD Desc <3999,8000h,0bh,ATDW,,> ;显示缓冲区描述符
Stacks Desc <StackLen-1,,,ATDW,,> ;堆栈段描述符
;----------------------------------------------------------------------------
GDTLen = $-GDT ;全局描述符表长度
VGDTR PDesc <GDTLen-1,> ;伪描述符
;----------------------------------------------------------------------------
SaveSP DW ? ;用于保存SP寄存器
SaveSS DW ? ;用于保存SS寄存器
;----------------------------------------------------------------------------
Normal_Sel = Normal-GDT ;规范段描述符选择子
Code32_Sel = Code32-GDT ;32位代码段选择子
Code16_Sel = Code16-GDT ;16位代码段选择子
DataS_Sel = Datas-GDT ;源数据段选择子
DataD_Sel = DataD-GDT ;目标数据段选择子
Stacks_Sel = Stacks-GDT ;堆栈段描述符选择子
;----------------------------------------------------------------------------
DataLen = 16
;----------------------------------------------------------------------------
DSEG ENDS ;数据段定义结束
;----------------------------------------------------------------------------
StackSeg SEGMENT PARA STACK USE16
StackLen = 256
DB StackLen DUP(0)
StackSeg ENDS
;----------------------------------------------------------------------------
CSEG1 SEGMENT USE16 'REAL' ;16位代码段
ASSUME CS:CSEG1,DS:DSEG
;----------------------------------------------------------------------------
Start PROC
mov ax,DSEG
mov ds,ax
;准备要加载到GDTR的伪描述符
mov bx,16
mul bx
add ax,OFFSET GDT ;计算并设置基地址
adc dx,0 ;界限已在定义时设置好
mov WORD PTR VGDTR.Base,ax
mov WORD PTR VGDTR.Base+2,dx
;设置32位代码段描述符
mov ax,CSEG2
mul bx
mov WORD PTR Code32.BaseL,ax
mov BYTE PTR Code32.BaseM,dl
mov BYTE PTR Code32.BaseH,dh
;设置16位代码段描述符
mov ax,CSEG3
mul bx
mov WORD PTR Code16.BaseL,ax ;代码段开始偏移为0
mov BYTE PTR Code16.BaseM,dl ;代码段界限已在定义时设置好
mov BYTE PTR Code16.BaseH,dh
;设置堆栈段描述符
mov ax,ss
mov WORD PTR SaveSS,ax
mov WORD PTR SaveSP,sp
mov ax,StackSeg
mul bx
mov WORD PTR Stacks.BaseL,ax
mov BYTE PTR Stacks.BaseM,dl
mov BYTE PTR Stacks.BaseH,dh
;加载GDTR
lgdt QWORD PTR VGDTR
cli ;关中断
EnableA20 ;打开地址线A20
;切换到保护方式
mov eax,cr0
or al,1
mov cr0,eax
;清指令预取队列,并真正进入保护方式
JUMP16 Code32_Sel,<OFFSET SPM32>
ToReal: ;现在又回到实方式
mov ax,DSEG
mov ds,ax
mov sp,SaveSP
mov ss,SaveSS
DisableA20
sti
mov ax,4c00h
int 21h
Start ENDP
;----------------------------------------------------------------------------
CSEG1 ENDS ;代码段定义结束
;----------------------------------------------------------------------------
CSEG2 SEGMENT USE32 'PM32'
ASSUME CS:CSEG2
;----------------------------------------------------------------------------
SPM32 PROC
mov ax,Stacks_Sel
mov ss,ax
mov esp,StackLen
mov ax,DataS_Sel
mov ds,ax
mov ax,DataD_Sel
mov es,ax
xor esi,esi
xor edi,edi
mov ecx,DataLen
cld
Next: lodsb
push ax
CALL ToASCII
mov ah,7
shl eax,16
pop ax
shr al,4
CALL ToASCII
mov ah,7
stosd
mov al,20h
stosw
loop Next
JUMP32 Code16_Sel,<OFFSET SPM16>
SPM32 ENDP
;----------------------------------------------------------------------------
ToASCII PROC
and al,00001111b
add al,30h
cmp al,39h
jbe Isdig
add al,7
IsDig: ret
ToASCII ENDP
;----------------------------------------------------------------------------
C32Len = $
;----------------------------------------------------------------------------
CSEG2 ENDS
;----------------------------------------------------------------------------
CSEG3 SEGMENT USE16 'PM16'
ASSUME CS:CSEG3
;----------------------------------------------------------------------------
SPM16 PROC
xor si,si
mov di,DataLen*3*2
mov ah,7
mov cx,DataLen
AGain: lodsb
stosw
loop AGain
mov ax,Normal_sel
mov ds,ax
mov es,ax
mov ss,ax
mov eax,cr0
and al,11111110b
mov cr0,eax
jmp FAR PTR ToReal
SPM16 ENDP
;----------------------------------------------------------------------------
CSEG3 ENDS
;----------------------------------------------------------------------------
END Start

2.关于实现步骤的注释
    (1)切换到保护模式的准备工作 
     建立全局描述符表,这里的全局描述符表含有两个16位数据段的描述符、一个16位代码段的描述符和一个16位的堆栈段描述符。此外,GDT中还有一个32位的代码段描述符,描述32位代码段,该描述符的属性字段中的D位为1。 
    (2)由实模式切换到保护模式
     由实模式切换到保护模式32位代码段的方法与切换到16位代码段的方法相同。由保护模式16位代码段切换回实模式的方法与实例一相似。
     在保护模式下,通过如下直接段间转移指令从32位代码段切换到16位代码段:
     JUMP32 Code16_Sel ,< OFFSET SPM16 > 
     从该宏指令的定义可知,该转移指令含48位指针,其高16位是16位代码段的选择子,低32位是16位代码段的入口偏移。 该指令在32位方式下预取并执行 。由于在32位方式下执行,所以要使用48位指针。
    (3)显示指定内存区域的内容
     在本实例中,采用直接写显示缓冲区的方法实现显示。假设显示缓冲区的开始物理地址是0B8000H, 3号文本显示模式,在屏幕的第一行进行显示。
     3.特别说明
     本实例在保护方式下使用了涉及堆栈操作的指令,因此建立了一个16位的保护模式下的堆栈段。
     本实例仍作了大量的简化处理。如:没有建立IDT和LDT等,各特权级均是0。也没有采用分页管理机制。 
     从本实例的GDT中可见,两个数据段的界限都是根据实际大小而设置的。从源程序代码段CSEG3可见,在切换到实模式之前,把一个指向似乎没有用的数据段的描述符Normal的选择子装载到DS和ES。这是为什么呢? 
实模
式下
段描
述符
高速
缓冲
寄存
器的
内容段寄存器段基地址段界限(固定)段属性(固定)存在性特权级已存取粒度扩展方向可读性可写性可执行堆栈大小一致特权CS当前CS*160000FFFFHY0YBUYYY-NSS当前SS*160000FFFFHY0YBUYYNW-DS当前DS*160000FFFFHY0YBUYYN--ES当前ES*160000FFFFHY0YBUYYN--FS当前FS*160000FFFFHY0YBUYYN--GS当前GS*160000FFFFHY0YBUYYN--     在分段管理机制一文中已介绍过,每个段寄存器都配有段描述符高速缓冲寄存器,这些高速缓冲寄存器在实方式下仍发挥作用,只是内容上与保护模式下有所不同。如上表所示,其中“Y”表示“是”; “N”表示“否”;“B”表示字节;“U”表示向上扩展,“W”表示以字方式操作堆栈。段基地址仍是 32位,其值是相应段寄存器值(段值)乘以16,在把段值装载到段寄存器时刷新。由于其值是16位段值乘上16,所以在实模式下基地址实际上有效位只有20位。每个段的32位段界限都固定为0FFFFH,段属性的许多位也是固定的。所谓固定是指在实方式下不可设置这些属性值,只能继续沿用保护方式下所设置的值。因此,在准备结束保护模式回到实模式之前,要通过加载一个合适的描述符选择子到有关段寄存器,以使得对应段描述符高速缓冲寄存器中含有合适的段界限和属性。本实例GDT中的描述符Normal就是这样一个描述符,在返回实模式之前把对应选择子Normal_Sel加载到DS和ES就是此目的。由于SS段描述符中的内容已符合实模式的需要,所以尽管也改变了SS,但不需要重新加载SS(本实例中重新加载了SS,这除了稍增加运行时间外,并没有什么坏处)。16位代码段描述符中的内容也符合实模式的需要,所以在通过16位代码段返回实模式时,CS段描述符中的内容也符合实模式的要求。需要注意的是,不能从32位代码段返回实模式,这是因为无法实现从32位代码段返回时CS高速缓冲寄存器中的属性符合实模式的要求(实模式不能改变段属性)。顺便说以下,实例一中的描述符都是符合实模式要求的。段描述符高速缓冲寄存器中含有合适的段界限
4.关于32位代码段程序设计的说明
     在32位代码段中,缺省的操作数大小是32位,缺省的存储单元地址大小是32位。由于串操作指令使用的指针寄存器是ESI和EDI,LOOP指令使用的计数器是ECX,所以,在代码段CSEG2中,为了使用串操作指令,对ESI和EDI等寄存器赋初值。请比较代码段CSEG3中的相关片段和实例一中的相关片段,它们是16位代码段。

原创粉丝点击