程序的加载和执行(一)——《x86汇编语言:从实模式到保护模式》读书笔记21
程序的加載和執行(一)
本文及之后的幾篇博文是原書第13章的學習筆記。
本章主要是學習一個例子,對應的代碼分為3個文件:
因為代碼比較長,完整的我就不貼了。有需要朋友的可以到http://download.csdn.net/detail/u013490896/9388139下載。
本章的例子清楚地說明了4個步驟:
1. 主引導程序開始執行
2. 主引導程序加載內核(其實這個內核太簡陋了,只是為了說明原理,我們就這樣叫吧),并轉交控制權給內核
3. 內核加載用戶程序,執行用戶程序
4. 用戶程序通過調用內核例程返回到內核
內核的結構
我把代碼清單13-2的源文件精簡了一下,以清晰表示內核的結構。
;代碼清單13-2 ;文件名:c13_core.asm ;文件說明:內核結構 ;以下常量定義部分。內核的大部分內容都應當固定 core_code_seg_sel equ 0x38 ;內核代碼段選擇子 core_data_seg_sel equ 0x30 ;內核數據段選擇子 sys_routine_seg_sel equ 0x28 ;系統公共例程代碼段的選擇子 video_ram_seg_sel equ 0x20 ;視頻顯示緩沖區的段選擇子 core_stack_seg_sel equ 0x18 ;內核堆棧段選擇子 mem_0_4_gb_seg_sel equ 0x08 ;整個0-4GB內存的段的選擇子;------------------------------------------------------------------------------- ;以下是系統核心的頭部,用于加載核心程序 core_length dd core_end ;核心程序總長度#00sys_routine_seg dd section.sys_routine.start;系統公用例程段位置#04core_data_seg dd section.core_data.start;核心數據段位置#08core_code_seg dd section.core_code.start;核心代碼段位置#0ccore_entry dd start ;核心代碼段入口點#10dw core_code_seg_sel;=============================================================================== [bits 32]SECTION sys_routine vstart=0 ;系統公共例程代碼段 ......SECTION core_data vstart=0 ;系統核心的數據段......SECTION core_code vstart=0 ;內核代碼段 ...... start:...... ;=============================================================================== core_end:首先,用EQU聲明了一些常量,需要注意的是:EQU聲明的常量不占用空間。
其次,是內核的頭部;
最后,是公共例程代碼段、內核數據段、內核代碼段。
內核頭部示意圖如下:
注意,內核代碼段的入口共6個字節,前4個字節是段內偏移地址(它來自標號start,以后會被傳送到EIP),后2個字節是內核代碼段的選擇子(=0x38)。
當引導程序加載完內核,內核加載完用戶程序之后,內存布局示意圖如下(只是示意圖,沒有嚴格按照比例繪制)
內核的加載
1 ;代碼清單13-1 2 ;文件名:c13_mbr.asm 3 ;文件說明:硬盤主引導扇區代碼 4 ;創建日期:2011-10-28 22:35 5 6 core_base_address equ 0x00040000 ;常數,內核加載的起始內存地址 7 core_start_sector equ 0x00000001 ;常數,內核的起始邏輯扇區號 8 9 mov ax,cs 10 mov ss,ax 11 mov sp,0x7c00 12 13 ;計算GDT所在的邏輯段地址 14 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址 15 xor edx,edx 16 mov ebx,16 17 div ebx ;分解成16位邏輯地址 18 19 mov ds,eax ;令DS指向該段以進行操作 20 mov ebx,edx ;段內起始偏移地址第6、7行,作者定義了2個常量,分別是內核加載的起始物理內存地址(也不一定非要這個值,只要合理規劃就行)和內核的起始邏輯扇區號(在寫入鏡像文件的時候,要和這個扇區號對應)。
9~11行,設置實模式的棧和棧指針。
14~17,像之前的程序一樣,把GDT的物理地址分解為邏輯地址(段地址:偏移地址),于是 DS:EBX就指向了GDT的起始位置。
22 ;跳過0#號描述符的槽位 23 ;創建1#描述符,這是一個數據段,對應0~4GB的線性地址空間 24 mov dword [ebx+0x08],0x0000ffff ;基地址為0,段界限為0xFFFFF 25 mov dword [ebx+0x0c],0x00cf9200 ;粒度為4KB,存儲器段描述符 26 27 ;創建保護模式下初始代碼段描述符 28 mov dword [ebx+0x10],0x7c0001ff ;基地址為0x00007c00,界限0x1FF 29 mov dword [ebx+0x14],0x00409800 ;粒度為1個字節,代碼段描述符 30 31 ;建立保護模式下的堆棧段描述符 ;基地址為0x00007C00,界限0xFFFFE 32 mov dword [ebx+0x18],0x7c00fffe ;粒度為4KB 33 mov dword [ebx+0x1c],0x00cf9600 34 35 ;建立保護模式下的顯示緩沖區描述符 36 mov dword [ebx+0x20],0x80007fff ;基地址為0x000B8000,界限0x07FFF 37 mov dword [ebx+0x24],0x0040920b ;粒度為字節 38 39 ;初始化描述符表寄存器GDTR 40 mov word [cs: pgdt+0x7c00],39 ;描述符表的界限 41 42 lgdt [cs: pgdt+0x7c00] 43 44 in al,0x92 ;南橋芯片內的端口 45 or al,0000_0010B 46 out 0x92,al ;打開A20 47 48 cli ;中斷機制尚未工作 49 50 mov eax,cr0 51 or eax,1 52 mov cr0,eax ;設置PE位 53 54 ;以下進入保護模式... ... 55 jmp dword 0x0010:flush ;16位的描述符選擇子:32位偏移 56 ;清流水線并串行化處理器24~37行,建立描述符,下圖是GDT示意圖。
在進入保護模式之后,首先設置DS和堆棧段,然后會加載內核的第一個扇區,因為第一個扇區包含了頭部數據。
57 [bits 32] 58 flush: 59 mov eax,0x0008 ;加載數據段(0..4GB)選擇子 60 mov ds,eax 61 62 mov eax,0x0018 ;加載堆棧段選擇子 63 mov ss,eax 64 xor esp,esp ;堆棧指針 <- 0于是DS指向了0~4GB的數據段;
66 ;以下加載系統核心程序 67 mov edi,core_base_address 68 69 mov eax,core_start_sector 70 mov ebx,edi ;起始地址 71 call read_hard_disk_0 ;以下讀取程序的起始部分(一個扇區) 138 read_hard_disk_0: ;從硬盤讀取一個邏輯扇區 139 ;EAX=邏輯扇區號 140 ;DS:EBX=目標緩沖區地址 141 ;返回:EBX=EBX+512關于read_hard_disk_0這個過程代碼我就不貼了,這個過程和原書第八章的代碼類似。具體講解可以參考我的博文:硬盤和顯卡的訪問與控制(二)——《x86匯編語言:從實模式到保護模式》讀書筆記02
http://blog.csdn.net/longintchar/article/details/49454459
與第八章的那個讀硬盤的過程相比,這個過程僅有幾處不同:
1.用EAX傳入28位的邏輯扇區號。
2.DS:EBX指向目標緩沖區的地址。
3.每次返回時,EBX會自增512.
因為DS指向0-4GB的數據段,所以67~71把內核的第一個扇區加載到了物理地址core_start_sector (=0x40000)處。如下圖所示:
73 ;以下判斷整個程序有多大 74 mov eax,[edi] ;核心程序尺寸 75 xor edx,edx 76 mov ecx,512 ;512字節每扇區 77 div ecx 78 79 or edx,edx 80 jnz @1 ;未除盡,因此結果比實際扇區數少1 81 dec eax ;已經讀了一個扇區,扇區總數減1 82 @1: 83 or eax,eax ;考慮實際長度≤512個字節的情況 84 jz setup ;EAX=0 ? 85 86 ;讀取剩余的扇區 87 mov ecx,eax ;32位模式下的LOOP使用ECX 88 mov eax,core_start_sector 89 inc eax ;從下一個邏輯扇區接著讀 90 @2: 91 call read_hard_disk_0 92 inc eax 93 loop @2 ;循環讀,直到讀完整個內核 94上面這段代碼首先判斷程序的尺寸(保存在EAX中),然后做除法 EDX:EAX/512=EAX…EDX,根據商和余數讀取剩余的扇區。計算原理與第八章的“代碼清單8-1”中的代碼類似。流程圖可以參考我剛才提到的那篇博文。
需要特別提醒的是:83~84行的判斷是必要的,不然的話,當剩余扇區數(EAX)為0時,循環將會執行(0xFFFF_FFFF+1)次,哦,這真是一個重大的BUG。
加載完內核后,我們要根據頭部信息向GDT追加描述符。
95 setup: 96 mov esi,[0x7c00+pgdt+0x02] ;不可以在代碼段內尋址pgdt,但可以 97 ;通過4GB的段來訪問 98 ;建立公用例程段描述符 99 mov eax,[edi+0x04] ;公用例程代碼段起始匯編地址 100 mov ebx,[edi+0x08] ;核心數據段匯編地址 101 sub ebx,eax 102 dec ebx ;公用例程段界限 103 add eax,edi ;公用例程段基地址 104 mov ecx,0x00409800 ;字節粒度的代碼段描述符 105 call make_gdt_descriptor 106 mov [esi+0x28],eax 107 mov [esi+0x2c],edx 108 109 ;建立核心數據段描述符 110 mov eax,[edi+0x08] ;核心數據段起始匯編地址 111 mov ebx,[edi+0x0c] ;核心代碼段匯編地址 112 sub ebx,eax 113 dec ebx ;核心數據段界限 114 add eax,edi ;核心數據段基地址 115 mov ecx,0x00409200 ;字節粒度的數據段描述符 116 call make_gdt_descriptor 117 mov [esi+0x30],eax 118 mov [esi+0x34],edx 119 120 ;建立核心代碼段描述符 121 mov eax,[edi+0x0c] ;核心代碼段起始匯編地址 122 mov ebx,[edi+0x00] ;程序總長度 123 sub ebx,eax 124 dec ebx ;核心代碼段界限 125 add eax,edi ;核心代碼段基地址 126 mov ecx,0x00409800 ;字節粒度的代碼段描述符 127 call make_gdt_descriptor 128 mov [esi+0x38],eax 129 mov [esi+0x3c],edx 130 131 mov word [0x7c00+pgdt],63 ;描述符表的界限 132 133 lgdt [0x7c00+pgdt]此時,整個的GDT示意圖如下:
第98~107,是添加公共例程段描述符的具體代碼。
98 ;建立公用例程段描述符 99 mov eax,[edi+0x04] ;公用例程代碼段起始匯編地址 100 mov ebx,[edi+0x08] ;核心數據段匯編地址 101 sub ebx,eax 102 dec ebx ;公用例程段界限 103 add eax,edi ;公用例程段基地址 104 mov ecx,0x00409800 ;字節粒度的代碼段描述符 105 call make_gdt_descriptor 106 mov [esi+0x28],eax 107 mov [esi+0x2c],edx第105行,調用了過程 call make_gdt_descriptor
195 make_gdt_descriptor: ;構造描述符 196 ;輸入:EAX=線性基地址 197 ; EBX=段界限 198 ; ECX=屬性(各屬性位都在原始 199 ; 位置,其它沒用到的位置清0) 200 ;返回:EDX:EAX=完整的描述符 201 mov edx,eax 202 shl eax,16 203 or ax,bx ;描述符前32位(EAX)構造完畢 204 205 and edx,0xffff0000 ;清除基地址中無關的位 206 rol edx,8 207 bswap edx ;裝配基址的31~24和23~16 (80486+) 208 209 xor bx,bx 210 or edx,ebx ;裝配段界限的高4位 211 212 or edx,ecx ;裝配屬性 213 214 ret根據注釋,這個過程的輸入和返回都已經很清楚了,這個過程的功能是通過“段基地址(EAX),段限長(EBX),屬性值(ECX)”這三個參數來構造一個描述符(EDX:EAX)。下面就具體講解這個過程。
我們先復習一下段描述符的通用格式(圖片選自趙炯的《Linux內核完全注釋》)。
首先構造描述符的低32位(圖片中下面的那個東東)。
201行,先備份一個EAX到EDX中,留在后面用。
202行,EAX左移16位,于是基地址的0-15位就位;
203行,段限長的0-15位就位;
于是描述符的低32位構造完畢。
接下來,構造描述符的高32位(圖片中上面那個東東)。這個構造起來有點麻煩。
我們先學習一個指令——字節交換指令:bswap
在標準的32位處理器上,這個指令只允許32位的寄存器操作數,其格式為
處理器執行該指令時,按如下過程操作(DEST是指令中的操作數,TEMP是處理器內的一個臨時寄存器)
是不是有些暈呢?沒有關系,我繪制了一張圖,這張圖的特色是“漸變色”,很清楚地說明了字節是如何交換的。
看清楚了吧。
OK,我們繼續。
以上代碼是具體的構造過程,引用原書圖13-6。
205~207三行執行完后,段基地址已經就位。
209行,清除段界限的15-0位,只保留19-16位。這里假設EBX寄存器的高12位全為0,其實安全的做法是把209行修改為
210行,裝配段界限到EDX寄存器。
212行,裝配屬性值到EDX寄存器。
至此,在EDX:EAX中得到了完整的64位的段描述符。
好了,現在再回到98~107行。代碼再貼一次。
98 ;建立公用例程段描述符 99 mov eax,[edi+0x04] ;公用例程代碼段起始匯編地址 100 mov ebx,[edi+0x08] ;核心數據段匯編地址 101 sub ebx,eax 102 dec ebx ;公用例程段界限 103 add eax,edi ;公用例程段基地址 104 mov ecx,0x00409800 ;字節粒度的代碼段描述符 105 call make_gdt_descriptor 106 mov [esi+0x28],eax 107 mov [esi+0x2c],edx此時,DS:EDI仍然指向內核的起始位置。根據內核頭部的構造,我們從頭部取出公共例程代碼段的起始匯編地址到EAX,再取出內核數據段的起始匯編地址到EBX,后者減去前者,就是公共例程段的長度,再減去一,就是段界限,EBX這個參數就準備好了。
然后準備參數EAX(段基址):因為公共例程段的起始匯編地址(已經傳送到EAX了)是相對于內核的起始位置的,加載內核后,內核的起始位置在線性地址0x40000(就是EDI的值)處,所以,公共例程段的起始地址是(EAX+EDI),這就是103行的用意。
104行,填寫屬性值,注意要把無關的位都清零。
105行調用過程。
106~107利用返回值安裝描述符。
其他描述符的構造和安裝過程類似,這里從略。
跳轉到內核入口點
最后一步,跳到內核的入口點開始執行內核程序。
135 jmp far [edi+0x10]這是一個16位直接絕對遠轉移,在ds:[edi+0x10]處,是6字節的內核入口點。低32位是偏移地址,高16位是內核代碼段的選擇子。關于jmp指令,可以參考我的博文:
8086處理器的無條件轉移指令——《x86匯編語言:從實模式到保護模式》讀書筆記13
http://blog.csdn.net/longintchar/article/details/50529164
總結
啰嗦了這么多,不知道你是不是覺得什么都沒有記住呢…
我們來總結一下引導程序的引導步驟吧。
1. 創建GDT
2. 令DS指向0-4GB數據段;初始化SS和ESP;
3. 調用read_hard_disk_0讀取內核的第一個扇區到0x40000;
4. 判斷內核的長度,根據長度再讀取若干個扇區
5. ESI指向GDT的起始,調用過程make_gdt_descriptor向GDT追加關于內核的段描述符
6. 跳轉到內核入口點
關于內核的執行,下篇博文我們再討論。敬請期待……
總結
以上是生活随笔為你收集整理的程序的加载和执行(一)——《x86汇编语言:从实模式到保护模式》读书笔记21的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: java 内部接口 内部类_Java的接
- 下一篇: 产品经理是个实战类科目