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