Chapter 4:保護模式入門 === :::info 這是讀書筆記 ![book](https://hackmd.io/_uploads/H1rFFZZAkx.jpg =30%x) 作者:鄭鋼 出版社:佳魁資訊股份有限公司 出版日期:2017/05/31 ::: --- # 保護模式概述 * 第一次出現在 Intel 80286 CPU,address line change from 20-bit to 24-bit。 * 1985年推出首款 32 bits CPU: Intel 80386。 # 保護模式 * 定址範圍達到 4GB。 ## 暫存器擴充 * 暫存器寬度達到 32 bits。 * Descriptor Cache registers:記憶體段的 base address。 ## 定址擴充 ![IMG_0535](https://hackmd.io/_uploads/r1TQ-2i0Jx.jpg) ## 模式反轉 * [bits 16] : 告訴編譯器要編譯成 16 bits machine code。 * [bits 32] : 告訴編譯器要編譯成 32 bits machine code。 * 編譯器預設是 [bits 16]。 * 0x66 : 運算元大小模式反轉。 * 0x67 : 定址方式模式反轉。 ## 指令擴充 * push 指令:同時 support 8-bit, 16-bit, 32-bit。 # Global Descriptor Table(GDT) ## Segment Descriptor >參考資料:[Segment descriptor](https://en.wikipedia.org/wiki/Segment_descriptor) ![SegmentDescriptor.svg](https://hackmd.io/_uploads/H1V0zVnCkl.png) ## Selector ![selector](https://hackmd.io/_uploads/r1VkBVn0Jl.gif) ## GDTR / LDTR ![062111_1451_x86x8664CPU11](https://hackmd.io/_uploads/BJqJ4V30ye.png) :::warning Q:如果 GDT element 的範圍涵蓋另外一個 GDT element 的範圍會怎樣? Q:如果同個範圍有兩個不同 GDT 規則,該怎麼辦? 當我們在存取記憶體的時候,會利用 selector 來選擇 GDT,然後再搭配 base register 就可以操作該記憶體位置。所以是因應各種情境,來選擇適合的 GDT element。 ::: # 開啟 A20 位址線 >參考資料:[Bochs' map of I/O ports to functions](https://bochs.sourceforge.io/techspec/PORTS.LST) ![截圖 2025-04-16 凌晨3.19.28](https://hackmd.io/_uploads/BkTtZ4nCyg.png) # 保護模式開關 ![control-registers-cr4-and-cr0-l](https://hackmd.io/_uploads/Hy7-uE3Cye.jpg) # 正式啟動保護模式 ## Source Code >mbr.S ```asm= ;主引導程序 ;------------------------------------------------------------ %include "boot.inc" SECTION MBR vstart=0x7c00 mov ax,cs mov ds,ax mov es,ax mov ss,ax mov fs,ax mov sp,0x7c00 mov ax,0xb800 mov gs,ax ; 清屏 ;利用0x06號功能,上卷全部行,則可清屏。 ; ----------------------------------------------------------- ;INT 0x10 功能號:0x06 功能描述:上卷窗口 ;------------------------------------------------------ ;輸入: ;AH 功能號= 0x06 ;AL = 上卷的行數(如果為0,表示全部) ;BH = 上卷行屬性 ;(CL,CH) = 窗口左上角的(X,Y)位置 ;(DL,DH) = 窗口右下角的(X,Y)位置 ;無返回值: mov ax, 0600h mov bx, 0700h mov cx, 0 ; 左上角: (0, 0) mov dx, 184fh ; 右下角: (80,25), ; 因為VGA文本模式中,一行只能容納80個字符,共25行。 ; 下標從0開始,所以0x18=24,0x4f=79 int 10h ; int 10h ; 輸出字符串:MBR mov byte [gs:0x00],'1' mov byte [gs:0x01],0xA4 mov byte [gs:0x02],' ' mov byte [gs:0x03],0xA4 mov byte [gs:0x04],'M' mov byte [gs:0x05],0xA4 ;A表示綠色背景閃爍,4表示前景色為紅色 mov byte [gs:0x06],'B' mov byte [gs:0x07],0xA4 mov byte [gs:0x08],'R' mov byte [gs:0x09],0xA4 mov eax,LOADER_START_SECTOR ; 起始扇區lba地址 mov bx,LOADER_BASE_ADDR ; 寫入的地址 mov cx,4 ; 待讀入的扇區數 call rd_disk_m_16 ; 以下讀取程序的起始部分(一個扇區) jmp LOADER_BASE_ADDR ;------------------------------------------------------------------------------- ;功能:讀取硬盤n個扇區 rd_disk_m_16: ;------------------------------------------------------------------------------- ; eax=LBA扇區號 ; ebx=將數據寫入的內存地址 ; ecx=讀入的扇區數 mov esi,eax ;備份eax mov di,cx ;備份cx ;讀寫硬盤: ;第1步:設置要讀取的扇區數 mov dx,0x1f2 mov al,cl out dx,al ;讀取的扇區數 mov eax,esi ;恢覆ax ;第2步:將LBA地址存入0x1f3 ~ 0x1f6 ;LBA地址7~0位寫入端口0x1f3 mov dx,0x1f3 out dx,al ;LBA地址15~8位寫入端口0x1f4 mov cl,8 shr eax,cl mov dx,0x1f4 out dx,al ;LBA地址23~16位寫入端口0x1f5 shr eax,cl mov dx,0x1f5 out dx,al shr eax,cl and al,0x0f ;lba第24~27位 or al,0xe0 ; 設置7~4位為1110,表示lba模式 mov dx,0x1f6 out dx,al ;第3步:向0x1f7端口寫入讀命令,0x20 mov dx,0x1f7 mov al,0x20 out dx,al ;第4步:檢測硬盤狀態 .not_ready: ;同一端口,寫時表示寫入命令字,讀時表示讀入硬盤狀態 nop in al,dx and al,0x88 ;第4位為1表示硬盤控制器已準備好數據傳輸,第7位為1表示硬盤忙 cmp al,0x08 jnz .not_ready ;若未準備好,繼續等。 ;第5步:從0x1f0端口讀數據 mov ax, di mov dx, 256 mul dx mov cx, ax ; di為要讀取的扇區數,一個扇區有512字節,每次讀入一個字, ; 共需di*512/2次,所以di*256 mov dx, 0x1f0 .go_on_read: in ax,dx mov [bx],ax add bx,2 loop .go_on_read ret times 510-($-$$) db 0 db 0x55,0xaa ``` >boot.inc ```asm= ;------------- loader和kernel ---------- LOADER_BASE_ADDR equ 0x900 LOADER_START_SECTOR equ 0x2 ;-------------- gdt描述符屬性 ------------- DESC_G_4K equ 1_00000000000000000000000b DESC_D_32 equ 1_0000000000000000000000b DESC_L equ 0_000000000000000000000b ; 64位代碼標記,此處標記為0便可。 DESC_AVL equ 0_00000000000000000000b ; cpu不用此位,暫置為0 DESC_LIMIT_CODE2 equ 1111_0000000000000000b DESC_LIMIT_DATA2 equ DESC_LIMIT_CODE2 DESC_LIMIT_VIDEO2 equ 0000_000000000000000b DESC_P equ 1_000000000000000b DESC_DPL_0 equ 00_0000000000000b DESC_DPL_1 equ 01_0000000000000b DESC_DPL_2 equ 10_0000000000000b DESC_DPL_3 equ 11_0000000000000b DESC_S_CODE equ 1_000000000000b DESC_S_DATA equ DESC_S_CODE DESC_S_sys equ 0_000000000000b DESC_TYPE_CODE equ 1000_00000000b ;x=1,c=0,r=0,a=0 代碼段是可執行的,非依從的,不可讀的,已訪問位a清0. DESC_TYPE_DATA equ 0010_00000000b ;x=0,e=0,w=1,a=0 數據段是不可執行的,向上擴展的,可寫的,已訪問位a清0. DESC_CODE_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_CODE2 + DESC_P + DESC_DPL_0 + DESC_S_CODE + DESC_TYPE_CODE + 0x00 DESC_DATA_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_DATA2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x00 DESC_VIDEO_HIGH4 equ (0x00 << 24) + DESC_G_4K + DESC_D_32 + DESC_L + DESC_AVL + DESC_LIMIT_VIDEO2 + DESC_P + DESC_DPL_0 + DESC_S_DATA + DESC_TYPE_DATA + 0x0b ;-------------- 選擇子屬性 --------------- RPL0 equ 00b RPL1 equ 01b RPL2 equ 10b RPL3 equ 11b TI_GDT equ 000b TI_LDT equ 100b ``` >loader.S ```asm= %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR jmp loader_start ; 此處的物理地址是: ;構建gdt及其內部的描述符 GDT_BASE: dd 0x00000000 dd 0x00000000 CODE_DESC: dd 0x0000FFFF dd DESC_CODE_HIGH4 DATA_STACK_DESC: dd 0x0000FFFF dd DESC_DATA_HIGH4 VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7 dd DESC_VIDEO_HIGH4 ; 此時dpl已改為0 GDT_SIZE equ $ - GDT_BASE GDT_LIMIT equ GDT_SIZE - 1 times 60 dq 0 ; 此處預留60個描述符的slot SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相當於(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0 SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ; 同上 SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上 ;以下是定義gdt的指針,前2字節是gdt界限,後4字節是gdt起始地址 gdt_ptr dw GDT_LIMIT dd GDT_BASE loadermsg db '2 loader in real.' loader_start: ;打印字符,"2 LOADER"說明loader已經成功加載 ; 輸出背景色綠色,前景色紅色,並且跳動的字符串"1 MBR" mov byte [gs:160],'2' mov byte [gs:161],0xA4 ; A表示綠色背景閃爍,4表示前景色為紅色 mov byte [gs:162],' ' mov byte [gs:163],0xA4 mov byte [gs:164],'L' mov byte [gs:165],0xA4 mov byte [gs:166],'O' mov byte [gs:167],0xA4 mov byte [gs:168],'A' mov byte [gs:169],0xA4 mov byte [gs:170],'D' mov byte [gs:171],0xA4 mov byte [gs:172],'E' mov byte [gs:173],0xA4 mov byte [gs:174],'R' mov byte [gs:175],0xA4 ;------------------------------------------------------------ ;INT 0x10 功能號:0x13 功能描述:打印字符串 ;------------------------------------------------------------ ;輸入: ;AH 子功能號=13H ;BH = 頁碼 ;BL = 屬性(若AL=00H或01H) ;CX=字符串長度 ;(DH、DL)=坐標(行、列) ;ES:BP=字符串地址 ;AL=顯示輸出方式 ; 0——字符串中只含顯示字符,其顯示屬性在BL中。顯示後,光標位置不變 ; 1——字符串中只含顯示字符,其顯示屬性在BL中。顯示後,光標位置改變 ; 2——字符串中含顯示字符和顯示屬性。顯示後,光標位置不變 ; 3——字符串中含顯示字符和顯示屬性。顯示後,光標位置改變 ;無返回值 mov sp, LOADER_BASE_ADDR mov bp, loadermsg ; ES:BP = 字符串地址 mov cx, 17 ; CX = 字符串長度 mov ax, 0x1301 ; AH = 13, AL = 01h mov bx, 0x001f ; 頁號為0(BH = 0) 藍底粉紅字(BL = 1fh) mov dx, 0x1800 ; int 0x10 ; 10h 號中斷 ;--------- 準備進入保護模式 --------- ;1 打開A20 ;2 加載gdt ;3 將cr0的pe位置1 ;----------------- 打開A20 ---------------- in al,0x92 or al,0000_0010B out 0x92,al ;----------------- 加載GDT ---------------- lgdt [gdt_ptr] ;----------------- cr0第0位置1 ---------------- mov eax, cr0 or eax, 0x00000001 mov cr0, eax ;jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水線,避免分支預測的影響,這種cpu優化策略,最怕jmp跳轉, jmp SELECTOR_CODE:p_mode_start ; 刷新流水線,避免分支預測的影響,這種cpu優化策略,最怕jmp跳轉, ; 這將導致之前做的預測失效,從而起到了刷新的作用。 [bits 32] p_mode_start: mov ax, SELECTOR_DATA mov ds, ax mov es, ax mov ss, ax mov esp,LOADER_STACK_TOP mov ax, SELECTOR_VIDEO mov gs, ax mov byte [gs:320], 'P' jmp $ ``` ## Compile ``` nasm -I inc/ -o ./out/mbr.bin mbr.S nasm -I inc/ -o ./out/loader.bin loader.S ``` ## Hard Disk Image ``` dd if=../code/out/mbr.bin of=./sr_hd60m.img bs=512 count=1 conv=notrunc dd if=../code/out/loader.bin of=./sr_hd60m.img bs=512 count=4 seek=2 conv=notrunc ``` ## Result ### Before Protected-mode ![截圖 2025-04-18 上午8.21.19](https://hackmd.io/_uploads/HkNVozkkex.png) ### Check GDT ![截圖 2025-04-18 上午10.02.25](https://hackmd.io/_uploads/H1RAGNy1ll.png) ### After Protected-mode ![截圖 2025-04-18 上午10.05.43](https://hackmd.io/_uploads/r1ysX41Jeg.png) # CPU 簡介 ## 管線化 ![截圖 2025-04-18 中午12.27.40](https://hackmd.io/_uploads/ryRCN8ykle.png) >[!Warning] CPU 遇到 jmp 指令,會直接清空 pipeline。 ## Descriptor Cache Registers >[!Warning] 重新載入 selector 就可以更新 Descriptor Cache Registers。 # 記憶體段的保護 根據 GDT 進行記憶體位址的檢查。