1. 程式人生 > >儲存器的保護(三)——《x86組合語言:從真實模式到保護模式》讀書筆記20

儲存器的保護(三)——《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 ; 表示26次方 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 ;基地址為0x00007c00512位元組 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), 表示xy列 ; 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), 表示xy列 ; 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), 表示xy列 ; 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

程式碼分析

設計思路

  1. 這個程式實現的主要功能是:檢測1MB以上的記憶體空間,比如檢測實體地址為1M~8M的單元。
  2. 檢測方法是向每個雙字單元寫入0x55aa55aa,並讀出來和0x55aa55aa做比較,如果相等,則再寫入0xaa55aa55,並讀出來和0xaa55aa55作比較,如果相等,那麼這個雙字單元是OK的,把實體地址加上4,繼續檢測。如果讀出的和寫入的不相等,那麼檢測出錯,程式停止。
  3. 檢測的時候,顯示正在檢測的記憶體地址
  4. 顯示一個進度條
  5. 顯示“已經檢測的記憶體數 / 總共需要檢測的記憶體數”

下面我們分析具體的實現。不打算逐行講述所有程式碼,僅選擇重點部分講解。

定義一些常量

        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        ; 表示26次方
        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),  表示xy列
;      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),  表示xy列
;      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),  表示xy列
;      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】