儲存器的保護(三)——《x86組合語言:從真實模式到保護模式》讀書筆記20
儲存器的保護(三)
修改本章程式碼清單,使之可以檢測1MB以上的記憶體空間(從地址0x0010_0000開始,不考慮快取記憶體的影響)。要求:對記憶體的讀寫按雙字的長度進行,並在檢測的同時顯示已檢測的記憶體數量。建議對每個雙字單元用兩個花碼0x55AA55AA和0xAA55AA55進行檢測。
上面的文字選自原書第12章的習題1.
這篇博文就討論一下這道題。由於是初學,我不對自己做太高的要求,只要實現功能即可。
程式碼清單
;檔案說明:第12章習題-1
;建立日期:2016-3-7
;--------- equ some colors
GREEN equ 0x02
RED equ 0x04
BLUE_LIGHT equ 0x09
YELLOW equ 0x0e
MEMORY_START equ 0x100000
MEMORY_END equ 0x800000
MEMORY_SIZE equ (MEMORY_END-MEMORY_START)/4 ;以雙字為單位
LENGTH_OF_BAR equ 6 ; 表示2的6次方
BAR_POSITION equ 10*80+4 ;進度條的位置
;設定堆疊段和棧指標
mov eax,cs
mov ss,eax
mov sp,0x7c00
mov ah,0x00; 清屏
mov al,0x03
int 0x10
;計算GDT所在的邏輯段地址
mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位線性基地址
xor edx,edx
mov ebx,16
div ebx ;分解成16位邏輯地址
mov ds,eax ;令DS指向該段以進行操作
mov ebx,edx ;段內起始偏移地址
;跳過0 #描述符
;建立1#描述符,這是一個數據段,對應0~4GB的線性地址空間
mov dword [ebx+0x08],0x0000ffff ;基地址為0,段界限為0xfffff
mov dword [ebx+0x0c],0x00cf9200 ;粒度為4KB,儲存器段描述符
;建立保護模式下初始程式碼段描述符,程式碼段可讀
mov dword [ebx+0x10],0x7c0001ff ;基地址為0x00007c00,512位元組
mov dword [ebx+0x14],0x00409a00 ;粒度為1個位元組,程式碼段描述符
;建立棧段描述符
mov dword [ebx+0x18],0x7c00fffe
mov dword [ebx+0x1c],0x00cf9600
;初始化描述符表暫存器GDTR
mov word [cs: pgdt+0x7c00],31 ;描述符表的界限
lgdt [cs: pgdt+0x7c00]
in al,0x92 ;南橋晶片內的埠
or al,0000_0010B
out 0x92,al ;開啟A20
cli ;中斷機制尚未工作
mov eax,cr0
or eax,1
mov cr0,eax ;設定PE位
;以下進入保護模式... ...
jmp dword 0x0010:flush ;16位的描述符選擇子:32位偏移
[bits 32]
flush:
mov eax,0x0008 ;載入資料段(0..4GB)選擇子; ds,es,fs,gs指向了(0..4G)
mov ds,eax
mov es,eax
mov fs,eax
mov gs,eax
mov eax,0x0018 ;載入棧段選擇子
mov ss,eax
xor esp,esp ;ESP <- 0
; 繪製白色條
push (1<<LENGTH_OF_BAR) ;number of blocks
push BAR_POSITION
push 0x7720 ; white block
call put_char
push 21*80+25
push BLUE_LIGHT
push MEMORY_SIZE
call show_hex_dword ;顯示總共要檢測的數量(以雙字為單位)
; 顯示 '/'
push 1
push 21*80+23
push 0x092f ; 藍色的'/'
call put_char
xor ecx,ecx ;計數器清零,記錄檢測了多少個雙字
mov ebx,MEMORY_START ;檢測的起始地址
;-----------------------------------------------------
exam: ;顯示正在檢測的地址
push 21*80+6
push YELLOW
push ebx
call show_hex_dword
mov dword [es:ebx],0x55aa55aa
cmp dword [es:ebx],0x55aa55aa
jnz err
mov dword [es:ebx],0xaa55aa55
cmp dword [es:ebx],0xaa55aa55
jnz err
add ebx,4 ;地址增加4個位元組
inc ecx
push 21*80+15
push BLUE_LIGHT
push ecx
call show_hex_dword ;顯示已經檢測的數量(以雙字為單位)
push BAR_POSITION ;繪製進度條
push ecx
push MEMORY_SIZE
call draw_progress_bar
cmp ebx,MEMORY_END
jnz exam
err:
hlt
;--------------------------------------
;功能:在指定位置顯示N個字元
;輸入: push 顯示的個數
; push (x*80+y), 表示x行y列
; push 屬性和字元
;返回:無
put_char:
pushad
mov ebp,esp
mov ecx,[ebp+11*4] ; 取得個數
mov ebx,[ebp+10*4] ; 取得位置
mov ax,[ebp+9*4] ;取得屬性和字
put:
mov [es:0xb8000+ebx*2],ax
inc ebx
loop put
popad
ret 3*4
;-----------------------------------------
;功能:根據比例在指定位置繪製進度條
;輸入:
; push (x*80+y), 表示x行y列
; push 分子
; push 分母
;返回:無
draw_progress_bar:
pushad
mov ebp,esp
mov esi,[ebp+11*4] ; 取得位置
mov eax,[ebp+10*4] ; 取得分子
mov ebx,[ebp+9*4] ;取得分母
shr ebx,LENGTH_OF_BAR
xor edx,edx
div ebx
cmp eax,1
jb out
push eax
push esi
push 0x2020; 綠色背景,空格
call put_char
out:
popad
ret 3*4
;-----------------------------------
;功能:在指定位置顯示16進位制的數字
;輸入:
; push (x*80+y), 表示x行y列
; push 屬性
; push 要顯示的值
;返回:無
show_hex_dword:
pushad
mov ebp,esp
mov esi,[ebp+11*4] ;取得@1:(x,y)
mov eax,[ebp+9*4] ;取得@3:value
mov ebx,16
xor ecx,ecx
remainder:
xor edx,edx
div ebx
inc ecx
push edx
cmp eax,0
jnz remainder
mov ah,[ebp+10*4] ;取得屬性
print:
pop ebx
mov al,[cs:string_hex+ebx]
mov [es:0xb8000+esi*2],ax
inc esi
loop print
popad
ret 3*4
;-------------------------------------------------------------------------------
pgdt dw 0
dd 0x00007e00 ;GDT的實體地址
string_hex: db'0123456789ABCDEF'
;-------------------------------------------------------------------------------
times 510-($-$$) db 0
db 0x55,0xaa
程式碼分析
設計思路
- 這個程式實現的主要功能是:檢測1MB以上的記憶體空間,比如檢測實體地址為1M~8M的單元。
- 檢測方法是向每個雙字單元寫入0x55aa55aa,並讀出來和0x55aa55aa做比較,如果相等,則再寫入0xaa55aa55,並讀出來和0xaa55aa55作比較,如果相等,那麼這個雙字單元是OK的,把實體地址加上4,繼續檢測。如果讀出的和寫入的不相等,那麼檢測出錯,程式停止。
- 檢測的時候,顯示正在檢測的記憶體地址
- 顯示一個進度條
- 顯示“已經檢測的記憶體數 / 總共需要檢測的記憶體數”
下面我們分析具體的實現。不打算逐行講述所有程式碼,僅選擇重點部分講解。
定義一些常量
GREEN equ 0x02 ; 黑底綠字
RED equ 0x04 ; 黑底紅字
BLUE_LIGHT equ 0x09 ; 黑底藍色字
YELLOW equ 0x0e ; 黑底黃字
MEMORY_START equ 0x100000
MEMORY_END equ 0x800000
MEMORY_SIZE equ (MEMORY_END-MEMORY_START)/4 ;以雙字為單位
LENGTH_OF_BAR equ 6 ; 表示2的6次方
BAR_POSITION equ 10*80+4 ;進度條的位置
前四行定義了字元屬性;
中間三行定義了要檢測的記憶體起始地址,結束地址(檢測不包含結束地址),還有檢測的記憶體大小(以雙字為單位)。之所以用equ定義是因為修改起來方便。
LENGTH_OF_BAR equ 6 ; 表示2的6次方
這句話表示進度條的總長度佔64(2^6=64)個字元,當然可以根據需要修改。但應該是2的N次方(具體原因下文會說明)。
BAR_POSITION equ 10*80+4 ;進度條的位置
這行定義了進度條的位置,如果是x行y列,對應的表示就是(x*80+y);因為一行有80個字元。
清屏
mov ah,0x00; 清屏
mov al,0x03
int 0x10
這三行程式碼是為了清屏。具體原理可以參見我的博文《BIOS功能呼叫之滾屏與清屏》
建立GDT
;跳過0#描述符
;建立1#描述符,這是一個數據段,對應0~4GB的線性地址空間
mov dword [ebx+0x08],0x0000ffff ;基地址為0,段界限為0xfffff
mov dword [ebx+0x0c],0x00cf9200 ;粒度為4KB,儲存器段描述符
;建立保護模式下初始程式碼段描述符,程式碼段可讀
mov dword [ebx+0x10],0x7c0001ff ;基地址為0x00007c00,512位元組
mov dword [ebx+0x14],0x00409a00 ;粒度為1個位元組,程式碼段描述符
;建立棧段描述符
mov dword [ebx+0x18],0x7c00fffe
mov dword [ebx+0x1c],0x00cf9600
以上程式碼用於建立GDT。由於想在載入程式中實現全部功能,所以編譯後的檔案不能超過512位元組。為了節省筆墨,我跳過了0#描述符。
關於程式碼段,必須是可讀的,因為過程“show_hex_dword”需要訪問程式碼段中的一個表格:
string_hex: db'0123456789ABCDEF'
關於棧段描述符的定義,具體講解參見 儲存器的保護(一)——《x86組合語言:從真實模式到保護模式》讀書筆記18 http://blog.csdn.net/longintchar/article/details/50759826
繪製白色條
; 繪製白色條
push (1<<LENGTH_OF_BAR) ;number of blocks
push BAR_POSITION
push 0x7720 ; white block
call put_char
這裡呼叫了過程 put_char
;--------------------------------------
;功能:在指定位置顯示N個字元
;輸入: push 顯示的個數
; push (x*80+y), 表示x行y列
; push 屬性和字元
;返回:無
put_char:
pushad
mov ebp,esp
mov ecx,[ebp+11*4] ; 取得個數
mov ebx,[ebp+10*4] ; 取得位置
mov ax,[ebp+9*4] ;取得屬性和字元
put:
mov [es:0xb8000+ebx*2],ax
inc ebx
loop put
popad
ret 3*4
以前我們都是用暫存器傳遞引數,這次我們用棧傳遞引數。在呼叫過程之前,先按照要求把引數壓入棧中。當進入過程,執行完pushad這條指令後,棧的情況如下圖:
這裡用到了pushad和popad指令,如果你不懂的話,可以參考我的另一篇博文:
《PUSHA/PUSHAD POPA/POPAD 指令詳解》
所以以下四行就可以取得棧中的引數。
mov ebp,esp
mov ecx,[ebp+11*4] ; 取得個數
mov ebx,[ebp+10*4] ; 取得位置
mov ax,[ebp+9*4] ;取得屬性和字元
還有一點需要說明,
ret 3*4
這句話使用了帶運算元的過程返回指令。這種用法在原書P278頁講解了。
如果希望在過程返回的同時,順便彈出呼叫者壓入的引數(使棧平衡),那麼可以用帶運算元的過程返回指令。指令格式是:
ret imm16
retf imm16
這兩條指令都允許用16位的立即數作為引數,不同之處僅在於前者是近返回,後者是遠返回。立即數一般總是偶數,原因是棧操作總是以字或者雙字進行。立即數的值表示在過程返回時應當從棧中彈出多少位元組的資料。
對於我們的put_char
過程,因為呼叫的時候壓入了3個引數(3*4=12位元組),所以ret
後面的引數是12.
push 0x7720
這句表示壓入白底的空格符,顯示出來就是白色的小方塊了。
顯示總共要檢測的記憶體數量(以雙字為單位)
push 21*80+25
push BLUE_LIGHT
push MEMORY_SIZE
call show_hex_dword ;顯示總共要檢測的數量(以雙字為單位)
依然用棧來傳遞引數,呼叫了過程show_hex_dword
;
;-----------------------------------
;功能:在指定位置顯示16進位制的數字
;輸入:
; push (x*80+y), 表示x行y列
; push 屬性
; push 要顯示的值
;返回:無
show_hex_dword:
pushad
mov ebp,esp
mov esi,[ebp+11*4] ;取得@1:(x,y)
mov eax,[ebp+9*4] ;取得@3:value
mov ebx,16
xor ecx,ecx
remainder:
xor edx,edx
div ebx
inc ecx
push edx
cmp eax,0
jnz remainder
mov ah,[ebp+10*4] ;取得屬性
print:
pop ebx
mov al,[cs:string_hex+ebx]
mov [es:0xb8000+esi*2],ax
inc esi
loop print
popad
ret 3*4
這段程式碼的功能就是在指定的位置(壓入第一個引數,比如3行4列就寫 push 3*80+4
),顯示指定屬性(壓入第二個引數,僅低位元組有效,比如綠色0x02
)的16進位制數字(壓入第三個引數,比如想在螢幕上顯示16進位制的8b9c,那麼就push 0x8b9c
).
這段程式碼的設計思路就是把要顯示的數不斷除以16(因為是以16進位制顯示),並且把餘數壓棧,直到商等於0.之後再從棧依次彈出餘數,把餘數作為索引值查表,將對應的字元寫到螢幕上。查表的關鍵語句是:
mov al,[cs:string_hex+ebx]
表格定義在原始檔的倒數第三行
string_hex: db'0123456789ABCDEF'
因為查表需要對程式碼段進行訪問,所以在建立程式碼段描述符的時候,一定要讓程式碼段可讀。
開始記憶體檢測
xor ecx,ecx ;計數器清零,記錄檢測了多少個雙字
mov ebx,MEMORY_START ;檢測的起始地址
在檢測之前,計數器清零,檢測的起始地址傳送到EBX暫存器。
exam: ;顯示正在檢測的地址
push 21*80+6
push YELLOW
push ebx
call show_hex_dword
mov dword [es:ebx],0x55aa55aa
cmp dword [es:ebx],0x55aa55aa
jnz err
mov dword [es:ebx],0xaa55aa55
cmp dword [es:ebx],0xaa55aa55
jnz err
add ebx,4 ;地址增加4個位元組
inc ecx
push 21*80+15
push BLUE_LIGHT
push ecx
call show_hex_dword ;顯示已經檢測的數量(以雙字為單位)
push BAR_POSITION ;繪製進度條
push ecx
push MEMORY_SIZE
call draw_progress_bar
cmp ebx,MEMORY_END
jnz exam
err:
hlt
上面的程式碼就是記憶體檢測的主體部分了。
首先顯示正在檢測的地址(要檢測的地址在ebx中)。然後向這個地址寫入花碼,並讀出比較,如果不相等,就跳轉到
err:
hlt
如果相等,則ebx加上4,ecx加上1,並且顯示ecx的值,繪製進度條,然後繼續檢測。
繪製進度條
;-----------------------------------------
;功能:根據比例在指定位置繪製進度條
;輸入:
; push (x*80+y), 表示x行y列
; push 分子
; push 分母
;返回:無
draw_progress_bar:
pushad
mov ebp,esp
mov esi,[ebp+11*4] ; 取得位置
mov eax,[ebp+10*4] ; 取得分子
mov ebx,[ebp+9*4] ;取得分母
shr ebx,LENGTH_OF_BAR
xor edx,edx
div ebx
cmp eax,1
jb out
push eax
push esi
push 0x2020; 綠色背景,空格
call put_char
out:
popad
ret 3*4
上面的這個過程是在指定的位置繪製進綠色的進度條,要求壓入三個引數。第一個是位置,第二個是分子,第三個是分母。
比如說要檢測160個雙字,當前已經檢測了10個了,那麼第二個引數就是10,第三個引數就是160。如果之前的白色條的長度是64,那麼就繪製64*(10/160)=4個綠色方塊。看上去的效果就是綠色條的長度是總長度的十六分之一。
在每次檢測4個位元組後,我們就呼叫這個過程,這樣程式執行後就有一個動畫效果了。
這個過程實現的關鍵是計算出要繪製多少個綠色空格。
假設白色空格數可以表示成2的m次方。
計算公式推導如下圖:
根據公式,我們把ebx右移LENGTH_OF_BAR(=6)位,作為除數,被除數就在eax中,然後edx清零,再然後
edx:eax / ebx(移位運算後的值) = eax …edx
餘數捨去,假如計算出來畫1.5個方塊,那麼就繪製1個。
需要注意的是,計算後eax的值可能為0,如果為0就一定要跳出,一個綠色方塊也不繪製。
如果eax大於等於1,那麼呼叫過程put_char
繪製綠色方塊。
好了,整個程式碼的分析就到這裡了,我們趕緊看看結果吧。
檢測結束後:
【end】