分頁是現在CPU核心的管理內存方式,網上介紹材料很多,這里不贅述,簡單介紹一下分頁的背景和原理
1、先說說為什么要分段
- 實模式下程序之間不隔離,互相能直接讀寫對方內存,或跳轉到其他進程的代碼運行,導致泄密、出錯,通過分段隔離不同程序代碼對不同內存單元的讀寫權限;
- 用戶程序在內存種加載的地址不確定,通過分段對程序的數據、代碼重定位,才能在運行時正確尋址(如果沒有特殊聲明,編譯器編譯后生成文件的代碼和數據都是相對文件頭開始計算偏移的)
2、再說說為什么要分頁?
物理內存是有限的,主流普通PC機內存也就8G~16G,除了運行os,還要盡可能多地運行用戶程序。但現代大型的用戶程序動則大幾百M,甚至幾個G,要想“同時”把這么多的用戶程序加載到內存運行該怎么辦了?
- CPU的分頁機制把物理內存分割成4K大小的空間,稱為“頁”。
- 32位的windows操作系統針對每個進程,虛擬出了4GB的進程空間。對於進程來說,低2G的空間隨便用,無需任何顧忌。那么問題又來了,不同進程很有可能用到了同樣的地址,怎么防止沖突?
- os會根據實際情況,把虛擬地址”掛載“到適合的物理頁。對於不同的進程,代碼種即使用了同樣的地址,os也會掛載到不同的物理內存,這些對於進程來說都是透明不可見的,也不需要關心;
- 內存的空間是有限的,為了盡量多”並發“運行進程,os會酌情把物理頁的數據存儲到磁盤的pagefile.sys文件。當進程執行需要用到時,發現物理內存沒有,此時產生缺頁異常,os負責從磁盤取回這些數據放回內存,讓進程繼續執行;
- 分頁可以讓段基址和limit變平坦(64位已經這樣了),段僅用來鑒權,或在32位和64位之間來回切換(利用這個特性可以讓64位的os兼容32位的應用程序,也可以將32位程序的某些重要數據,比如key、密鑰、密碼之類的放在64位模式下,達到在3環下反調試、反逆向的目的,詳細的過程見這里:https://www.bilibili.com/video/BV1SJ411K7LR)
- windwos會對頁賦予各種屬性,比如可執行,可讀寫。可人為將頁屬性更改,比如代碼所在的頁改為不可執行、不可讀,進程運行到這種頁時產生缺頁異常。此時如果hook pagefault函數,根據異常原因分別處理:如果是執行,那么把頁屬性改成可執行,替換成自己想要執行的代碼;如果是讀取異常,那么給該線性地址掛載原物理頁。這種hook能達到隱藏鈎子的目的,能在VT下過PG保護,這就是著名的shaodw walker,詳細過程可以參考這里:https://www.bilibili.com/video/BV1Hb411n7Mw
3、核心代碼解讀
(1)准備PDT
- 頁目錄物理地址0x20000開始,后續會把這個地址賦值給CR3;
- PDE也是32位=4字節,那么PDE大小=1024*4=4096字節,剛好是一個頁,那么PDT結尾就是0x20000+0x1000=0x21000;
;創建系統內核的頁目錄表PDT ;頁目錄表清零 mov ecx,1024 ;1024個目錄項PDE mov ebx,0x00020000 ;頁目錄的物理地址 xor esi,esi .b1: mov dword [es:ebx+esi],0x00000000 ;頁目錄表項清零 add esi,4 loop .b1 ;在頁目錄內創建指向頁目錄自己的目錄項,最后一項指向自己,那么線性地址高20位是0xFFFFF的時候,轉成物理地址就是頁目錄自己 mov dword [es:ebx+4092],0x00020003 ;在頁目錄內創建與線性地址0x00000000對應的目錄項 mov dword [es:ebx+0],0x00021003 ;寫入目錄項(頁表的物理地址和屬性)
- 以上代碼執行完畢后,內存圖如下:分別在PDT的首位寫入兩個地址,其他的都清零,那么問題來了,為啥要分別寫這兩個數,而不是其他的數?

- 先解釋一下PDT的第一項為什么會是0x00021003
一旦開啟分頁,所有地址都會被認為是線性地址,都會經過轉換才能獲取物理地址,這是CPU的硬件機制決定的,操作系統都要遵守,無法例外。既然0x20000~0x21000這段地址已經被用於存放PDT,那么就不應該再被寫入,避免PDT被破壞,導致線性地址映射到物理地址出錯,所以物理地址必須從0x21000開始;這里把0x21000開始的地方用來存放頁表;
- 再解釋一下最后一個PDE為什么是0x20003
由於業務變化多端,無法在開啟分頁前全部確定最終地址,導致很多PDT要開啟分頁后再填;那么問題又來了,一旦開啟分頁,任何線性地址都要轉換才能得到物理地址,PDT也不例外,怎么讓線性地址轉換后落入0x20000~0x21000這個物理區間了?
來分析一種特殊的地址,前20位都是1,比如0xFFFFF200. 按照10-10-12拆分,3個偏移分別0x3ff, 0x3ff乘以4后分別是 0xffc,0xffc;
第一次轉換:0x20000+0xffc=0x20ffc,得到0x20003;后3byte是屬性,基址就是0x20000;
第二次轉換:0x20000+0xffc=0x20ffc,得到0x20003;后3byte是屬性,基址還是0x20000;
最后一次轉換:0x20000 + 0x200= 0x20200,地址還是落在0x20000~0x21000區間;所以結論就是:線性地址前20位都是1,轉成物理地址會落在PDT內部,線性地址最后12位就是PDT內的偏移;通過一些巧妙的數字設置,這里把頁目錄當成頁表在用了;
最后12位是屬性位:
(2)正式開始分頁前最后的准備工作:初始化PET頁表,讓其映射最低端0~1MB的物理地址;實模式下低端1MB物理地址都有用了,所以必須先把這部分地址映射,防止分頁開啟后找不到;下面有第(3)點有PDT和PET的內存表,方便理解
;創建與上面那個目錄項相對應的頁表,初始化頁表項 mov ebx,0x00021000 ;頁表的物理地址 xor eax,eax ;起始頁的物理地址 xor esi,esi ;esi=0 .b2: mov edx,eax ;edx=eax; eax = 0x1000*n or edx,0x00000003 ;edx=0x1000*n+3;u/s=1,不允許3環程序訪問;P=1,頁在內存種;RW=1,頁可讀可寫; mov [es:ebx+esi*4],edx ;登記頁的物理地址; 0x21000~0x21400都是PTE,隱射從0~1MB(256*4096=1Mb)的物理地址; add eax,0x1000 ;下一個相鄰頁的物理地址 inc esi cmp esi,256 ;僅低端1MB內存對應的頁才是有效的 jl .b2 .b3: ;其余的頁表項置為無效 mov dword [es:ebx+esi*4],0x00000000 ;0x21400~(0x21400+(1024-256)*4=0x22000)清零; inc esi cmp esi,1024 jl .b3
(3)這里 es:ebx+esi = 0xFFFFF800, 開啟分頁機制后,會映射到0x20800,同樣也賦值0x21003,指向頁表第一個位置;
;在頁目錄內創建與線性地址0x80000000對應的目錄項 mov ebx,0xfffff000 ;頁目錄自己的線性地址;高5字節都是F,低3字節就是PDT內的偏移 mov esi,0x80000000 ;映射的起始地址 shr esi,22 ;取線性地址高10位(目錄索引),esi=0x200 shl esi,2 ;索引乘以4得到偏移 mov dword [es:ebx+esi],0x00021003 ;寫入目錄項(頁表的物理地址和屬性)es:ebx+esi = 0xFFFFF800
雖說這兩個PDE都指向同一個頁表,但各自的線性地址確不同:第一個線性地址范圍0x00000000~0x000FFFFF(PDT的索引是0), 第二個線性地址的范圍是0x80000000~0x800FFFFF(PDT的索引是800);為什么要讓兩個不同的線性地址段指向同一個PTE,進而共享同一塊物理內存了? 站在應用開發角度,已經習慣了將0x80000000作為內核地址,並且各個用戶程序共享。但此時GDT已加載到0x0~0xFFFFF的低1MB空間,后續內核代碼、內核數據段、API也會加載到這1MB空間,為了兼容現有的用戶習慣,需要將0x80000000也映射到這里的物理地址;所以這里的結論:線性地址0x80000000~0x800FFFFF映射的物理地址:0x00000~0xFFFFF;

物理地址內容如下,這里設計就很巧妙了:
- 比如未分頁的時候物理地址0x00007e10, or 0x80000000后變成0x80007e10,經過下面PDE和PTE的轉換,線性地址0x80007e10又變回了物理地址0x00007e10,分頁開啟在在物理地址保存的各個GDT or 0x80000000 就行,其他沒任何影響,照常使用;
- 原0x00000000~0x000FFFFF 低1MB的物理空間,分頁開啟后轉成的物理地址沒變。比如0x00007e10,當成線性地址轉換成物理地址后還是0x00007e10;
- 巧妙之處:(1)高10位是0x000或0x800的線性地址,在PDT表中查找到0x00021003,這是PTE的起始地址; (2)中間10位是PTD的偏移,每個偏移都乘以0x1000,比如上面的0x007,得到0x7000;(3)最后3字節是頁內偏移,所以得到的結果還是以前的物理地址0x00007e10;

(4)此時已開啟了分頁模式,所有地址都會被認為是線性地址,為了正常找到在實模式下已經存好的描述符,這里對每個描述符最高位置1,原因上面已經解釋過:這么做能讓新的線性地址經過PDE和PTE的轉換后還能變回以前的物理地址,比如線性地址0x80007e10又變回了物理地址0x00007e10;
這里把內核各個核心段的描述符最高位都置1,構建內核區域的線性地址:
;將GDT中的段描述符映射到線性地址0x80000000 sgdt [pgdt] mov ebx,[pgdt+2] ;ebx存放GDT的base or dword [es:ebx+0x10+4],0x80000000 ; or dword [es:ebx+0x18+4],0x80000000 ;內核堆棧段 or dword [es:ebx+0x20+4],0x80000000 ;視頻顯示緩沖區 or dword [es:ebx+0x28+4],0x80000000 ;API段 or dword [es:ebx+0x30+4],0x80000000 ;內核數據段 or dword [es:ebx+0x38+4],0x80000000 ;內核代碼段 add dword [pgdt+2],0x80000000 ;GDTR也用的是線性地址 lgdt [pgdt]
此刻問題又來了:這個時候不是已經開啟分頁了么?es:ebx+0x18+4 = 0x7e00+0x18+0x4= 0x7e1c,這個地址會被當成線性地址看待;如果按照10-10-12分頁,0x7e1c轉成物理地址后還是0x7e1c,描述符的最高位成功置1; 更改后的描述符 0x80cf9600`0x7c00fffe ,段基址0x80007c00,轉成物理地址后還是0x7c00;

(5)API段一共提供了4個函數,在內核數據段對這4個函數都有登記,每個函數的格式:函數名(不超過256字節,不夠的填0補充)、API段內偏移、API段選擇子,這個類似於導出表;這里構造每個API函數調用們(權限控制在3環的程序訪問),然后將selector寫回原選擇子處;
其實在API(原作者稱為sys_routine段),出了這4個,還有其他函數,比如make_gate_descriptor、set_up_gdt_descriptor、alloc_inst_a_page等,只不過這兩個函數並未在導出表列舉,一般情況下用戶程序是不知道其地址的;同時也是內核0環權限,普通3環程序也無權訪問,但還是有辦法調用,比如在windows下,做逆向時需要調用很多內核未導出函數,在驅動中完全可以根據特征碼查找這些函數的偏移地址,然后call調用,詳細可參考之前的文章:https://www.cnblogs.com/theseventhson/p/13024325.html
;以下開始安裝為整個系統服務的調用門。特權級之間的控制轉移必須使用門 mov edi,salt ;C-SALT表的起始位置,內核API函數導出表,有函數名稱、函數在API段內的偏移、API段的選擇子 mov ecx,salt_items ;C-SALT表的條目數量,ecx=4 .b4: push ecx mov eax,[edi+256] ;該條目入口點的32位偏移地址;API函數的段內偏移地址 mov bx,[edi+260] ;該條目入口點的段選擇子 ;API函數所在段的選擇子 mov cx,1_11_0_1100_000_00000B ;特權級3的調用門(3以上的特權級才 ;允許訪問),0個參數(因為用寄存器 ;傳遞參數,而沒有用棧) call sys_routine_seg_sel:make_gate_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [edi+260],cx ;將返回的門描述符選擇子回填 add edi,salt_item_len ;指向下一個C-SALT條目 pop ecx loop .b4
(6)分配物理頁:為了簡單,這里只使用2M內存,可用512個頁;512個頁用512位保存狀態,0表示空閑,1表示使用,這512位存放在page_bit_map中;分配內存時先逐個遍歷,是0的話就占用;同時把索引號乘以0x1000就是物理地址了;
allocate_a_4k_page: ;分配一個4KB的頁 ;輸入:無 ;輸出:EAX=頁的物理地址 push ebx push ecx push edx push ds mov eax,core_data_seg_sel mov ds,eax xor eax,eax .b1: ;遍歷page_bit_map,找到第一個標識是0的位,說明該頁還未使用 bts [page_bit_map],eax ;[page_bit_map]第eax的位復制給CF,同時置1 jnc .b2 ;CF=0,說明找到了空閑的物理頁;物理頁索引存放在eax inc eax ;沒有找到,eax+1繼續找 cmp eax,page_map_len*8 ;遍歷到page_bit_map末尾了嗎? jl .b1 ;沒有就從頭繼續找 mov ebx,message_3 call sys_routine_seg_sel:put_string hlt ;沒有可以分配的頁,停機 .b2: shl eax,12 ;eax存放了空閑的物理頁索引,乘以4096(0x1000)就是地址 pop ds pop edx pop ecx pop ebx ret
(7)給指定的線性地址掛載物理頁
- 線性地址也要求0x1000對齊
- 這里構造新的線性地址:(1)原線性地址高10位放在新地址中間13~22位;原線性地址中間10位(13~22)放新地址低3~12位;新地址高10位置1,這樣一來,原地址高10位會作為頁目錄表的偏移,原地址中間10位作為頁表內偏移;mov [esi],eax 會把找好的物理頁地址放入合適的頁表項,最終完成線性地址到物理地址的映射;
alloc_inst_a_page: ;給指定的線性地址掛載物理頁 ;層級分頁結構中 ;輸入:EBX=頁的線性地址,比如0x80104000 push eax push ebx push esi push ds mov eax,mem_0_4_gb_seg_sel mov ds,eax ;檢查該線性地址所對應的頁表是否存在;把ebx高10位作為PDT的索引查找PTE; mov esi,ebx ;esi=0x80104000 and esi,0xffc00000 ;只保留最高的10位,低22位清零,得到PDT的索引,esi=0x80000000 shr esi,20 ;高12位移到低12位:得到頁目錄索引,並乘以4,得到PTE在PDE內的偏移地址;esi=0x00000800 or esi,0xfffff000 ;頁目錄自身的線性地址+表內偏移;最高20位置1的線性地址,轉換成物理地址=PDT基址(這里是0x20000)+esi,相當於最低3字節就是PDT內的偏移,高20位置1確保物理地址還是落在PDT內;esi=0xfffff800 test dword [esi],0x00000001 ;P位是否為“1”.如果PDT某項有PTE,結尾不會是0;如果是0,說明還未掛載物理頁;[esi]=0x00000003,最后4位是0011; jnz .b1 ;否已經有對應的頁表 ;創建該線性地址所對應的頁表 call allocate_a_4k_page ;分配一個頁做為頁表 or eax,0x00000007 ;該頁的屬性:U/S=1,允許3環訪問;RW=1,可讀可寫;P=1,表明有物理頁了 mov [esi],eax ;在頁目錄中登記該物理地址 .b1: ;不論是否執行JNZ .b1,代碼最終會走到這里來 ;開始訪問該線性地址所對應的頁表 mov esi,ebx ;esi=0x80104000 shr esi,10 ;高22位移到低22位,esi=0x00200410 and esi,0x003ff000 ;只保留原線性地址高10位,也就是PDT的偏移;esi=0x00200000 or esi,0xffc00000 ;原線性地址最高10位保存在esi的中間10位,即11-20位;高10位置1,這樣在PDT內查的時候能得到0x21003,也就是頁表的基址; ;得到該線性地址在頁表內的對應條目(頁表項) and ebx,0x003ff000 ;ebx=0x00104000,保留原線性地址中間10位 shr ebx,10 ;相當於右移12位,再乘以4;原線性地址中間10位右移到低2~11位,得到頁表內的偏移;ebx=0x410 or esi,ebx ;頁表項的線性地址;原線性地址的高10位、中間10位依次右移,現在是從2~20位,高11位置1;原線性地址高10位用來作為頁表的偏移,中間10位用來做頁表的偏移; esi=0xFFF00410 call allocate_a_4k_page ;分配一個頁,這才是要安裝的頁 or eax,0x00000007 mov [esi],eax pop ds pop esi pop ebx pop eax retf
第一次傳入的線性地址是0x80101000,還查不到對應的物理頁:

執行完mov [esi],eax后,0x8010100的線性地址被映射到了0x2b000的物理地址:

(8) 在當前PDT,ebx低3字節就是頁目錄內的偏移;把底2G的頁目錄清空,根據實際情況填上用戶程序的頁目錄,再復制到其他地方,這樣不用切換CR3(一旦切換,需要新的頁目錄和頁表,但還未建設好了,CPU會拋異常的),可以利用現有的地址轉換體系;后續每創建新任務,這部分的頁目錄表都要清零;從0x20800開始的頁目錄都是映射0x80000000的線性地址,這部分屬於各個任務共享的內核;
;清空當前頁目錄的前半部分(對應低2GB的局部地址空間) mov ebx,0xfffff000 xor esi,esi .b1: mov dword [es:ebx+esi*4],0x00000000 inc esi cmp esi,512 jl .b1
運行完后,內存變成這樣:

(9)所謂 “每個用戶程序都擁有4GB的虛擬空間” ,核心原理體現在這里了: 每個用戶程序都單獨定制一個頁目錄表和頁表。每個用戶程序頁目錄表的第1項到512項都映射自己的物理地址,盡管不同用戶程序同樣用低2G的線性地址,但映射的物理地址卻可以不同;
mov [0xfffffff8],ebx: 這里把存放用戶程序頁目錄表的物理地址放在內核地址頁目錄表的倒數第二項;如果有第二個用戶程序,可以放在倒數第三項,即mov [0xfffffff4],ebx 以此類推;
create_copy_cur_pdir: ;創建新頁目錄,並復制當前頁目錄內容 ;輸入:無 ;輸出:EAX=新頁目錄的物理地址 push ds push es push esi push edi push ebx push ecx mov ebx,mem_0_4_gb_seg_sel mov ds,ebx mov es,ebx call allocate_a_4k_page mov ebx,eax or ebx,0x00000007 ;用戶程序的頁目錄和頁表,當然是3環能訪問的,所以U/S=1;RW=1可讀可寫;P=1表明已經有物理頁 mov [0xfffffff8],ebx ;頁目錄表倒數第二項(最后一項已經是0x20003了) mov esi,0xfffff000 ;ESI->當前頁目錄的線性地址 mov edi,0xffffe000 ;EDI->新頁目錄的線性地址,剛好指向頁目錄表的倒數第二項,存放了剛才申請的物理地址 mov ecx,1024 ;ECX=要復制的目錄項數 cld repe movsd pop ecx pop ebx pop edi pop esi pop es pop ds retf
(10) API段的描述符和選擇子都重置並寫回,3環的用戶程序才能調用
push edi push esi push ecx mov ecx,64 ;檢索表中,每條目的比較次數 repe cmpsd ;每次比較4字節 jnz .b6 mov eax,[esi] ;若匹配,則esi恰好指向其后的地址 mov [es:edi-256],eax ;將字符串改寫成偏移地址 mov ax,[esi+4] or ax,0000000000000011B ;以用戶程序自己的特權級使用調用門 ;故RPL=3 mov [es:edi-252],ax ;回填調用門選擇子
(11)把用戶程序導入表需要的函數和內核API段的函數根據名稱一一對比,發現名稱一樣的說明匹配上了,把這些內核API的物理地址、選擇子等回填到用戶程序的導入表,當用戶程序調用API時,才能跳轉到正確的地方執行:
;重定位SALT mov eax,mem_0_4_gb_seg_sel ;訪問任務的4GB虛擬地址空間時用 mov es,eax mov eax,core_data_seg_sel mov ds,eax cld mov ecx,[es:0x0c] ;U-SALT條目數;位於用戶程序程序0x0C處 mov edi,[es:0x08] ;U-SALT在4GB空間內的偏移;位於用戶程序0x08偏移處 .b4: push ecx push edi mov ecx,salt_items mov esi,salt .b5: push edi push esi push ecx mov ecx,64 ;檢索表中,每條目的比較次數 repe cmpsd ;每次比較4字節 jnz .b6 mov eax,[esi] ;esi是內核API地址 mov [es:edi-256],eax ;edi是用戶程序導入表的API地址,這里把內核API地址寫入用戶程序導入表,用戶程序調用時直接跳轉到內核API處執行 mov ax,[esi+4] ; or ax,0000000000000011B ;以用戶程序自己的特權級使用調用門 ;故RPL=3 mov [es:edi-252],ax ;回填調用門選擇子到用戶程序的導入表
把內核API的偏移和選擇子回填到用戶程序導入表關鍵代碼:

其他代碼都是利用TSS、TR、任務門切換任務相關的。在32位下,利用TSS切換任務效率較低,需要數百個時鍾周期,所以windwos和linux並未采用該方式;64位下連intel自己都廢棄這種方式,感興趣的讀者可自行分析剩余代碼;
4、分頁機制要點
- 為了最大程度利用內存,物理頁都是挨着連續分配的,第一個頁0x00001000,第二個頁0x00002000,直到最后一個頁0xFFFFF000;不難發現物理頁地址必須以000結尾(或則說除以0x1000余數為0);
- 一旦分頁開啟,所有地址都會被CPU當成線性地址處理,需要先轉成物理地址,這是硬件機制決定的,os也不例外,所以最初構造頁目錄表的時候有一定的技巧,比如頁目錄表最后一項指向開始,中間0x20800也指向頁表第一基址、低512個頁目錄給用戶程序使用、每個用戶程序各自賦值一份頁目錄表和頁表;
- 有了分頁,分段就不再那么重要了(64位windows段都平坦了)。通過對頁目錄表和頁表的控制,同樣可以達到控制程序對物理內存的使用;
5、為方便理解,這里梳理了一下核心的步驟和流程:

MBR引導代碼
core_base_address equ 0x00040000 ;常數,內核加載的起始內存地址 core_start_sector equ 0x00000001 ;常數,內核的起始邏輯扇區號 mov ax,cs mov ss,ax mov sp,0x7c00 ;計算GDT所在的邏輯段地址 mov eax,[cs:pgdt+0x7c00+0x02] ;GDT的32位物理地址 xor edx,edx mov ebx,16 div ebx ;分解成16位邏輯地址 mov ds,eax ;令DS指向該段以進行操作;ds=0x7e0 mov ebx,edx ;段內起始偏移地址,ebx =0x00 ;跳過0#號描述符的槽位 ;創建1#描述符,這是一個數據段,對應0~4GB的線性地址空間 mov dword [ebx+0x08],0x0000ffff ;基地址為0,段界限為0xFFFFF mov dword [ebx+0x0c],0x00cf9200 ;粒度為4KB,存儲器段描述符 ;創建保護模式下初始代碼段描述符 mov dword [ebx+0x10],0x7c0001ff ;基地址為0x00007c00,界限0x1FF mov dword [ebx+0x14],0x00409800 ;粒度為1個字節,代碼段描述符 ;建立保護模式下的堆棧段描述符 ;基地址為0x00007C00,界限0xFFFFE mov dword [ebx+0x18],0x7c00fffe ;粒度為4KB mov dword [ebx+0x1c],0x00cf9600 ;建立保護模式下的顯示緩沖區描述符 mov dword [ebx+0x20],0x80007fff ;基地址為0x000B8000,界限0x07FFF mov dword [ebx+0x24],0x0040920b ;粒度為字節 ;初始化描述符表寄存器GDTR mov word [cs: pgdt+0x7c00],39 ;描述符表的界限 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)選擇子 mov ds,eax mov eax,0x0018 ;加載堆棧段選擇子 mov ss,eax xor esp,esp ;堆棧指針 <- 0 ;以下加載系統核心程序 mov edi,core_base_address mov eax,core_start_sector mov ebx,edi ;起始地址 call read_hard_disk_0 ;以下讀取程序的起始部分(一個扇區) ;以下判斷整個程序有多大 mov eax,[edi] ;核心程序尺寸 xor edx,edx mov ecx,512 ;512字節每扇區 div ecx or edx,edx jnz @1 ;未除盡,因此結果比實際扇區數少1 dec eax ;已經讀了一個扇區,扇區總數減1 @1: or eax,eax ;考慮實際長度≤512個字節的情況 jz setup ;EAX=0 ? ;讀取剩余的扇區 mov ecx,eax ;32位模式下的LOOP使用ECX mov eax,core_start_sector inc eax ;從下一個邏輯扇區接着讀 @2: call read_hard_disk_0 inc eax loop @2 ;循環讀,直到讀完整個內核 setup: ;系統各個段在0x00040000內存中重定位 mov esi,[0x7c00+pgdt+0x02] ;不可以在代碼段內尋址pgdt,但可以 ;通過4GB的段來訪問, esi=0x7e00 ;建立公用例程段描述符 mov eax,[edi+0x04] ;公用例程sys_routine代碼段起始匯編地址=0x18;edi=0x00040000 mov ebx,[edi+0x08] ;核心數據段core_data匯編地址=0x01e4 sub ebx,eax ;core_data緊跟着sys_routine,core_data-sys_routine得到sys_routine長度 dec ebx ;core_data的前面,也就是公用例程段sys_routine界限 add eax,edi ;公用例程段基地址:sys_routine=0x18,加上0x00040000得到sys_routine在內存的地址; mov ecx,0x00409800 ;字節粒度的代碼段描述符 call make_gdt_descriptor mov [esi+0x28],eax ;描述符低32位eax=0x001801cb,存入0x7e00+0x28處 mov [esi+0x2c],edx ;描述符高32位edx=0x00409804,存入0x7e00+0x2c處 ;00409804`001801cb: 段基址00040018,limit=0x01cb;4:G=0,D/B=1,L=0,AVL=0;9:p=1,DPL=00,s=1;TYPE=8是代碼段; ;在0x7e00處原描述符的末尾追加新描述符,原有描述符不變 ;建立核心數據段描述符 mov eax,[edi+0x08] ;核心數據段起始匯編地址 mov ebx,[edi+0x0c] ;核心代碼段匯編地址 sub ebx,eax dec ebx ;核心數據段界限 add eax,edi ;核心數據段基地址 mov ecx,0x00409200 ;字節粒度的數據段描述符 call make_gdt_descriptor mov [esi+0x30],eax mov [esi+0x34],edx ;建立核心代碼段描述符 mov eax,[edi+0x0c] ;核心代碼段core_code起始匯編地址 mov ebx,[edi+0x00] ;程序總長度 sub ebx,eax dec ebx ;核心代碼段界限 add eax,edi ;核心代碼段基地址 mov ecx,0x00409800 ;字節粒度的代碼段描述符 call make_gdt_descriptor mov [esi+0x38],eax mov [esi+0x3c],edx mov word [0x7c00+pgdt],63 ;描述符表的界限; 0x3f,0x7e00:高4byte是GDT基址,低2byte是limit lgdt [0x7c00+pgdt] ;保護模式新增3個段,分別對應內核3個段 jmp far [edi+0x10] ;edi=0x00040000,edi+0x10=core_code ;------------------------------------------------------------------------------- read_hard_disk_0: ;從硬盤讀取一個邏輯扇區 ;EAX=邏輯扇區號 ;DS:EBX=目標緩沖區地址 ;返回:EBX=EBX+512 push eax push ecx push edx push eax mov dx,0x1f2 mov al,1 out dx,al ;讀取的扇區數 inc dx ;0x1f3 pop eax out dx,al ;LBA地址7~0 inc dx ;0x1f4 mov cl,8 shr eax,cl out dx,al ;LBA地址15~8 inc dx ;0x1f5 shr eax,cl out dx,al ;LBA地址23~16 inc dx ;0x1f6 shr eax,cl or al,0xe0 ;第一硬盤 LBA地址27~24 out dx,al inc dx ;0x1f7 mov al,0x20 ;讀命令 out dx,al .waits: in al,dx and al,0x88 cmp al,0x08 jnz .waits ;不忙,且硬盤已准備好數據傳輸 mov ecx,256 ;總共要讀取的字數 mov dx,0x1f0 .readw: in ax,dx mov [ebx],ax add ebx,2 loop .readw pop edx pop ecx pop eax ret ;------------------------------------------------------------------------------- make_gdt_descriptor: ;構造描述符 ;輸入:EAX=線性基地址,比如sys_routine=0x00040018; ; EBX=段界限,比如sys_routine=0x1e4-0x18-1=0x1cb ; ECX=屬性(各屬性位都在原始 比如sys_routine=0x00409800 ; 位置,其它沒用到的位置0) ;返回:EDX:EAX=完整的描述符 mov edx,eax shl eax,16 ;eax從0x00040018變為0x00180000; or ax,bx ;描述符前32位(EAX)構造完畢,eax=0x001801cb; and edx,0xffff0000 ;清除基地址中無關的位 edx=0x00040000 rol edx,8 ;edx = 0x04000000 bswap edx ;裝配基址的31~24和23~16 (80486+); edx = 0x00000004; 31-24於0-7交換,23-16與8-15交換 xor bx,bx ;ebx=0x00000000 or edx,ebx ;裝配段界限的高4位,edx=0x00000004 or edx,ecx ;裝配屬性 edx=0x00409804 ret ;------------------------------------------------------------------------------- pgdt dw 0 dd 0x00007e00 ;GDT的物理地址 ;------------------------------------------------------------------------------- times 510-($-$$) db 0 db 0x55,0xaa
內核代碼:
;以下常量定義部分。內核的大部分內容都應當固定 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 ;核心程序總長度#00 sys_routine_seg dd section.sys_routine.start ;系統公用例程段位置#04 core_data_seg dd section.core_data.start ;核心數據段位置#08 core_code_seg dd section.core_code.start ;核心代碼段位置#0c core_entry dd start ;核心代碼段入口點#10 dw core_code_seg_sel ;=============================================================================== [bits 32] ;=============================================================================== SECTION sys_routine vstart=0 ;系統公共例程代碼段 ;------------------------------------------------------------------------------- ;字符串顯示例程 put_string: ;顯示0終止的字符串並移動光標 ;輸入:DS:EBX=串地址 push ecx .getc: mov cl,[ebx] or cl,cl jz .exit call put_char inc ebx jmp .getc .exit: pop ecx retf ;段間返回 ;------------------------------------------------------------------------------- put_char: ;在當前光標處顯示一個字符,並推進 ;光標。僅用於段內調用 ;輸入:CL=字符ASCII碼 pushad ;以下取當前光標位置 mov dx,0x3d4 mov al,0x0e out dx,al inc dx ;0x3d5 in al,dx ;高字 mov ah,al dec dx ;0x3d4 mov al,0x0f out dx,al inc dx ;0x3d5 in al,dx ;低字 mov bx,ax ;BX=代表光標位置的16位數 cmp cl,0x0d ;回車符? jnz .put_0a mov ax,bx mov bl,80 div bl mul bl mov bx,ax jmp .set_cursor .put_0a: cmp cl,0x0a ;換行符? jnz .put_other add bx,80 jmp .roll_screen .put_other: ;正常顯示字符 push es mov eax,video_ram_seg_sel ;0x800b8000段的選擇子 mov es,eax shl bx,1 mov [es:bx],cl pop es ;以下將光標位置推進一個字符 shr bx,1 inc bx .roll_screen: cmp bx,2000 ;光標超出屏幕?滾屏 jl .set_cursor push ds push es mov eax,video_ram_seg_sel mov ds,eax mov es,eax cld mov esi,0xa0 ;小心!32位模式下movsb/w/d mov edi,0x00 ;使用的是esi/edi/ecx mov ecx,1920 rep movsd mov bx,3840 ;清除屏幕最底一行 mov ecx,80 ;32位程序應該使用ECX .cls: mov word[es:bx],0x0720 add bx,2 loop .cls pop es pop ds mov bx,1920 .set_cursor: mov dx,0x3d4 mov al,0x0e out dx,al inc dx ;0x3d5 mov al,bh out dx,al dec dx ;0x3d4 mov al,0x0f out dx,al inc dx ;0x3d5 mov al,bl out dx,al popad ret ;------------------------------------------------------------------------------- read_hard_disk_0: ;從硬盤讀取一個邏輯扇區,也就是每次讀512字節;1個頁需要讀8次 ;EAX=邏輯扇區號 ;DS:EBX=目標緩沖區地址 ;返回:EBX=EBX+512 push eax push ecx push edx push eax mov dx,0x1f2 mov al,1 out dx,al ;讀取的扇區數 inc dx ;0x1f3 pop eax out dx,al ;LBA地址7~0 inc dx ;0x1f4 mov cl,8 shr eax,cl out dx,al ;LBA地址15~8 inc dx ;0x1f5 shr eax,cl out dx,al ;LBA地址23~16 inc dx ;0x1f6 shr eax,cl or al,0xe0 ;第一硬盤 LBA地址27~24 out dx,al inc dx ;0x1f7 mov al,0x20 ;讀命令 out dx,al .waits: in al,dx and al,0x88 cmp al,0x08 jnz .waits ;不忙,且硬盤已准備好數據傳輸 mov ecx,256 ;總共要讀取的字數 mov dx,0x1f0 .readw: in ax,dx mov [ebx],ax add ebx,2 loop .readw pop edx pop ecx pop eax retf ;段間返回 ;------------------------------------------------------------------------------- put_hex_dword: ;在當前光標處以十六進制形式顯示 ;一個雙字並推進光標 ;輸入:EDX=要轉換並顯示的數字 ;輸出:無 pushad push ds mov ax,core_data_seg_sel ;切換到核心數據段 mov ds,ax mov ebx,bin_hex ;指向核心數據段內的轉換表 mov ecx,8 .xlt: rol edx,4 mov eax,edx and eax,0x0000000f xlat push ecx mov cl,al call put_char pop ecx loop .xlt pop ds popad retf ;------------------------------------------------------------------------------- set_up_gdt_descriptor: ;在GDT內安裝一個新的描述符,還是在0x7e00的地方 ;輸入:EDX:EAX=描述符 ;輸出:CX=描述符的選擇子 push eax push ebx push edx push ds push es mov ebx,core_data_seg_sel ;切換到核心數據段 mov ds,ebx sgdt [pgdt] ;以便開始處理GDT mov ebx,mem_0_4_gb_seg_sel mov es,ebx movzx ebx,word [pgdt] ;GDT界限 inc bx ;GDT總字節數,也是下一個描述符偏移 add ebx,[pgdt+2] ;下一個描述符的線性地址 mov [es:ebx],eax ; mov [es:ebx+4],edx ; add word [pgdt],8 ;增加一個描述符的大小 lgdt [pgdt] ;對GDT的更改生效 mov ax,[pgdt] ;得到GDT界限值 xor dx,dx mov bx,8 div bx ;除以8,去掉余數 mov cx,ax shl cx,3 ;將索引號移到正確位置 pop es pop ds pop edx pop ebx pop eax retf ;------------------------------------------------------------------------------- make_seg_descriptor: ;構造存儲器和系統的段描述符 ;輸入:EAX=線性基地址 ; EBX=段界限 ; ECX=屬性。各屬性位都在原始 ; 位置,無關的位清零 ;返回:EDX:EAX=描述符 mov edx,eax shl eax,16 or ax,bx ;描述符前32位(EAX)構造完畢 and edx,0xffff0000 ;清除基地址中無關的位 rol edx,8 bswap edx ;裝配基址的31~24和23~16 (80486+) xor bx,bx or edx,ebx ;裝配段界限的高4位 or edx,ecx ;裝配屬性 retf ;------------------------------------------------------------------------------- make_gate_descriptor: ;構造門的描述符(調用門等) ;輸入:EAX=門代碼在段內偏移地址 ; BX=門代碼所在段的選擇子 ; CX=段類型及屬性等(各屬 ; 性位都在原始位置) ;返回:EDX:EAX=完整的描述符 push ebx push ecx mov edx,eax and edx,0xffff0000 ;得到偏移地址高16位 or dx,cx ;組裝屬性部分到EDX and eax,0x0000ffff ;得到偏移地址低16位 shl ebx,16 or eax,ebx ;組裝段選擇子部分 pop ecx pop ebx retf ;------------------------------------------------------------------------------- allocate_a_4k_page: ;分配一個4KB的頁 ;輸入:無 ;輸出:EAX=頁的物理地址 push ebx push ecx push edx push ds mov eax,core_data_seg_sel mov ds,eax xor eax,eax .b1: ;遍歷page_bit_map,找到第一個標識是0的位,說明該頁還未使用 bts [page_bit_map],eax ;[page_bit_map]第eax的位復制給CF,同時置1 jnc .b2 ;CF=0,說明找到了空閑的物理頁;物理頁索引存放在eax inc eax ;沒有找到,eax+1繼續找 cmp eax,page_map_len*8 ;遍歷到page_bit_map末尾了嗎? jl .b1 ;沒有就從頭繼續找 mov ebx,message_3 call sys_routine_seg_sel:put_string hlt ;沒有可以分配的頁,停機 .b2: shl eax,12 ;eax存放了空閑的物理頁索引,乘以4096(0x1000)就是地址 pop ds pop edx pop ecx pop ebx ret ;------------------------------------------------------------------------------- alloc_inst_a_page: ;給指定的線性地址掛載物理頁 ;層級分頁結構中 ;輸入:EBX=頁的線性地址,比如0x80104000 push eax push ebx push esi push ds mov eax,mem_0_4_gb_seg_sel mov ds,eax ;檢查該線性地址所對應的頁表是否存在;把ebx高10位作為PDT的索引查找PTE; mov esi,ebx ;esi=0x80104000 and esi,0xffc00000 ;只保留最高的10位,低22位清零,得到PDT的索引,esi=0x80000000 shr esi,20 ;高12位移到低12位:得到頁目錄索引,並乘以4,得到PTE在PDE內的偏移地址;esi=0x00000800 or esi,0xfffff000 ;頁目錄自身的線性地址+表內偏移;最高20位置1的線性地址,轉換成物理地址=PDT基址(這里是0x20000)+esi,相當於最低3字節就是PDT內的偏移,高20位置1確保物理地址還是落在PDT內;esi=0xfffff800 test dword [esi],0x00000001 ;P位是否為“1”.如果PDT某項有PTE,結尾不會是0;如果是0,說明還未掛載物理頁;[esi]=0x00000003,最后4位是0011; jnz .b1 ;否已經有對應的頁表 ;創建該線性地址所對應的頁表 call allocate_a_4k_page ;分配一個頁做為頁表 or eax,0x00000007 ;該頁的屬性:U/S=1,允許3環訪問;RW=1,可讀可寫;P=1,表明有物理頁了 mov [esi],eax ;在頁目錄中登記該物理地址 .b1: ;不論是否執行JNZ .b1,代碼最終會走到這里來 ;開始訪問該線性地址所對應的頁表 mov esi,ebx ;esi=0x80104000 shr esi,10 ;高22位移到低22位,esi=0x00200410 and esi,0x003ff000 ;只保留原線性地址高10位,也就是PDT的偏移;esi=0x00200000 or esi,0xffc00000 ;原線性地址最高10位保存在esi的中間10位,即11-20位;高10位置1,這樣在PDT內查的時候能得到0x21003,也就是頁表的基址; ;得到該線性地址在頁表內的對應條目(頁表項) and ebx,0x003ff000 ;ebx=0x00104000,保留原線性地址中間10位 shr ebx,10 ;相當於右移12位,再乘以4;原線性地址中間10位右移到低2~11位,得到頁表內的偏移;ebx=0x410 or esi,ebx ;頁表項的線性地址;原線性地址的高10位、中間10位依次右移,現在是從2~20位,高11位置1;原線性地址高10位用來作為頁表的偏移,中間10位用來做頁表的偏移; esi=0xFFF00410 call allocate_a_4k_page ;分配一個頁,這才是要安裝的頁 or eax,0x00000007 mov [esi],eax pop ds pop esi pop ebx pop eax retf ;------------------------------------------------------------------------------- create_copy_cur_pdir: ;創建新頁目錄,並復制當前頁目錄內容 ;輸入:無 ;輸出:EAX=新頁目錄的物理地址 push ds push es push esi push edi push ebx push ecx mov ebx,mem_0_4_gb_seg_sel mov ds,ebx mov es,ebx call allocate_a_4k_page mov ebx,eax or ebx,0x00000007 ;用戶程序的頁目錄和頁表,當然是3環能訪問的,所以U/S=1;RW=1可讀可寫;P=1表明已經有物理頁 mov [0xfffffff8],ebx ;頁目錄表倒數第二項(最后一項已經是0x20003了) mov esi,0xfffff000 ;ESI->當前頁目錄的線性地址 mov edi,0xffffe000 ;EDI->新頁目錄的線性地址,剛好指向頁目錄表的倒數第二項,存放了剛才申請的物理地址 mov ecx,1024 ;ECX=要復制的目錄項數 cld repe movsd pop ecx pop ebx pop edi pop esi pop es pop ds retf ;------------------------------------------------------------------------------- terminate_current_task: ;終止當前任務 ;注意,執行此例程時,當前任務仍在 ;運行中。此例程其實也是當前任務的 ;一部分 mov eax,core_data_seg_sel mov ds,eax pushfd pop edx test dx,0100_0000_0000_0000B ;測試NT位 jnz .b1 ;當前任務是嵌套的,到.b1執行iretd jmp far [program_man_tss] ;程序管理器任務 .b1: iretd sys_routine_end: ;=============================================================================== SECTION core_data vstart=0 ;系統核心的數據段 ;------------------------------------------------------------------------------- pgdt dw 0 ;用於設置和修改GDT dd 0 ;為了簡化,這里只用2M內存,有512個物理頁;已經占用的置1,沒用的置0 page_bit_map db 0xff,0xff,0xff,0xff,0xff,0x55,0x55,0xff ;低地址基本都用光了,高地址還空着 db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff db 0xff,0xff,0xff,0xff,0xff,0xff,0xff,0xff db 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55 db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 db 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00 page_map_len equ $-page_bit_map ;符號地址檢索表,類似於導出表,詳細記錄了可供第三方調用的函數名、函數地址 salt: salt_1 db '@PrintString' ;從@PrintString開始,長度是12字節 times 256-($-salt_1) db 0 ;剩余256-12=244字節填0;API函數名最長不超過256字節 dd put_string ;函數在API段內的偏移 dw sys_routine_seg_sel ;API段的選擇子,根據后面這6字節可以直接調用API函數 salt_2 db '@ReadDiskData' times 256-($-salt_2) db 0 dd read_hard_disk_0 dw sys_routine_seg_sel salt_3 db '@PrintDwordAsHexString' times 256-($-salt_3) db 0 dd put_hex_dword dw sys_routine_seg_sel salt_4 db '@TerminateProgram' times 256-($-salt_4) db 0 dd terminate_current_task dw sys_routine_seg_sel salt_item_len equ $-salt_4 salt_items equ ($-salt)/salt_item_len message_0 db ' Working in system core,protect mode.' db 0x0d,0x0a,0 message_1 db ' Paging is enabled.System core is mapped to' db ' address 0x80000000.',0x0d,0x0a,0 message_2 db 0x0d,0x0a db ' System wide CALL-GATE mounted.',0x0d,0x0a,0 message_3 db '********No more pages********',0 message_4 db 0x0d,0x0a,' Task switching...@_@',0x0d,0x0a,0 message_5 db 0x0d,0x0a,' Processor HALT.',0 bin_hex db '0123456789ABCDEF' ;put_hex_dword子過程用的查找表 core_buf times 512 db 0 ;內核用的緩沖區 cpu_brnd0 db 0x0d,0x0a,' ',0 cpu_brand times 52 db 0 cpu_brnd1 db 0x0d,0x0a,0x0d,0x0a,0 ;任務控制塊鏈 tcb_chain dd 0 ;內核信息 core_next_laddr dd 0x80100000 ;內核空間中下一個可分配的線性地址;每次在線性地址分配一塊內存,該值就會增加; program_man_tss dd 0 ;程序管理器的TSS描述符選擇子 dw 0 core_data_end: ;=============================================================================== SECTION core_code vstart=0 ;------------------------------------------------------------------------------- fill_descriptor_in_ldt: ;在LDT內安裝一個新的描述符 ;輸入:EDX:EAX=描述符 ; EBX=TCB基地址 ;輸出:CX=描述符的選擇子 push eax push edx push edi push ds mov ecx,mem_0_4_gb_seg_sel mov ds,ecx mov edi,[ebx+0x0c] ;獲得LDT基地址 xor ecx,ecx mov cx,[ebx+0x0a] ;獲得LDT界限 inc cx ;LDT的總字節數,即新描述符偏移地址 mov [edi+ecx+0x00],eax mov [edi+ecx+0x04],edx ;安裝描述符 add cx,8 dec cx ;得到新的LDT界限值 mov [ebx+0x0a],cx ;更新LDT界限值到TCB mov ax,cx xor dx,dx mov cx,8 div cx mov cx,ax shl cx,3 ;左移3位,並且 or cx,0000_0000_0000_0100B ;使TI位=1,指向LDT,最后使RPL=00 pop ds pop edi pop edx pop eax ret ;------------------------------------------------------------------------------- load_relocate_program: ;加載並重定位用戶程序 ;輸入: PUSH 邏輯扇區號 ; PUSH 任務控制塊基地址 ;輸出:無 pushad push ds push es mov ebp,esp ;為訪問通過堆棧傳遞的參數做准備 mov ecx,mem_0_4_gb_seg_sel mov es,ecx ;清空當前頁目錄的前半部分(對應低2GB的局部地址空間) mov ebx,0xfffff000 xor esi,esi .b1: mov dword [es:ebx+esi*4],0x00000000 inc esi cmp esi,512 jl .b1 ;以下開始分配內存並加載用戶程序 mov eax,core_data_seg_sel mov ds,eax ;切換DS到內核數據段 mov eax,[ebp+12*4] ;從堆棧中取出用戶程序起始扇區號 mov ebx,core_buf ;讀取程序頭部數據 call sys_routine_seg_sel:read_hard_disk_0 ;以下判斷整個程序有多大 mov eax,[core_buf] ;程序尺寸 mov ebx,eax and ebx,0xfffff000 ;使之4KB對齊 add ebx,0x1000 test eax,0x00000fff ;程序的大小正好是4KB的倍數嗎? cmovnz eax,ebx ;不是。使用湊整的結果 mov ecx,eax shr ecx,12 ;程序占用的總4KB頁數,即用戶程序需要幾個頁加載 mov eax,mem_0_4_gb_seg_sel ;切換DS到0-4GB的段 mov ds,eax mov eax,[ebp+12*4] ;起始扇區號 mov esi,[ebp+11*4] ;從堆棧中取得TCB的基地址 .b2: mov ebx,[es:esi+0x06] ;取得可用的線性地址 add dword [es:esi+0x06],0x1000 ;線性地址分配后加0x1000,下次從這里繼續申請新內存 call sys_routine_seg_sel:alloc_inst_a_page push ecx mov ecx,8 .b3: call sys_routine_seg_sel:read_hard_disk_0 inc eax loop .b3 pop ecx loop .b2 ;在內核地址空間內創建用戶任務的TSS mov eax,core_data_seg_sel ;切換DS到內核數據段 mov ds,eax mov ebx,[core_next_laddr] ;用戶任務的TSS必須在全局空間上分配 call sys_routine_seg_sel:alloc_inst_a_page add dword [core_next_laddr],4096 mov [es:esi+0x14],ebx ;在TCB中填寫TSS的線性地址 mov word [es:esi+0x12],103 ;在TCB中填寫TSS的界限值 ;在用戶任務的局部地址空間內創建LDT mov ebx,[es:esi+0x06] ;從TCB中取得可用的線性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov [es:esi+0x0c],ebx ;填寫LDT線性地址到TCB中 ;建立程序代碼段描述符 mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0f800 ;4KB粒度的代碼段描述符,特權級3 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0011B ;設置選擇子的特權級為3 mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+76],cx ;填寫TSS的CS域 ;建立程序數據段描述符 mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0f200 ;4KB粒度的數據段描述符,特權級3 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0011B ;設置選擇子的特權級為3 mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+84],cx ;填寫TSS的DS域 mov [es:ebx+72],cx ;填寫TSS的ES域 mov [es:ebx+88],cx ;填寫TSS的FS域 mov [es:ebx+92],cx ;填寫TSS的GS域 ;將數據段作為用戶任務的3特權級固有堆棧 mov ebx,[es:esi+0x06] ;從TCB中取得可用的線性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+80],cx ;填寫TSS的SS域 mov edx,[es:esi+0x06] ;堆棧的高端線性地址 mov [es:ebx+56],edx ;填寫TSS的ESP域 ;在用戶任務的局部地址空間內創建0特權級堆棧 mov ebx,[es:esi+0x06] ;從TCB中取得可用的線性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c09200 ;4KB粒度的堆棧段描述符,特權級0 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0000B ;設置選擇子的特權級為0 mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+8],cx ;填寫TSS的SS0域 mov edx,[es:esi+0x06] ;堆棧的高端線性地址 mov [es:ebx+4],edx ;填寫TSS的ESP0域 ;在用戶任務的局部地址空間內創建1特權級堆棧 mov ebx,[es:esi+0x06] ;從TCB中取得可用的線性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0b200 ;4KB粒度的堆棧段描述符,特權級1 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0001B ;設置選擇子的特權級為1 mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+16],cx ;填寫TSS的SS1域 mov edx,[es:esi+0x06] ;堆棧的高端線性地址 mov [es:ebx+12],edx ;填寫TSS的ESP1域 ;在用戶任務的局部地址空間內創建2特權級堆棧 mov ebx,[es:esi+0x06] ;從TCB中取得可用的線性地址 add dword [es:esi+0x06],0x1000 call sys_routine_seg_sel:alloc_inst_a_page mov eax,0x00000000 mov ebx,0x000fffff mov ecx,0x00c0d200 ;4KB粒度的堆棧段描述符,特權級2 call sys_routine_seg_sel:make_seg_descriptor mov ebx,esi ;TCB的基地址 call fill_descriptor_in_ldt or cx,0000_0000_0000_0010B ;設置選擇子的特權級為2 mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+24],cx ;填寫TSS的SS2域 mov edx,[es:esi+0x06] ;堆棧的高端線性地址 mov [es:ebx+20],edx ;填寫TSS的ESP2域 ;重定位SALT mov eax,mem_0_4_gb_seg_sel ;訪問任務的4GB虛擬地址空間時用 mov es,eax mov eax,core_data_seg_sel mov ds,eax cld mov ecx,[es:0x0c] ;U-SALT條目數 mov edi,[es:0x08] ;U-SALT在4GB空間內的偏移 .b4: push ecx push edi mov ecx,salt_items mov esi,salt .b5: push edi push esi push ecx mov ecx,64 ;檢索表中,每條目的比較次數 repe cmpsd ;每次比較4字節 jnz .b6 mov eax,[esi] ;若匹配,則esi恰好指向其后的地址 mov [es:edi-256],eax ;將字符串改寫成偏移地址 mov ax,[esi+4] or ax,0000000000000011B ;以用戶程序自己的特權級使用調用門 ;故RPL=3 mov [es:edi-252],ax ;回填調用門選擇子 .b6: pop ecx pop esi add esi,salt_item_len pop edi ;從頭比較 loop .b5 pop edi add edi,256 pop ecx loop .b4 ;在GDT中登記LDT描述符 mov esi,[ebp+11*4] ;從堆棧中取得TCB的基地址 mov eax,[es:esi+0x0c] ;LDT的起始線性地址 movzx ebx,word [es:esi+0x0a] ;LDT段界限 mov ecx,0x00408200 ;LDT描述符,特權級0 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [es:esi+0x10],cx ;登記LDT選擇子到TCB中 mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov [es:ebx+96],cx ;填寫TSS的LDT域 mov word [es:ebx+0],0 ;反向鏈=0 mov dx,[es:esi+0x12] ;段長度(界限) mov [es:ebx+102],dx ;填寫TSS的I/O位圖偏移域 mov word [es:ebx+100],0 ;T=0 mov eax,[es:0x04] ;從任務的4GB地址空間獲取入口點 mov [es:ebx+32],eax ;填寫TSS的EIP域 pushfd pop edx mov [es:ebx+36],edx ;填寫TSS的EFLAGS域 ;在GDT中登記TSS描述符 mov eax,[es:esi+0x14] ;從TCB中獲取TSS的起始線性地址 movzx ebx,word [es:esi+0x12] ;段長度(界限) mov ecx,0x00408900 ;TSS描述符,特權級0 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [es:esi+0x18],cx ;登記TSS選擇子到TCB ;創建用戶任務的頁目錄 ;注意!頁的分配和使用是由頁位圖決定的,可以不占用線性地址空間 call sys_routine_seg_sel:create_copy_cur_pdir mov ebx,[es:esi+0x14] ;從TCB中獲取TSS的線性地址 mov dword [es:ebx+28],eax ;填寫TSS的CR3(PDBR)域 pop es ;恢復到調用此過程前的es段 pop ds ;恢復到調用此過程前的ds段 popad ret 8 ;丟棄調用本過程前壓入的參數 ;------------------------------------------------------------------------------- append_to_tcb_link: ;在TCB鏈上追加任務控制塊 ;輸入:ECX=TCB線性基地址 push eax push edx push ds push es mov eax,core_data_seg_sel ;令DS指向內核數據段 mov ds,eax mov eax,mem_0_4_gb_seg_sel ;令ES指向0..4GB段 mov es,eax mov dword [es: ecx+0x00],0 ;當前TCB指針域清零,以指示這是最 ;后一個TCB mov eax,[tcb_chain] ;TCB表頭指針 or eax,eax ;鏈表為空? jz .notcb .searc: mov edx,eax mov eax,[es: edx+0x00] or eax,eax jnz .searc mov [es: edx+0x00],ecx jmp .retpc .notcb: mov [tcb_chain],ecx ;若為空表,直接令表頭指針指向TCB .retpc: pop es pop ds pop edx pop eax ret ;------------------------------------------------------------------------------- start: mov ecx,core_data_seg_sel ;令DS指向核心數據段 mov ds,ecx mov ecx,mem_0_4_gb_seg_sel ;令ES指向4GB數據段 mov es,ecx mov ebx,message_0 call sys_routine_seg_sel:put_string ;顯示處理器品牌信息 mov eax,0x80000002 cpuid mov [cpu_brand + 0x00],eax mov [cpu_brand + 0x04],ebx mov [cpu_brand + 0x08],ecx mov [cpu_brand + 0x0c],edx mov eax,0x80000003 cpuid mov [cpu_brand + 0x10],eax mov [cpu_brand + 0x14],ebx mov [cpu_brand + 0x18],ecx mov [cpu_brand + 0x1c],edx mov eax,0x80000004 cpuid mov [cpu_brand + 0x20],eax mov [cpu_brand + 0x24],ebx mov [cpu_brand + 0x28],ecx mov [cpu_brand + 0x2c],edx mov ebx,cpu_brnd0 ;顯示處理器品牌信息 call sys_routine_seg_sel:put_string mov ebx,cpu_brand call sys_routine_seg_sel:put_string mov ebx,cpu_brnd1 call sys_routine_seg_sel:put_string ;准備打開分頁機制 ;創建系統內核的頁目錄表PDT ;頁目錄表清零 mov ecx,1024 ;1024個目錄項PDE mov ebx,0x00020000 ;頁目錄的物理地址 xor esi,esi .b1: mov dword [es:ebx+esi],0x00000000 ;頁目錄表項清零 add esi,4 loop .b1 ;在頁目錄內創建指向頁目錄自己的目錄項,最后一項指向自己,那么線性地址高20位是0xFFFFF的時候,轉成物理地址就是頁目錄自己 mov dword [es:ebx+4092],0x00020003 ;頁目錄的第一項,內核第一個頁表的物理地址:0x00021000 mov dword [es:ebx+0],0x00021003 ;寫入目錄項(頁表的物理地址和屬性) ;創建與上面那個目錄項相對應的頁表,初始化頁表項 mov ebx,0x00021000 ;頁表的物理地址 xor eax,eax ;起始頁的物理地址 xor esi,esi ;esi=0 .b2: mov edx,eax ;edx=eax; eax=0x1000*n or edx,0x00000003 ;edx=0x1000*n+3;u/s=1,允許所有特權級別的程序訪問; mov [es:ebx+esi*4],edx ;登記頁的物理地址; 0x21000~0x21400都是PTE,隱射從0~1MB(256*4096=1Mb)的物理地址; add eax,0x1000 ;下一個相鄰頁的物理地址 inc esi cmp esi,256 ;僅低端1MB內存對應的頁才是有效的 jl .b2 .b3: ;其余的頁表項置為無效 mov dword [es:ebx+esi*4],0x00000000 ;0x21400~(0x21400+(1024-256)*4=0x22000)清零; inc esi cmp esi,1024 jl .b3 ;令CR3寄存器指向頁目錄,並正式開啟頁功能 mov eax,0x00020000 ;PCD=PWT=0,PDT基址=0x00020000 mov cr3,eax mov eax,cr0 or eax,0x80000000 mov cr0,eax ;開啟分頁機制 ;在頁目錄內創建與線性地址0x80000000對應的目錄項,有了這個項,0x800000000才會被映射到0x21000的PET; 線性地址0x80000000~0x800FFFFF映射的物理地址:0x00000~0xFFFFF mov ebx,0xfffff000 ;頁目錄自己的線性地址;高5字節都是F,低3字節就是PDT內的偏移 mov esi,0x80000000 ;映射的起始地址 shr esi,22 ;取線性地址高10位(目錄索引),esi=0x200 shl esi,2 ;索引乘以4得到偏移 mov dword [es:ebx+esi],0x00021003 ;寫入目錄項(頁表的物理地址和屬性)es:ebx+esi = 0xFFFFF800 ;將GDT中的段描述符映射到線性地址0x80000000 sgdt [pgdt] mov ebx,[pgdt+2] ;ebx存放GDT的base or dword [es:ebx+0x10+4],0x80000000 ; or dword [es:ebx+0x18+4],0x80000000 ;內核堆棧段 or dword [es:ebx+0x20+4],0x80000000 ;視頻顯示緩沖區 or dword [es:ebx+0x28+4],0x80000000 ;API段 or dword [es:ebx+0x30+4],0x80000000 ;內核數據段 or dword [es:ebx+0x38+4],0x80000000 ;內核代碼段 add dword [pgdt+2],0x80000000 ;GDTR也用的是線性地址 lgdt [pgdt] jmp core_code_seg_sel:flush ;刷新段寄存器CS,啟用高端線性地址 flush: mov eax,core_stack_seg_sel mov ss,eax mov eax,core_data_seg_sel mov ds,eax mov ebx,message_1 call sys_routine_seg_sel:put_string ;以下開始安裝為整個系統服務的調用門。特權級之間的控制轉移必須使用門 mov edi,salt ;C-SALT表的起始位置,內核API函數導出表,有函數名稱、函數在API段內的偏移、API段的選擇子 mov ecx,salt_items ;C-SALT表的條目數量,ecx=4 .b4: push ecx mov eax,[edi+256] ;該條目入口點的32位偏移地址;API函數的段內偏移地址 mov bx,[edi+260] ;該條目入口點的段選擇子 ;API函數所在段的選擇子 mov cx,1_11_0_1100_000_00000B ;特權級3的調用門(3以上的特權級才 ;允許訪問),0個參數(因為用寄存器 ;傳遞參數,而沒有用棧) call sys_routine_seg_sel:make_gate_descriptor ;返回完整的描述符,保存在EDX:EAX; call sys_routine_seg_sel:set_up_gdt_descriptor ;上一步構造好的門描述符寫回GDT表 mov [edi+260],cx ;將返回的門描述符選擇子回填 add edi,salt_item_len ;指向下一個C-SALT條目 pop ecx loop .b4 ;對門進行測試 mov ebx,message_2 call far [salt_1+256] ;通過門顯示信息(偏移量將被忽略);salt_1+256,低4字節是段內偏移,高2字節是選擇子 ;為程序管理器的TSS分配內存空間 mov ebx,[core_next_laddr] ;從0x80100000開始分配,查找還沒使用的線性地址 call sys_routine_seg_sel:alloc_inst_a_page ;給線性地址掛載物理頁 add dword [core_next_laddr],4096 ;線性地址增加0x1000; ;在程序管理器的TSS中設置必要的項目;該線性地址已經掛載物理頁,可以正常使用了 mov word [es:ebx+0],0 ;反向鏈=0 mov eax,cr3 mov dword [es:ebx+28],eax ;登記CR3(PDBR) mov word [es:ebx+96],0 ;沒有LDT。處理器允許沒有LDT的任務。 mov word [es:ebx+100],0 ;T=0 mov word [es:ebx+102],103 ;沒有I/O位圖。0特權級事實上不需要。 ;創建程序管理器的TSS描述符,並安裝到GDT中 mov eax,ebx ;TSS的起始線性地址 mov ebx,103 ;段長度(界限) mov ecx,0x00408900 ;TSS描述符,特權級0 call sys_routine_seg_sel:make_seg_descriptor call sys_routine_seg_sel:set_up_gdt_descriptor mov [program_man_tss+4],cx ;保存程序管理器的TSS描述符選擇子 ;任務寄存器TR中的內容是任務存在的標志,該內容也決定了當前任務是誰。 ;下面的指令為當前正在執行的0特權級任務“程序管理器”后補手續(TSS)。 ltr cx ;現在可認為“程序管理器”任務正執行中 ;創建用戶任務的任務控制塊,類似windows下的進程控制塊PCB mov ebx,[core_next_laddr] ;從0x80100000開始分配 call sys_routine_seg_sel:alloc_inst_a_page add dword [core_next_laddr],4096 mov dword [es:ebx+0x06],0 ;用戶任務局部空間的分配從0開始。 mov word [es:ebx+0x0a],0xffff ;登記LDT初始的界限到TCB中 mov ecx,ebx call append_to_tcb_link ;將此TCB添加到TCB鏈中,類似windows下EPROCESS的鏈條 push dword 50 ;用戶程序位於邏輯50扇區 push ecx ;壓入任務控制塊起始線性地址 call load_relocate_program mov ebx,message_4 call sys_routine_seg_sel:put_string call far [es:ecx+0x14] ;執行任務切換。 mov ebx,message_5 call sys_routine_seg_sel:put_string hlt core_code_end: ;------------------------------------------------------------------------------- SECTION core_trail ;------------------------------------------------------------------------------- core_end:
用戶程序:
program_length dd program_end ;程序總長度#0x00 = 0x1F88E entry_point dd start ;程序入口點#0x04 = 0x1F85B salt_position dd salt_begin ;SALT表起始偏移量#0x08 =0x10 salt_items dd (salt_end-salt_begin)/256 ;SALT條目數#0x0C = 0x1F8 ;------------------------------------------------------------------------------- ;符號地址檢索表 salt_begin: PrintString db '@PrintString' ;內核代碼會對導入表做重定位,把內核API的實際偏移、選擇子寫回,覆蓋@PrintString前6個字節,下面就可以直接通過call far [PrintString]調用內核API函數了 times 256-($-PrintString) db 0 TerminateProgram db '@TerminateProgram' times 256-($-TerminateProgram) db 0 ;------------------------------------------------------------------------------- reserved times 256*500 db 0 ;保留一個空白區,以演示分頁 ;------------------------------------------------------------------------------- ReadDiskData db '@ReadDiskData' times 256-($-ReadDiskData) db 0 PrintDwordAsHex db '@PrintDwordAsHexString' times 256-($-PrintDwordAsHex) db 0 salt_end: message_0 db 0x0d,0x0a, db ' ............User task is running with ' db 'paging enabled!............',0x0d,0x0a,0 space db 0x20,0x20,0 ;------------------------------------------------------------------------------- [bits 32] ;------------------------------------------------------------------------------- start: mov ebx,message_0 call far [PrintString] xor esi,esi mov ecx,88 .b1: mov ebx,space call far [PrintString] mov edx,[esi*4] call far [PrintDwordAsHex] inc esi loop .b1 call far [TerminateProgram] ;退出,並將控制權返回到核心 ;------------------------------------------------------------------------------- program_end:
擴展一下:
這里用PE或ELF文件重定位做個對比:windows下加載dll一般都會重定位函數call、全局變量等,這時就要依賴PE文件頭中的重定位表了,和這里的虛擬地址到物理地址的“重定位”異曲同工,都是靠某種表格映射來實現的!
