x86彙編分頁模式實驗 --《ORANGE'S一個作業系統的實現》中 pmtest8.asm解析
序言(廢話) : 在看書的過程中發現一開始不是很能理解pmtest8的目的,以及書上說得很抽象..於是在自己閱讀過原始碼後,將一些自己的心得寫在這裡。
正文 :
講解順序依然按照書上貼程式碼的順序來。但是是幾乎逐句解釋的。可能會稍微有點囉嗦。廢話就不多說了直接貼程式碼。
LABEL_DESC_FLAT_C: Descriptor 0, 0fffffh, DA_CR|DA_32|DA_LIMIT_4K; 0~4G LABEL_DESC_FLAT_RW: Descriptor 0, 0fffffh, DA_DRW|DA_LIMIT_4K ; 0~4G SelectorFlatC equ LABEL_DESC_FLAT_C - LABEL_GDT SelectorFlatRW equ LABEL_DESC_FLAT_RW - LABEL_GDT
顯然,兩個分別是 FLAT_C 和 FLAT_RW 的描述符和選擇子。
問題 : 為什麼要有這兩個東西?
解釋 : FLAT_C是用來執行的非一致性32位程式碼段,粒度為4k,也就是 limit(段限長) = (0xfffff + 1) * 4k = 4G,FLAT_RW 是用來修改資料的,因為需要利用這個描述符的許可權(可寫)來將程式碼寫入到目的地(這個目的地允許在 0 - 4G區間內)。之所以要分兩個選擇符,是防止在執行的時候修改程式碼(所以FLAT_C不能給寫的許可權),但是又必須在執行之前進行復制,所以一定要有一個入口能提供寫入的方式,於是設定兩個描述符來進行。這樣既安全又有章法。
SetupPaging: ; 根據記憶體大小計算應初始化多少PDE以及多少頁表 xor edx, edx mov eax, [dwMemSize] mov ebx, 400000h ; 400000h = 4M = 4096 * 1024, 一個頁表對應的記憶體大小 div ebx mov ecx, eax ; 此時 ecx 為頁表的個數,也即 PDE 應該的個數 test edx, edx jz .no_remainder inc ecx ; 如果餘數不為 0 就需增加一個頁表 .no_remainder: mov [PageTableNumber], ecx ; 暫存頁表個數 ; 為簡化處理, 所有線性地址對應相等的實體地址. 並且不考慮記憶體空洞. ; 首先初始化頁目錄 mov ax, SelectorFlatRW mov es, ax mov edi, PageDirBase0 ; 此段首地址為 PageDirBase0 xor eax, eax mov eax, PageTblBase0 | PG_P | PG_USU | PG_RWW .1: ; es:edi 初始等於 PageDirBase0 (當前頁目錄表項), eax 初始基地址等於 PageTblBase0 stosd add eax, 4096 ; 為了簡化, 所有頁表在記憶體中是連續的. loop .1 ; 再初始化所有頁表 mov eax, [PageTableNumber] ; 頁表個數 mov ebx, 1024 ; 每個頁表 1024 個 PTE mul ebx mov ecx, eax ; PTE個數 = 頁表個數 * 1024 mov edi, PageTblBase0 ; 此段首地址為 PageTblBase0 xor eax, eax mov eax, PG_P | PG_USU | PG_RWW .2: ; es:edi 初始等於 PageTblBase0 (當前頁表項), eax = 0 (線性地址 = 實體地址) stosd add eax, 4096 ; 每一頁指向 4K 的空間 loop .2 mov eax, PageDirBase0 mov cr3, eax mov eax, cr0 or eax, 80000000h mov cr0, eax jmp short .3 .3: nop ret
這段程式碼我加註了兩句註釋 分別在 .1 和 .2 這兩個標籤那行,其實這裡和之前的setPaging並沒有很大的區別,需要注意的就是 這裡的 頁目錄表 的地址是 PageDirBase0, 頁表的地址是PageTblBase0,強調這點的原因在於之後的 PSwitch 這個函式中則是 PageDirBase1 和 PageTblBase1。也就是說實際上資料中有兩個頁面管理的資料結構(頁目錄表和頁表合起來相當於一個管理頁面的資料結構)。
1 PagingDemo: 2 mov ax, cs 3 mov ds, ax 4 mov ax, SelectorFlatRW ; 設定es為基地址為0的可讀寫的段(便於複製程式碼) 5 mov es, ax 6 7 push LenFoo 8 push OffsetFoo 9 push ProcFoo ; 00401000h 10 call MemCpy 11 add esp, 12 12 13 push LenBar ; 被複制程式碼段(但是以ds為段基址)的長度 14 push OffsetBar ; 被複制程式碼段(但是以ds為段基址)的段偏移量 15 push ProcBar ; 目的程式碼段的物理空間地址 00501000h 16 call MemCpy 17 add esp, 12 18 19 push LenPagingDemoAll 20 push OffsetPagingDemoProc 21 push ProcPagingDemo ; [es:ProcPagingDemo] = ProcPagingDemo = 00301000h 22 call MemCpy 23 add esp, 12 24 25 mov ax, SelectorData 26 mov ds, ax ; 資料段選擇子 27 mov es, ax 28 29 call SetupPaging ; 啟動分頁 30 ; 當前線性地址依然等於實體地址 31 call SelectorFlatC:ProcPagingDemo ; 訪問的線性地址為 00301000h,實體地址也是 00301000h 32 call PSwitch ; 切換頁目錄,改變地址對映關係 33 call SelectorFlatC:ProcPagingDemo ; 訪問的線性地址為 00301000h 34 35 ret
在這裡首先要說明的是 MemCpy函式,這個函式有三個引數分別表示 :
1)被複制段(但是以ds為段基址)的 長度
2)被複制段(但是以ds為段基址)的 段偏移量
3)目的地的物理空間地址(之所以說是物理空間是因為當前線性地址等於實體地址,以es為段基址,但是es的段基址為0)
功能則是 將被複制段 的資料複製 引數1)的長度位元組 去目的地去(簡單說就是利用三個引數複製資料)
我們可以知道的是在上面程式碼中三次呼叫 MemCpy 都沒有進入分頁模式,也就是說當下線性地址等於實體地址。那麼根據我上面的註釋就可以知道三個程式碼分別複製到哪裡去了。
之後就是恢復資料段(之前將ds = cs,是為了複製程式碼),然後啟動分頁(上面已經講了),然後啟動分頁後當前線性地址依然等於實體地址。
這個時候第一次呼叫 call SelectorFlatC:ProcPagingDemo,也就是訪問的線性地址為 00301000h,實體地址也是 00301000h的程式碼(之前移動過去的)。
下面這段程式碼就是被移動到00301000h的程式碼,這段程式碼只做了一件事那就是呼叫 [cs:LinearAddrDemo]的程式碼,但請注意,由於 call SelectorFlatC:ProcPagingDemo
所以此時的 cs = SelectorFlatC,也就是說段基址等於0,於是實際上這段程式碼的功能就是訪問 實體地址為00401000h處的程式碼。
PagingDemoProc: OffsetPagingDemoProc equ PagingDemoProc - $$ mov eax, LinearAddrDemo call eax ; 未開始PSwitch前, eax = ProcFoo = 00401000h (cs 的段基址 = 0) retf LenPagingDemoAll equ $ - PagingDemoProc
而實體地址00401000h處就是ProcFoo的程式碼(第一次呼叫MemCpy拷貝的程式碼)。被拷貝的程式碼如下
foo: OffsetFoo equ foo - $$ mov ah, 0Ch ; 0000: 黑底 1100: 紅字 mov al, 'F' mov [gs:((80 * 17 + 0) * 2)], ax ; 螢幕第 17 行, 第 0 列。 mov al, 'o' mov [gs:((80 * 17 + 1) * 2)], ax ; 螢幕第 17 行, 第 1 列。 mov [gs:((80 * 17 + 2) * 2)], ax ; 螢幕第 17 行, 第 2 列。 ret LenFoo equ $ - foo
功能很明顯就是現實一個字串 Foo而已。
總結第一次分頁後的動作:
就是拷貝三份程式碼分別到ProcFoo, ProcBar, ProcPagingDemo 處(這四個都是實體記憶體哦,並且後面因為段基址是0(FLAT_C 段基址)於是很容易地就訪問到了實體地址)。然後開啟分頁模式(其實幾乎沒什麼影響 因為仍然和分段一樣 線性地址 = 實體地址)。然後呼叫 被拷貝的函式 ProcPagingDemo ,ProcPagingDemo 函式呼叫 ProcFoo函式,顯示字元 "Foo"然後兩次返回。
第二次分頁 : call PSwitch
被呼叫程式碼如下 :
1 PSwitch: 2 ; 初始化頁目錄 3 mov ax, SelectorFlatRW 4 mov es, ax 5 mov edi, PageDirBase1 ; 此段首地址為 PageDirBase1 6 xor eax, eax 7 mov eax, PageTblBase1 | PG_P | PG_USU | PG_RWW 8 mov ecx, [PageTableNumber] 9 .1: ; es:edi 初始等於 PageDirBase1 (當前頁目錄表項), eax 初始基地址等於 PageTblBase1 10 stosd 11 add eax, 4096 ; 為了簡化, 所有頁表在記憶體中是連續的. 12 loop .1 13 14 ; 再初始化所有頁表 15 mov eax, [PageTableNumber] ; 頁表個數 16 mov ebx, 1024 ; 每個頁表 1024 個 PTE 17 mul ebx 18 mov ecx, eax ; PTE個數 = 頁表個數 * 1024 19 mov edi, PageTblBase1 ; 此段首地址為 PageTblBase1 20 xor eax, eax 21 mov eax, PG_P | PG_USU | PG_RWW 22 .2: ; es:edi 初始等於 PageTblBase1 (當前頁表項), eax 初始基地址等於 0(線性地址等於實體地址) 23 stosd 24 add eax, 4096 ; 每一頁指向 4K 的空間 25 loop .2 26 27 ; 在此假設記憶體是大於 8M 的 28 ; 下列程式碼將LinearAddrDemo所處的頁表的相對第一個頁表的偏移地址放入ecx中 29 mov eax, LinearAddrDemo 30 shr eax, 22 31 mov ebx, 4096 ; (LinearAddrDemo / 4M)表示第幾個頁表 32 mul ebx ; 第幾個頁表 * 4k (1024(一個頁表項的數量) * 4(一個頁表項的位元組)) 33 mov ecx, eax ; 也就是對應頁表的偏移地址 34 35 ; 下列程式碼將LinearAddrDemo所處的頁表項相對第一個頁表項的偏移地址放入eax中 36 mov eax, LinearAddrDemo 37 shr eax, 12 ; LinearAddrDemo / 4k,表示第幾個頁表項 38 and eax, 03FFh ; 1111111111b (10 bits) ; 取低10位,也就是餘下的零散頁表項(一個頁表有2^10個頁表項) 39 mov ebx, 4 40 mul ebx ; * 4 表示的是具體偏移位元組數 41 add eax, ecx ; eax = (((LinearAddrDemo / 2^12) & 03FFh) * 4) + (4k * (LinearAddrDemo / 2^22)) 42 43 44 add eax, PageTblBase1 ; 第一個頁表的第一個頁表項 45 mov dword [es:eax], ProcBar | PG_P | PG_USU | PG_RWW 46 47 mov eax, PageDirBase1 48 mov cr3, eax 49 jmp short .3 50 .3: 51 nop 52 53 ret
在這裡我加了幾個比較重要的註釋分別在第 9, 22, 28,35處。
這段程式碼做了什麼?
首先是設定頁面管理的資料結構(頁表和頁目錄表),但是需要注意的是,這裡設定頁表和頁目錄表除了不是之前的頁面管理結構之外,其實內容是差不多的,也就是說當前(第25行)這裡的狀態也是 線性地址 = 實體地址 !!!
但是在第27行做了一個操作,就是將LinearAddrDemo對應的 頁表項的地址 換成了 ProcBar(00501000h) 的地址。(具體如何實現的請看27-45行我寫的註釋)。
在做完這些之後就返回第二次執行 call SelectorFlatC:ProcPagingDemo 了,在這個時候 cs = SelectorFlatC (段基址等於0), eip = ProcPagingDemo = 00301000h,也就是說訪問了
線性地址 = 00301000h處,但是這裡已經被修改,除了這個頁面之外,其他頁面都是 線性地址 = 實體地址,但是這裡 線性地址 = 00301000h ,對映的實體地址是 ProcBar(00501000h)
於是便呼叫了 ProcBar 段的程式碼,而這段的程式碼是第二次呼叫MemCpy時候複製過去的。被複制的具體程式碼是:
bar: OffsetBar equ bar - $$ mov ah, 0Ch ; 0000: 黑底 1100: 紅字 mov al, 'B' mov [gs:((80 * 18 + 0) * 2)], ax ; 螢幕第 18 行, 第 0 列。 mov al, 'a' mov [gs:((80 * 18 + 1) * 2)], ax ; 螢幕第 18 行, 第 1 列。 mov al, 'r' mov [gs:((80 * 18 + 2) * 2)], ax ; 螢幕第 18 行, 第 2 列。 ret LenBar equ $ - bar
也就是顯示一個字串 "Bar", 然後返回到PagingDemo的最後一句 ret,再次返回。於是這段程式碼也就結束了。
第二次程式碼是如何實現呼叫 ProcBar的?
通過將線性地址 = ProcPaging(00301000h)對應的頁表項的地址值給修改成了 PaocBar(00501000h)的實體地址,於是從 00301000h 的線性地址 對映到 00501000h的實體地址上去了,
但是其實其他地方(除了這個頁之外)的線性地址 = 實體地址依然成立。也是上面這段程式碼很小,一定是小於 4k(一頁的大小),於是只需要修改一個頁表項就可以了!
&nbs