【我所認知的BIOS】—>實模式&保護模式切换实例
LightSeed
2009-6-23
上一章從基礎概念上我談了談我的理解。素不知“紙上得來終覺淺,絕知此事要躬行”呀!不多說二話,我們來詳細剖析實模式和保護模式的相互切換。(我儘量解釋清楚每一句話,以下舉的例子是楊季文老師書里一個最簡單的例子)
Let us go!
1、這個例子中code做的事
下麵我們看到的這個code做了這樣的操作:
① 在實模式進入保護模式
② 在保護模式里把高端內存的value copy到低端內存中來(buffer中)
③ 返回到實模式
④ 在實模式下,顯示buffer中的內存數值。
對此需要說明的一點是這個例子是一個很簡單很簡單地實模式和保護模式的切換,中間很多東西都沒有考慮,不過拿來作為我們學習保護模式的入門我想到是再好不過了。
2、原程式加詳細註釋
;-------------这段代码copy form<80X86汇编语言程序设计教程>--------
;-------------只是一个最简单的实模式与保护模式的相互切换----------
;-------------切忌,本程序编译连接后生成的exe文件在纯DOS下--------
;-------------才能够执行LGDT这个命令,才能顺利进入保护模式--------
;-------------宏定义区域开始--------------------------------------
;16位偏移的段间直接转移指令的宏定义
JUMP macro selector,offsetv
db 0eah ;操作码 jmp
dw offsetv ;16位偏移
dw selector ;段值(real mode下)或者选择子(protect mode下)
endm
;字符显示宏指令的定义
ECHOCH macro ascii
mov ah, 2 ;选功能号
mov dl, ascii ;填将要显示的ASCII码给DL
int 21h ;调用DOS中断来显示ASCII码
endm
;-------------宏定义区域结束-------------------------------------
;-------------结构体定义区域开始---------------------------------
;存储段描述符结构类型的定义
DESCRIPTOR struc
Limitl dw 0 ;段界限(0~15)
Basel dw 0 ;段基地址(0~15)
Basem db 0 ;段基地址(16~23)
Attributes dw 0 ;段属性
Baseh db 0 ;段基地址(24~31)
DESCRIPTOR ENDS
;伪描述符结果类型的定义
PDESC struc
Limit dw 0 ;16界限
Base dd 0 ;基地址
PDESC ENDS
;-------------结构体定义区域结束---------------------------------
;常量定义
ATDW = 92H ;存在的可读写数据段属性值
ATCE = 98H ;存在的只执行代码段属性值
.386P
;--------------实模式下数据段定义开始----------------------------
dseg segment use16
align 16 ;16位段
GDT label byte ;全局描述符表GDT标志
DUMMY DESCRIPTOR <> ;空描述符【鉴于仅仅说明实模式与保护模式切换,为什么为空如果有兴趣我们再探讨】
CODE DESCRIPTOR <0FFFFH,,,ATCE,>;代码段描述符
CDDE_SEL = CODE - GDT ;代码段描述的选择子
DATAS DESCRIPTOR <0FFFFH,8eacH,20H,ATDW,0>;源数据段描述符
DATAS_SEL = DATAS - GDT ;源数据段描述符的选择子
DATAD DESCRIPTOR <0FFFFH,,,ATDW,>;目标数据段描述符
DATAD_SEL = DATAD - GDT ;目标数据段描述符的选择子
GDTLEN = $ - GDT ;全局描述符表长度
;
VGDTR PDESC <GDTLEN-1,>;①伪描述符
;
BUFFERLEN = 256 ;缓冲区字节长度
BUFFER DB BUFFERLEN DUP(0);缓冲区
dseg ends
;---------------实模式下数据段定义结束---------------------------
;---------------实模式下代码段定义开始---------------------------
cseg segment use16 ;16位段
assume cs:cseg,ds:dseg ;声明代码段和数据段
start:
mov ax,dseg
mov ds,ax ;初始化数据段
;准备要加载到GDTR的伪描述符
mov bx,16 ;乘数为16,是为了在实模式中计算地址
mul bx ;计算并设置GDT基地址
add ax,offset GDT ;此时AX中为GDT在实模式中的地址,界限在已定义时设置妥当
adc dx,0 ;如果有进位那么ADC加上
mov word ptr VGDTR.Base,ax;填入GDT的实际地址的低word到伪描述符结构体中
mov word ptr VGDTR.Base+2,dx ;填入GDT的实际地址的高word到伪描述符结构体中
;设置代码段描述符
mov ax,cs
mul bx ;计算代码段在实模式中的实际地址
mov code.Basel,ax ;代码段开始偏移为0
mov code.Basem,dl ;代码段界限已在定义时设置妥当
mov code.Baseh,dh
;设置目标数据段描述符
mov ax,ds
mul bx ;计算数据段在实模式中的实际地址
;计算并设置目标数据段基地址
add ax,offset BUFFER ;加上offset
adc dx,0 ;如果有进位那么ADC加上
mov DATAD.Basel,ax ;不解释了,同上
mov DATAD.Basem,dl
mov DATAD.Baseh,dh
;加载GDTR
DB 66H ; execute a 32 bit LGDT
LGDT VGDTR ;命令不熟悉的话去查查
cli ;关中断
call EnableA20 ;打开地址线A20
;切换到保护模式
mov eax,cr0
or eax,1
mov cr0,eax
;清指令预取队列,并真正进入保护方式
JUMP <CDDE_SEL>,<offset VIRTUAL> ;②far jmp目的是显示地修改CS,刷新段选择子的hidden部分(此步骤后面我会详细回头探讨)
VIRTUAL:
MOV AX,DATAS_SEL ;源数据段选择子
MOV DS,AX ;加载源数据段描述符
MOV AX,DATAD_SEL ;源数据段选择子
mov es,ax ;加载目标数据段描述符
cld ;指针(si & di)累加
xor si,si ;设置指针初值
xor di,di
mov cx,BUFFERLEN/4 ;设置4字节为单位的缓冲区长度
repz movsd ;传送
;切回到实方式
mov eax,cr0
and eax,0fffffffeh
mov cr0,eax
;清指令预取队列,进入实方式
;JUMP <seg REAL>,<OFFSET REAL> ;far jmp目的是显示地修改CS,刷新段寄存器的hidden部分(此步骤后面我会详回头探讨)
PUSH ss
push sp
REAL: ;现在又回到实方式
call DisableA20 ;③关闭地址线A20
sti ;开中断
mov ax,dseg ;重置数据段寄存器
mov ds,ax
mov si,offset BUFFER
cld ;显示缓冲区内容
mov bp,BUFFERLEN/16 ;bp作为显示的行数的计数器
Nextline:
mov cx,16 ;每行只显示16*2个字符
NextCH:
lodsb ;mov al,ds:[si],si++
push ax ;保存ax
shr al,4 ;准备显示高4bit中的值
call ToASCII ;把al中的值转换成ASCII
ECHOCH al ;显示之
pop ax ;回复ax
call ToASCII
ECHOCH al
ECHOCH ' ' ;在字符于字符之间显示空格
loop NextCH ;处理下一个字符
ECHOCH 0dh
ECHOCH 0ah ;显示这两个ASCII回车+换行
dec bp ;显示完了否?
jnz Nextline ;bp = 0显示完了
mov ax,4c00h ;结束
int 21h
;---------------实模式下代码段定义结束--------------------------
;---------------子程式定义开始----------------------------------
;***************************************************************
;子程序名 :HtoASC
;功 能 :十六进制数转换成 ASCII
;入口参数 :al=8位二进制数
;出口参数 :无
;说 明 :无
;***************************************************************
toASCII proc
and al,0fh ;屏蔽al的高4 bits
cmp al,9 ;compare 9
jbe toASCII1 ;小于9,直接+30H
add al,37h ;否则,al+37H
ret
toASCII1:
add al,30h
ret
toASCII endp
;***************************************************************
;打开a20地址线
;***************************************************************
EnableA20 proc
push ax
in al,92h
or al,00000010b
out 92h,al
pop ax
ret
EnableA20 endp
;***************************************************************
;关闭a20地址线
;***************************************************************
DisableA20 proc
push ax
in al,92h
and al,11111101b
out 92h,al
pop ax
ret
DisableA20 endp
;---------------子程式定义结束----------------------------------
cseg ends
;---------------实模式下代码段定义结束--------------------------
end start ;指明程序入口
3、說明程序中我標記(一定需要說明的)
3.1 ①伪描述符
爲什麽我們要專門拿一個結構體來給LDGT這個命令用呢?原因是這樣的,由於GDT不能由GDT本身之類的描述符進行描述定義,所以處理器採用GDTR為GDT這一特色的系統段提供一個偽描述符。它的數據格式如圖1。
圖1 GDTR給定GDT的地址
在用GDTR的是,我們必須要注意到,由於CPU取的時候是以word的形式來讀取的,所以GDTR的地址必須是以word對齊的。
那麼我們怎麼用這個GDTR呢?用programming guide里的原話說“Before the GDT can be used, the base address and limit for the GDT must be loaded into the GDTR register using an LGDT instruction”
並且我們在用LGDT這個命令的時候必須要始終保證是word對齊的。於是我們就用一個結構體來存儲這個特殊的偽描述符了。(當然啦,你是可以不用定義結構體的,只要能夠讓CPU識別的到就沒有任何問題啦。)
3.2 ②far jmp的作用
3.2.1 提出疑問,爲什麽要用far jmp?
爲什麽我要專門把這個提出來呢?是因為在當時學的時候,就有個疑問。“爲什麽在programming guide”裏面和《80X86彙編語言程序設計教程》里都“淺淺”地說,“清指令預取隊列(prefetch queue)”。
3.2.2 清prefetch queue的實質
我們再詳細查資料可以知道,在mov cr0,eax执行后,处理器实际上已经处于保护模式。然而,Prefetch Queue值仍为实模式下的值。(其中最重要的是cs)注意,段寄存器不仅包括16位可见部分,还包括48位高速缓冲(也就是我们programming guide里的,“visible” part and a “hidden” part)。在进入保护模式后,这些段寄存器高速缓冲中的内容仍然存在,而且CPU为了其执行指令的效率只要CS或者其他的DS等等段寄存器如果没有改变,那么CPU都直接从这些高速缓冲器中去取CS和DS的值。(但是进入保护模式后,CPU预取的指令却全是实模式下的地址,当然会在保护模式下寻址时出问题。)那么我们就需要改变CS等等段寄存器的值(不過在保护模式下应该叫做段选择子了)需要显式(就是直接修改CS的意思)地重新加载。加载其他段寄存器可以mov,但是只有jmp(far ptr)可以加载cs。注意,还有一些转移指令,如retf,iret等,但是这牵扯到保护模式下复杂的远返回操作,而且需要堆栈(通常这个时候保护模式堆栈还没有准备好)所以far jmp是最简单有效的办法。所以最主要的还是要去改变CS的值,从而能够在保护模式下让code正常运行。顺便提示一下,我们著名的the big real mode就是运用了这个原理,当不去改变相应段选择子的時候,他们各自的hidden部分(高速缓冲部分)是不会被重新加载的。所以我们进入保护模式后修改好相应段选择子然后返回real mode,直接可以访问4G内存空间。
3.2.3 讓我們大膽去猜想并驗證
我想大多數人到了這裡可能都會想,那麼我們的CALL指令等等其他可以改變CS的指令都可以做到JMP的作用么(去改變CS)?我可以回答大家,答案是肯定的!如果大家有興趣,您可以修改一下上面的程式,看看能否達到您的效果。(筆者驗證是可以正確切換和訪問的)
3.3 ③關於A20
A20這個東西,介紹它的東西很多。Detail的東西我就不詳細講了,我想把大概的東西說一下(筆者:宏觀上起碼我們要知道它究竟是做什麽用的)。
3.3.1 再談實模式
現在的實模式與當初的8086CPU是緊密相關的。在實模式下我們只能訪問1M以內的內存,而且訪問的形式是segment:offset。(筆者:那麼不少人肯定會問用segment:offset的方式訪問內存的話,
FFFF * 16 + FFFF + 1
= FFFF0 + FFFF + 1
= FFFF0 + 10000
=10FFF0
所以上面的算法,實模式是可以訪問1M以上的空間的。)沒錯這種算法是沒有問題的,但是我們要想想當初的8086CPU它其實只有20根地址線,那麼他的定址最大就為220(即為1M)。所以在實模式下麵只能訪問1M了。
3.3.2 A20作用
正因為第一點的說明,我們現在的電腦定址能力早都已經超越20根地址線了。但是我們早已用實模式開發了很多的東西,這種訪問形式我們又捨不得擯棄,那麼就出現了A20這個東東。當A20沒有被打開的是,我們能夠訪問的最大地址就為1M。我們進入實模式要想訪問1M以上的內存就必須要把A20打開。否則訪問肯定就會出問題。
3.3.3 A20原理
A20的存在純粹就是為了兼容而做的東西,它其實是CPU的一根地址線,在我們的電腦上電的默認情況下它是一直都被拉低的。在南橋的datasheet里我們可以找到相應的說明,有相應的寄存器做控制。
4、运行程序后的截图
圖2是用adu讀取208EACH處及其后256個bytes的數據顯示,圖3是用我們寫的這個程式讀取的數據。可以看出完全是一模一樣的。
圖2
圖3
至此關於實模式和保護模式的切換實例就告一段落了。
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/lightseed/archive/2009/06/24/4295204.aspx