修改本章代碼清單,使之能夠檢測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
下面咱們分析具體的實現。不打算逐行講述全部代碼,僅選擇重點部分講解。動畫
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指針
;跳過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】