存儲器的保護(三)——《x86彙編語言:從實模式到保護模式》讀書筆記20

存儲器的保護(三)

修改本章代碼清單,使之能夠檢測1MB以上的內存空間(從地址0x0010_0000開始,不考慮高速緩存的影響)。要求:對內存的讀寫按雙字的長度進行,並在檢測的同時顯示已檢測的內存數量。建議對每一個雙字單元用兩個花碼0x55AA55AA和0xAA55AA55進行檢測。緩存

上面的文字選自原書第12章的習題1.
這篇博文就討論一下這道題。因爲是初學,我不對本身作過高的要求,只要實現功能便可。oop

代碼清單

;文件說明:第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

代碼分析

設計思路

  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        ; 表示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個字符。.net

清屏

mov ah,0x00; 清屏
        mov al,0x03
        int 0x10

這三行代碼是爲了清屏。具體原理能夠參見個人博文《BIOS功能調用之滾屏與清屏》設計

http://blog.csdn.net/longintchar/article/details/50806752指針

建立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/50759826code

繪製白色條

; 繪製白色條
        push (1<<LENGTH_OF_BAR) ;number of blocks
        push BAR_POSITION
        push 0x7720 ; white block
        call put_char

這裏調用了過程 put_charblog

;--------------------------------------     
;功能:在指定位置顯示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 指令詳解》

http://blog.csdn.net/longintchar/article/details/50866801

因此如下四行就能夠取得棧中的參數。

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】

相關文章
相關標籤/搜索