Chapter 5:向核心邁進 === :::info 這是讀書筆記 ![book](https://hackmd.io/_uploads/H1rFFZZAkx.jpg =30%x) 作者:鄭鋼 出版社:佳魁資訊股份有限公司 出版日期:2017/05/31 ::: --- # 取得實體記憶體資訊 >參考資料:[BIOS中斷呼叫](https://zh.wikipedia.org/zh-tw/BIOS%E4%B8%AD%E6%96%B7%E5%91%BC%E5%8F%AB) ![截圖 2025-04-18 下午6.59.08](https://hackmd.io/_uploads/Hky-Wnk1el.png) ## 利用 BIOS Interrupt 0x15/0xE820 取得實體記憶體 >[!Note] 檢查全部記憶體 利用資料結構(ARDS)來描述記憶體資訊,主要步驟如下: 1. 寫好呼叫前輸入的暫存器資訊。 2. 執行中斷呼叫 `int 0x15` 3. 檢查暫存器的結果,ARDS會存放在記憶體中。 ## 利用 BIOS Interrupt 0x15/0xE801 取得實體記憶體 >[!Note] 最大支援 4GB AX/BX : 單位是 1KB。 CX/DX : 單位是 64KB。 結果會放在暫存器中。 * 實際記憶體和檢測到的記憶體有 1MB 的差距,原因是 ISA 的緩衝區。 ## 利用 BIOS Interrupt 0x15/0xE88 取得實體記憶體 >[!Note] 最大支援 64MB 此中斷只會顯示 1MB 以上的記憶體,但不包含這 1MB。 AX : 單位是 1KB。 結果放在暫存器中。 ## 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 + 0x300 ;------------------------------------------------------------------------------- ;功能:讀取硬盤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 ``` >loader.S ```asm= %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR ;構建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 ; 同上 ; total_mem_bytes用於保存內存容量,以字節為單位,此位置比較好記。 ; 當前偏移loader.bin文件頭0x200字節,loader.bin的加載地址是0x900, ; 故total_mem_bytes內存中的地址是0xb00.將來在內核中咱們會引用此地址 total_mem_bytes dd 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;以下是定義gdt的指針,前2字節是gdt界限,後4字節是gdt起始地址 gdt_ptr dw GDT_LIMIT dd GDT_BASE ;人工對齊:total_mem_bytes4字節+gdt_ptr6字節+ards_buf244字節+ards_nr2,共256字節 ards_buf times 244 db 0 ards_nr dw 0 ;用於記錄ards結構體數量 loader_start: ;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 獲取內存布局 ------- xor ebx, ebx ;第一次調用時,ebx值要為0 mov edx, 0x534d4150 ;edx只賦值一次,循環體中不會改變 mov di, ards_buf ;ards結構緩沖區 .e820_mem_get_loop: ;循環獲取每個ARDS內存範圍描述結構 mov eax, 0x0000e820 ;執行int 0x15後,eax值變為0x534d4150,所以每次執行int前都要更新為子功能號。 mov ecx, 20 ;ARDS地址範圍描述符結構大小是20字節 int 0x15 jc .e820_failed_so_try_e801 ;若cf位為1則有錯誤發生,嘗試0xe801子功能 add di, cx ;使di增加20字節指向緩沖區中新的ARDS結構位置 inc word [ards_nr] ;記錄ARDS數量 cmp ebx, 0 ;若ebx為0且cf不為1,這說明ards全部返回,當前已是最後一個 jnz .e820_mem_get_loop ;在所有ards結構中,找出(base_add_low + length_low)的最大值,即內存的容量。 mov cx, [ards_nr] ;遍歷每一個ARDS結構體,循環次數是ARDS的數量 mov ebx, ards_buf xor edx, edx ;edx為最大的內存容量,在此先清0 .find_max_mem_area: ;無須判斷type是否為1,最大的內存塊一定是可被使用 mov eax, [ebx] ;base_add_low add eax, [ebx+8] ;length_low add ebx, 20 ;指向緩沖區中下一個ARDS結構 cmp edx, eax ;冒泡排序,找出最大,edx寄存器始終是最大的內存容量 jge .next_ards mov edx, eax ;edx為總內存大小 .next_ards: loop .find_max_mem_area jmp .mem_get_ok ;------ int 15h ax = E801h 獲取內存大小,最大支持4G ------ ; 返回後, ax cx 值一樣,以KB為單位,bx dx值一樣,以64KB為單位 ; 在ax和cx寄存器中為低16M,在bx和dx寄存器中為16MB到4G。 .e820_failed_so_try_e801: mov ax,0xe801 int 0x15 jc .e801_failed_so_try88 ;若當前e801方法失敗,就嘗試0x88方法 ;1 先算出低15M的內存,ax和cx中是以KB為單位的內存數量,將其轉換為以byte為單位 mov cx,0x400 ;cx和ax值一樣,cx用做乘數 mul cx shl edx,16 and eax,0x0000FFFF or edx,eax add edx, 0x100000 ;ax只是15MB,故要加1MB mov esi,edx ;先把低15MB的內存容量存入esi寄存器備份 ;2 再將16MB以上的內存轉換為byte為單位,寄存器bx和dx中是以64KB為單位的內存數量 xor eax,eax mov ax,bx mov ecx, 0x10000 ;0x10000十進制為64KB mul ecx ;32位乘法,默認的被乘數是eax,積為64位,高32位存入edx,低32位存入eax. add esi,eax ;由於此方法只能測出4G以內的內存,故32位eax足夠了,edx肯定為0,只加eax便可 mov edx,esi ;edx為總內存大小 jmp .mem_get_ok ;----------------- int 15h ah = 0x88 獲取內存大小,只能獲取64M之內 ---------- .e801_failed_so_try88: ;int 15後,ax存入的是以kb為單位的內存容量 mov ah, 0x88 int 0x15 jc .error_hlt and eax,0x0000FFFF ;16位乘法,被乘數是ax,積為32位.積的高16位在dx中,積的低16位在ax中 mov cx, 0x400 ;0x400等於1024,將ax中的內存容量換為以byte為單位 mul cx shl edx, 16 ;把dx移到高16位 or edx, eax ;把積的低16位組合到edx,為32位的積 add edx,0x100000 ;0x88子功能只會返回1MB以上的內存,故實際內存大小要加上1MB .mem_get_ok: mov [total_mem_bytes], edx ;將內存換為byte單位後存入total_mem_bytes處。 ;----------------- 準備進入保護模式 ------------------- ;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跳轉, ; 這將導致之前做的預測失效,從而起到了刷新的作用。 .error_hlt: ;出錯則掛起 hlt [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:160], '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 ![截圖 2025-06-09 下午4.56.54](https://hackmd.io/_uploads/HJ3QGQ47ee.png) # 記憶體分頁機制 ## 記憶體為何需要分頁? 允許實體位址不連續,增加記憶體管理的彈性,也避免不必要的與硬碟置換空間。 ## 一級分頁 * Register CR3 for page table address. * High 20 bits for index. * CR3 + index * 4 = page table physical address. * Get the Page table Entry (4 Bytes). * entry + Low 12 bits = physical address. >[!Tip]整個過程由硬體完成。 ## 二級分頁 為何需要二級分頁? 一級分頁表需要保留 4MB 給分頁表使用。二級分頁表只需要保留 4KB 給分頁目錄表,增加記憶體使用上的彈性,二級分頁表可以動態配置空間。 Page 5-28有錯誤,請參照:[OSDev wiki](https://wiki.osdev.org/Paging) ## 啟動分頁機制 1. 準備好分頁目錄表和分頁表。 2. 將分頁目錄表的實體位置寫入 Register CR3。 3. Register CR0 PG 的位置設置為 1。 ## 設計分頁 0-3 GB:User space 3-4 GB:Kernal space >[!Note] >PTE: page table entry >PDE: page directory entry 實作步驟: * 清空 PDE。 * 建立 PDE。 * 建立第一個 PTE:虛擬位址等於實體位址。 * 建立 kernel 相關的 PDE:總共 254 entry(1GB-4MB,4MB 預留給 page table 使用。) ## 重新載入 GDT * 更新顯示卡記憶體段。 * 更新stack pointer regrister(esp)。 ## 大哉問 ### GDT and Paging >[!Tip] the CPU does perform a check on the GDT before enabling paging https://wiki.osdev.org/Global_Descriptor_Table ![截圖 2025-06-11 晚上8.20.21](https://hackmd.io/_uploads/rJ9nElDmxl.png) ### Why two-level paging? https://www.youtube.com/watch?v=Z4kSOv49GNc <iframe width="560" height="315" src="https://www.youtube.com/embed/Z4kSOv49GNc?si=RzTNeY0Ec6FmeCG8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> ## Source Code >boot.inc ``` asm= ;------------- loader和kernel ---------- LOADER_BASE_ADDR equ 0x900 LOADER_START_SECTOR equ 0x2 KERNEL_BIN_BASE_ADDR equ 0x70000 KERNEL_IMAGE_BASE_ADDR equ 0x1500 KERNEL_START_SECTOR equ 0x9 PAGE_DIR_TABLE_POS equ 0x100000 ;二級頁目錄表,頁表放在內存中1M起始位置連續存放,盡可能簡單 ;-------------- 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 ;---------------- 頁表相關屬性 -------------- PG_P equ 1b PG_RW_R equ 00b PG_RW_W equ 10b PG_US_S equ 000b PG_US_U equ 100b ``` >loader.S ```asm= %include "boot.inc" section loader vstart=LOADER_BASE_ADDR LOADER_STACK_TOP equ LOADER_BASE_ADDR ;構建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 ; 同上 ; total_mem_bytes用於保存內存容量,以字節為單位,此位置比較好記。 ; 當前偏移loader.bin文件頭0x200字節,loader.bin的加載地址是0x900, ; 故total_mem_bytes內存中的地址是0xb00.將來在內核中咱們會引用此地址 total_mem_bytes dd 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;以下是定義gdt的指針,前2字節是gdt界限,後4字節是gdt起始地址 gdt_ptr dw GDT_LIMIT dd GDT_BASE ;人工對齊:total_mem_bytes4字節+gdt_ptr6字節+ards_buf244字節+ards_nr2,共256字節 ards_buf times 244 db 0 ards_nr dw 0 ;用於記錄ards結構體數量 loader_start: ;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 獲取內存布局 ------- xor ebx, ebx ;第一次調用時,ebx值要為0 mov edx, 0x534d4150 ;edx只賦值一次,循環體中不會改變 mov di, ards_buf ;ards結構緩沖區 .e820_mem_get_loop: ;循環獲取每個ARDS內存範圍描述結構 mov eax, 0x0000e820 ;執行int 0x15後,eax值變為0x534d4150,所以每次執行int前都要更新為子功能號。 mov ecx, 20 ;ARDS地址範圍描述符結構大小是20字節 int 0x15 jc .e820_failed_so_try_e801 ;若cf位為1則有錯誤發生,嘗試0xe801子功能 add di, cx ;使di增加20字節指向緩沖區中新的ARDS結構位置 inc word [ards_nr] ;記錄ARDS數量 cmp ebx, 0 ;若ebx為0且cf不為1,這說明ards全部返回,當前已是最後一個 jnz .e820_mem_get_loop ;在所有ards結構中,找出(base_add_low + length_low)的最大值,即內存的容量。 mov cx, [ards_nr] ;遍歷每一個ARDS結構體,循環次數是ARDS的數量 mov ebx, ards_buf xor edx, edx ;edx為最大的內存容量,在此先清0 .find_max_mem_area: ;無須判斷type是否為1,最大的內存塊一定是可被使用 mov eax, [ebx] ;base_add_low add eax, [ebx+8] ;length_low add ebx, 20 ;指向緩沖區中下一個ARDS結構 cmp edx, eax ;冒泡排序,找出最大,edx寄存器始終是最大的內存容量 jge .next_ards mov edx, eax ;edx為總內存大小 .next_ards: loop .find_max_mem_area jmp .mem_get_ok ;------ int 15h ax = E801h 獲取內存大小,最大支持4G ------ ; 返回後, ax cx 值一樣,以KB為單位,bx dx值一樣,以64KB為單位 ; 在ax和cx寄存器中為低16M,在bx和dx寄存器中為16MB到4G。 .e820_failed_so_try_e801: mov ax,0xe801 int 0x15 jc .e801_failed_so_try88 ;若當前e801方法失敗,就嘗試0x88方法 ;1 先算出低15M的內存,ax和cx中是以KB為單位的內存數量,將其轉換為以byte為單位 mov cx,0x400 ;cx和ax值一樣,cx用做乘數 mul cx shl edx,16 and eax,0x0000FFFF or edx,eax add edx, 0x100000 ;ax只是15MB,故要加1MB mov esi,edx ;先把低15MB的內存容量存入esi寄存器備份 ;2 再將16MB以上的內存轉換為byte為單位,寄存器bx和dx中是以64KB為單位的內存數量 xor eax,eax mov ax,bx mov ecx, 0x10000 ;0x10000十進制為64KB mul ecx ;32位乘法,默認的被乘數是eax,積為64位,高32位存入edx,低32位存入eax. add esi,eax ;由於此方法只能測出4G以內的內存,故32位eax足夠了,edx肯定為0,只加eax便可 mov edx,esi ;edx為總內存大小 jmp .mem_get_ok ;----------------- int 15h ah = 0x88 獲取內存大小,只能獲取64M之內 ---------- .e801_failed_so_try88: ;int 15後,ax存入的是以kb為單位的內存容量 mov ah, 0x88 int 0x15 jc .error_hlt and eax,0x0000FFFF ;16位乘法,被乘數是ax,積為32位.積的高16位在dx中,積的低16位在ax中 mov cx, 0x400 ;0x400等於1024,將ax中的內存容量換為以byte為單位 mul cx shl edx, 16 ;把dx移到高16位 or edx, eax ;把積的低16位組合到edx,為32位的積 add edx,0x100000 ;0x88子功能只會返回1MB以上的內存,故實際內存大小要加上1MB .mem_get_ok: mov [total_mem_bytes], edx ;將內存換為byte單位後存入total_mem_bytes處。 ;----------------- 準備進入保護模式 ------------------- ;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跳轉, ; 這將導致之前做的預測失效,從而起到了刷新的作用。 .error_hlt: ;出錯則掛起 hlt [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 ; 創建頁目錄及頁表並初始化頁內存位圖 call setup_page ;要將描述符表地址及偏移量寫入內存gdt_ptr,一會用新地址重新加載 sgdt [gdt_ptr] ; 存儲到原來gdt所有的位置 ;將gdt描述符中視頻段描述符中的段基址+0xc0000000 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xc0000000 ;視頻段是第3個段描述符,每個描述符是8字節,故0x18。 ;段描述符的高4字節的最高位是段基址的31~24位 ;將gdt的基址加上0xc0000000使其成為內核所在的高地址 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 將棧指針同樣映射到內核地址 ; 把頁目錄地址賦給cr3 mov eax, PAGE_DIR_TABLE_POS mov cr3, eax ; 打開cr0的pg位(第31位) mov eax, cr0 or eax, 0x80000000 mov cr0, eax ;在開啟分頁後,用gdt新的地址重新加載 lgdt [gdt_ptr] ; 重新加載 mov byte [gs:160], 'V' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:162], 'i' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:164], 'r' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:166], 't' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:168], 'u' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:170], 'a' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:172], 'l' ;視頻段段基址已經被更新,用字符v表示virtual addr jmp $ ;------------- 創建頁目錄及頁表 --------------- setup_page: ;先把頁目錄占用的空間逐字節清0 mov ecx, 4096 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir ;開始創建頁目錄項(PDE) .create_pde: ; 創建Page Directory Entry mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ; 此時eax為第一個頁表的位置及屬性 mov ebx, eax ; 此處為ebx賦值,是為.create_pte做準備,ebx為基址。 ; 下面將頁目錄項0和0xc00都存為第一個頁表的地址, ; 一個頁表可表示4MB內存,這樣0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的頁表, ; 這是為將地址映射為內核地址做準備 or eax, PG_US_U | PG_RW_W | PG_P ; 頁目錄項的屬性RW和P位為1,US為1,表示用戶屬性,所有特權級別都可以訪問. mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1個目錄項,在頁目錄表中的第1個目錄項寫入第一個頁表的位置(0x101000)及屬性(7) mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一個頁表項占用4字節,0xc00表示第768個頁表占用的目錄項,0xc00以上的目錄項用於內核空間, ; 也就是頁表的0xc0000000~0xffffffff共計1G屬於內核,0x0~0xbfffffff共計3G屬於用戶進程. sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最後一個目錄項指向頁目錄表自己的地址 ;下面創建頁表項(PTE) mov ecx, 256 ; 1M低端內存 / 每頁大小4k = 256 mov esi, 0 mov edx, PG_US_U | PG_RW_W | PG_P ; 屬性為7,US=1,RW=1,P=1 .create_pte: ; 創建Page Table Entry mov [ebx+esi*4],edx ; 此時的ebx已經在上面通過eax賦值為0x101000,也就是第一個頁表的地址 add edx,4096 ; edx inc esi loop .create_pte ;創建內核其它頁表的PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 ; 此時eax為第二個頁表的位置 or eax, PG_US_U | PG_RW_W | PG_P ; 頁目錄項的屬性US,RW和P位都為1 mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 ; 範圍為第769~1022的所有目錄項數量 mov esi, 769 .create_kernel_pde: mov [ebx+esi*4], eax inc esi add eax, 0x1000 loop .create_kernel_pde ret ``` ## 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 ![截圖 2025-06-15 下午5.03.59](https://hackmd.io/_uploads/Skbs2Z2mlg.png) ![截圖 2025-06-15 下午5.02.27](https://hackmd.io/_uploads/SJwunbnXgx.png) # 載入核心 ```c= int main(void) { while(1); return 0; } ``` ## 編譯連結 >手動連結 ``` x86_64-linux-gnu-gcc -m32 -c -o main.o main.c x86_64-linux-gnu-ld -m elf_i386 main.o -Ttext 0xc0001500 -e main -o kernel.bin ``` 執行結果 ![截圖 2025-06-19 清晨5.56.21](https://hackmd.io/_uploads/BJom83lEgg.png) ## ELF 解析 ``` struct Elf32_Ehdr { unsigned char e_ident[EI_NIDENT]; // ELF Identification bytes Elf32_Half e_type; // Type of file (see ET_* below) Elf32_Half e_machine; // Required architecture for this file (see EM_*) Elf32_Word e_version; // Must be equal to 1 Elf32_Addr e_entry; // Address to jump to in order to start program Elf32_Off e_phoff; // Program header table's file offset, in bytes Elf32_Off e_shoff; // Section header table's file offset, in bytes Elf32_Word e_flags; // Processor-specific flags Elf32_Half e_ehsize; // Size of ELF header, in bytes Elf32_Half e_phentsize; // Size of an entry in the program header table Elf32_Half e_phnum; // Number of entries in the program header table Elf32_Half e_shentsize; // Size of an entry in the section header table Elf32_Half e_shnum; // Number of entries in the section header table Elf32_Half e_shstrndx; // Sect hdr table index of sect name string table }; ``` ``` x86_64-linux-gnu-gcc -m32 -S -o main.S main.c ``` ![截圖 2025-06-19 清晨5.53.33](https://hackmd.io/_uploads/SyWKrhlNgl.png) ``` xxd -u -a -g 1 -s 0 -l 300 kernel.bin ``` ![截圖 2025-06-19 清晨5.51.22](https://hackmd.io/_uploads/HkH-SngVex.png) ``` readelf -e kernel.bin ``` ![截圖 2025-06-19 清晨6.34.47](https://hackmd.io/_uploads/ByHIJpeNlx.png) >[!Tip]Program header and Section header 的差別? Section header 用於描述 section;Program header 用於描述 segment。一個 segment 可以包含一個或多個 sections,相當於從程序執行的角度來看這些 sections。 ## 將核心載入記憶體 ### Hard Disk Design >[!Note] 1 block(sector) = 512 Byte = 0x200 | Address | Sector | Content | | -------- | -------- | -------- | | 0x0000_0000-0x0000_01FF | 0 | mbr.bin | | 0x0000_0200-0x0000_03FF | 1 | | | 0x0000_0400-0x0000_05FF | 2 | loader.bin | | 0x0000_0600-0x0000_07FF | 3 | loader.bin | | 0x0000_0800-0x0000_09FF | 4 | loader.bin | | 0x0000_0A00-0x0000_0BFF | 5 | | | 0x0000_0C00-0x0000_0DFF | 6 | | | 0x0000_0E00-0x0000_0FFF | 7 | | | 0x0000_1000-0x0000_11FF | 8 | | | 0x0000_1200-0x0000_13FF | 9 | kernel.bin | ### Memory Design | Address | Size | Content | | -------- | -------- | -------- | | 0x0000_0000-0x0000_03FF | 1K | Interrupt Vector Table | | 0x0000_0400-0x0000_04FF | 256B | BIOS Data Area | | 0x0000_0500-0x0000_08FF | 1K | buffer | 0x0000_0900-0x0000_15FF | 3K+256 | ==loader.bin== | 0x0000_1500-0x0000_7DFF | 512B | ==mbr.bin== | 0x0000_7E00-0x0009_FBFF | 608K | ==available== | 0x0009_FC00-0x0009_FFFF | 1K | EBDA | 0x000A_0000-0x000E_FFFF | 5\*64KB | 顯卡 BIOS | 0x000F_0000-0x000F_FFEF | 64KB-16B| BIOS | 0x000F_FFF0-0x000F_FFFF | 16B | BIOS entry ## Source Code :::spoiler boot.inc ```asm= ;------------- loader和kernel ---------- LOADER_BASE_ADDR equ 0x900 LOADER_STACK_TOP equ LOADER_BASE_ADDR LOADER_START_SECTOR equ 0x2 KERNEL_BIN_BASE_ADDR equ 0x70000 KERNEL_START_SECTOR equ 0x9 KERNEL_ENTRY_POINT equ 0xc0001500 ;------------- 頁表配置 ---------------- PAGE_DIR_TABLE_POS equ 0x100000 ;-------------- 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 ;---------------- 頁表相關屬性 -------------- PG_P equ 1b PG_RW_R equ 00b PG_RW_W equ 10b PG_US_S equ 000b PG_US_U equ 100b ;------------- program type 定義 -------------- PT_NULL equ 0 ``` ::: :::spoiler loader.S ```asm= %include "boot.inc" section loader vstart=LOADER_BASE_ADDR ;構建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 ; 同上 ; total_mem_bytes用於保存內存容量,以字節為單位,此位置比較好記。 ; 當前偏移loader.bin文件頭0x200字節,loader.bin的加載地址是0x900, ; 故total_mem_bytes內存中的地址是0xb00.將來在內核中咱們會引用此地址 total_mem_bytes dd 0 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;以下是定義gdt的指針,前2字節是gdt界限,後4字節是gdt起始地址 gdt_ptr dw GDT_LIMIT dd GDT_BASE ;人工對齊:total_mem_bytes4字節+gdt_ptr6字節+ards_buf244字節+ards_nr2,共256字節 ards_buf times 244 db 0 ards_nr dw 0 ;用於記錄ards結構體數量 loader_start: ;------- int 15h eax = 0000E820h ,edx = 534D4150h ('SMAP') 獲取內存布局 ------- xor ebx, ebx ;第一次調用時,ebx值要為0 mov edx, 0x534d4150 ;edx只賦值一次,循環體中不會改變 mov di, ards_buf ;ards結構緩沖區 .e820_mem_get_loop: ;循環獲取每個ARDS內存範圍描述結構 mov eax, 0x0000e820 ;執行int 0x15後,eax值變為0x534d4150,所以每次執行int前都要更新為子功能號。 mov ecx, 20 ;ARDS地址範圍描述符結構大小是20字節 int 0x15 jc .e820_failed_so_try_e801 ;若cf位為1則有錯誤發生,嘗試0xe801子功能 add di, cx ;使di增加20字節指向緩沖區中新的ARDS結構位置 inc word [ards_nr] ;記錄ARDS數量 cmp ebx, 0 ;若ebx為0且cf不為1,這說明ards全部返回,當前已是最後一個 jnz .e820_mem_get_loop ;在所有ards結構中,找出(base_add_low + length_low)的最大值,即內存的容量。 mov cx, [ards_nr] ;遍歷每一個ARDS結構體,循環次數是ARDS的數量 mov ebx, ards_buf xor edx, edx ;edx為最大的內存容量,在此先清0 .find_max_mem_area: ;無須判斷type是否為1,最大的內存塊一定是可被使用 mov eax, [ebx] ;base_add_low add eax, [ebx+8] ;length_low add ebx, 20 ;指向緩沖區中下一個ARDS結構 cmp edx, eax ;冒泡排序,找出最大,edx寄存器始終是最大的內存容量 jge .next_ards mov edx, eax ;edx為總內存大小 .next_ards: loop .find_max_mem_area jmp .mem_get_ok ;------ int 15h ax = E801h 獲取內存大小,最大支持4G ------ ; 返回後, ax cx 值一樣,以KB為單位,bx dx值一樣,以64KB為單位 ; 在ax和cx寄存器中為低16M,在bx和dx寄存器中為16MB到4G。 .e820_failed_so_try_e801: mov ax,0xe801 int 0x15 jc .e801_failed_so_try88 ;若當前e801方法失敗,就嘗試0x88方法 ;1 先算出低15M的內存,ax和cx中是以KB為單位的內存數量,將其轉換為以byte為單位 mov cx,0x400 ;cx和ax值一樣,cx用做乘數 mul cx shl edx,16 and eax,0x0000FFFF or edx,eax add edx, 0x100000 ;ax只是15MB,故要加1MB mov esi,edx ;先把低15MB的內存容量存入esi寄存器備份 ;2 再將16MB以上的內存轉換為byte為單位,寄存器bx和dx中是以64KB為單位的內存數量 xor eax,eax mov ax,bx mov ecx, 0x10000 ;0x10000十進制為64KB mul ecx ;32位乘法,默認的被乘數是eax,積為64位,高32位存入edx,低32位存入eax. add esi,eax ;由於此方法只能測出4G以內的內存,故32位eax足夠了,edx肯定為0,只加eax便可 mov edx,esi ;edx為總內存大小 jmp .mem_get_ok ;----------------- int 15h ah = 0x88 獲取內存大小,只能獲取64M之內 ---------- .e801_failed_so_try88: ;int 15後,ax存入的是以kb為單位的內存容量 mov ah, 0x88 int 0x15 jc .error_hlt and eax,0x0000FFFF ;16位乘法,被乘數是ax,積為32位.積的高16位在dx中,積的低16位在ax中 mov cx, 0x400 ;0x400等於1024,將ax中的內存容量換為以byte為單位 mul cx shl edx, 16 ;把dx移到高16位 or edx, eax ;把積的低16位組合到edx,為32位的積 add edx,0x100000 ;0x88子功能只會返回1MB以上的內存,故實際內存大小要加上1MB .mem_get_ok: mov [total_mem_bytes], edx ;將內存換為byte單位後存入total_mem_bytes處。 ;----------------- 準備進入保護模式 ------------------- ;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跳轉, ; 這將導致之前做的預測失效,從而起到了刷新的作用。 .error_hlt: ;出錯則掛起 hlt [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 ; ------------------------- 加載kernel ---------------------- mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇區號 mov ebx, KERNEL_BIN_BASE_ADDR ; 從磁盤讀出後,寫入到ebx指定的地址 mov ecx, 200 ; 讀入的扇區數 call rd_disk_m_32 ; 創建頁目錄及頁表並初始化頁內存位圖 call setup_page ;要將描述符表地址及偏移量寫入內存gdt_ptr,一會用新地址重新加載 sgdt [gdt_ptr] ; 存儲到原來gdt所有的位置 ;將gdt描述符中視頻段描述符中的段基址+0xc0000000 mov ebx, [gdt_ptr + 2] or dword [ebx + 0x18 + 4], 0xc0000000 ;視頻段是第3個段描述符,每個描述符是8字節,故0x18。 ;段描述符的高4字節的最高位是段基址的31~24位 ;將gdt的基址加上0xc0000000使其成為內核所在的高地址 add dword [gdt_ptr + 2], 0xc0000000 add esp, 0xc0000000 ; 將棧指針同樣映射到內核地址 ; 把頁目錄地址賦給cr3 mov eax, PAGE_DIR_TABLE_POS mov cr3, eax ; 打開cr0的pg位(第31位) mov eax, cr0 or eax, 0x80000000 mov cr0, eax ;在開啟分頁後,用gdt新的地址重新加載 lgdt [gdt_ptr] ; 重新加載 mov byte [gs:160], 'V' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:162], 'i' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:164], 'r' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:166], 't' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:168], 'u' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:170], 'a' ;視頻段段基址已經被更新,用字符v表示virtual addr mov byte [gs:172], 'l' ;視頻段段基址已經被更新,用字符v表示virtual addr ;;;;;;;;;;;;;;;;;;;;;;;;;;;; 此時不刷新流水線也沒問題 ;;;;;;;;;;;;;;;;;;;;;;;; ;由於一直處在32位下,原則上不需要強制刷新,經過實際測試沒有以下這兩句也沒問題. ;但以防萬一,還是加上啦,免得將來出來莫句奇妙的問題. jmp SELECTOR_CODE:enter_kernel ;強制刷新流水線,更新gdt enter_kernel: ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; mov byte [gs:320], 'k' ;視頻段段基址已經被更新 mov byte [gs:322], 'e' ;視頻段段基址已經被更新 mov byte [gs:324], 'r' ;視頻段段基址已經被更新 mov byte [gs:326], 'n' ;視頻段段基址已經被更新 mov byte [gs:328], 'e' ;視頻段段基址已經被更新 mov byte [gs:330], 'l' ;視頻段段基址已經被更新 mov byte [gs:480], 'w' ;視頻段段基址已經被更新 mov byte [gs:482], 'h' ;視頻段段基址已經被更新 mov byte [gs:484], 'i' ;視頻段段基址已經被更新 mov byte [gs:486], 'l' ;視頻段段基址已經被更新 mov byte [gs:488], 'e' ;視頻段段基址已經被更新 mov byte [gs:490], '(' ;視頻段段基址已經被更新 mov byte [gs:492], '1' ;視頻段段基址已經被更新 mov byte [gs:494], ')' ;視頻段段基址已經被更新 mov byte [gs:496], ';' ;視頻段段基址已經被更新 call kernel_init mov esp, 0xc009f000 jmp KERNEL_ENTRY_POINT ; 用地址0x1500訪問測試,結果ok ;----------------- 將kernel.bin中的segment拷貝到編譯的地址 ----------- kernel_init: xor eax, eax xor ebx, ebx ;ebx記錄程序頭表地址 xor ecx, ecx ;cx記錄程序頭表中的program header數量 xor edx, edx ;dx 記錄program header尺寸,即e_phentsize mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件42字節處的屬性是e_phentsize,表示program header大小 mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件開始部分28字節的地方是e_phoff,表示第1 個program header在文件中的偏移量 ; 其實該值是0x34,不過還是謹慎一點,這里來讀取實際值 add ebx, KERNEL_BIN_BASE_ADDR mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件開始部分44字節的地方是e_phnum,表示有幾個program header .each_segment: cmp byte [ebx + 0], PT_NULL ; 若p_type等於 PT_NULL,說明此program header未使用。 je .PTNULL ;為函數memcpy壓入參數,參數是從右往左依然壓入.函數原型類似於 memcpy(dst,src,size) push dword [ebx + 16] ; program header中偏移16字節的地方是p_filesz,壓入函數memcpy的第三個參數:size mov eax, [ebx + 4] ; 距程序頭偏移量為4字節的位置是p_offset add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加載到的物理地址,eax為該段的物理地址 push eax ; 壓入函數memcpy的第二個參數:源地址 push dword [ebx + 8] ; 壓入函數memcpy的第一個參數:目的地址,偏移程序頭8字節的位置是p_vaddr,這就是目的地址 call mem_cpy ; 調用mem_cpy完成段覆制 add esp,12 ; 清理棧中壓入的三個參數 .PTNULL: add ebx, edx ; edx為program header大小,即e_phentsize,在此ebx指向下一個program header loop .each_segment ret ;---------- 逐字節拷貝 mem_cpy(dst,src,size) ------------ ;輸入:棧中三個參數(dst,src,size) ;輸出:無 ;--------------------------------------------------------- mem_cpy: cld push ebp mov ebp, esp push ecx ; rep指令用到了ecx,但ecx對於外層段的循環還有用,故先入棧備份 mov edi, [ebp + 8] ; dst mov esi, [ebp + 12] ; src mov ecx, [ebp + 16] ; size rep movsb ; 逐字節拷貝 ;恢覆環境 pop ecx pop ebp ret ;------------- 創建頁目錄及頁表 --------------- setup_page: ;先把頁目錄占用的空間逐字節清0 mov ecx, 4096 mov esi, 0 .clear_page_dir: mov byte [PAGE_DIR_TABLE_POS + esi], 0 inc esi loop .clear_page_dir ;開始創建頁目錄項(PDE) .create_pde: ; 創建Page Directory Entry mov eax, PAGE_DIR_TABLE_POS add eax, 0x1000 ; 此時eax為第一個頁表的位置及屬性 mov ebx, eax ; 此處為ebx賦值,是為.create_pte做準備,ebx為基址。 ; 下面將頁目錄項0和0xc00都存為第一個頁表的地址, ; 一個頁表可表示4MB內存,這樣0xc03fffff以下的地址和0x003fffff以下的地址都指向相同的頁表, ; 這是為將地址映射為內核地址做準備 or eax, PG_US_U | PG_RW_W | PG_P ; 頁目錄項的屬性RW和P位為1,US為1,表示用戶屬性,所有特權級別都可以訪問. mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 第1個目錄項,在頁目錄表中的第1個目錄項寫入第一個頁表的位置(0x101000)及屬性(3) mov [PAGE_DIR_TABLE_POS + 0xc00], eax ; 一個頁表項占用4字節,0xc00表示第768個頁表占用的目錄項,0xc00以上的目錄項用於內核空間, ; 也就是頁表的0xc0000000~0xffffffff共計1G屬於內核,0x0~0xbfffffff共計3G屬於用戶進程. sub eax, 0x1000 mov [PAGE_DIR_TABLE_POS + 4092], eax ; 使最後一個目錄項指向頁目錄表自己的地址 ;下面創建頁表項(PTE) mov ecx, 256 ; 1M低端內存 / 每頁大小4k = 256 mov esi, 0 mov edx, PG_US_U | PG_RW_W | PG_P ; 屬性為7,US=1,RW=1,P=1 .create_pte: ; 創建Page Table Entry mov [ebx+esi*4],edx ; 此時的ebx已經在上面通過eax賦值為0x101000,也就是第一個頁表的地址 add edx,4096 inc esi loop .create_pte ;創建內核其它頁表的PDE mov eax, PAGE_DIR_TABLE_POS add eax, 0x2000 ; 此時eax為第二個頁表的位置 or eax, PG_US_U | PG_RW_W | PG_P ; 頁目錄項的屬性RW和P位為1,US為0 mov ebx, PAGE_DIR_TABLE_POS mov ecx, 254 ; 範圍為第769~1022的所有目錄項數量 mov esi, 769 .create_kernel_pde: mov [ebx+esi*4], eax inc esi add eax, 0x1000 loop .create_kernel_pde ret ;------------------------------------------------------------------------------- ;功能:讀取硬盤n個扇區 rd_disk_m_32: ;------------------------------------------------------------------------------- ; eax=LBA扇區號 ; ebx=將數據寫入的內存地址 ; ecx=讀入的扇區數 mov esi,eax ; 備份eax mov di,cx ; 備份扇區數到di ;讀寫硬盤: ;第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 ;;;;;;; 至此,硬盤控制器便從指定的lba地址(eax)處,讀出連續的cx個扇區,下面檢查硬盤狀態,不忙就能把這cx個扇區的數據讀出來 ;第4步:檢測硬盤狀態 .not_ready: ;測試0x1f7端口(status寄存器)的的BSY位 ;同一端口,寫時表示寫入命令字,讀時表示讀入硬盤狀態 nop in al,dx and al,0x88 ;第4位為1表示硬盤控制器已準備好數據傳輸,第7位為1表示硬盤忙 cmp al,0x08 jnz .not_ready ;若未準備好,繼續等。 ;第5步:從0x1f0端口讀數據 mov ax, di ;以下從硬盤端口讀數據用insw指令更快捷,不過盡可能多的演示命令使用, ;在此先用這種方法,在後面內容會用到insw和outsw等 mov dx, 256 ;di為要讀取的扇區數,一個扇區有512字節,每次讀入一個字,共需di*512/2次,所以di*256 mul dx mov cx, ax mov dx, 0x1f0 .go_on_read: in ax,dx mov [ebx], ax add ebx, 2 ; 由於在實模式下偏移地址為16位,所以用bx只會訪問到0~FFFFh的偏移。 ; loader的棧指針為0x900,bx為指向的數據輸出緩沖區,且為16位, ; 超過0xffff後,bx部分會從0開始,所以當要讀取的扇區數過大,待寫入的地址超過bx的範圍時, ; 從硬盤上讀出的數據會把0x0000~0xffff的覆蓋, ; 造成棧被破壞,所以ret返回時,返回地址被破壞了,已經不是之前正確的地址, ; 故程序出會錯,不知道會跑到哪里去。 ; 所以改為ebx代替bx指向緩沖區,這樣生成的機器碼前面會有0x66和0x67來反轉。 ; 0X66用於反轉默認的操作數大小! 0X67用於反轉默認的尋址方式. ; cpu處於16位模式時,會理所當然的認為操作數和尋址都是16位,處於32位模式時, ; 也會認為要執行的指令是32位. ; 當我們在其中任意模式下用了另外模式的尋址方式或操作數大小(姑且認為16位模式用16位字節操作數, ; 32位模式下用32字節的操作數)時,編譯器會在指令前幫我們加上0x66或0x67, ; 臨時改變當前cpu模式到另外的模式下. ; 假設當前運行在16位模式,遇到0X66時,操作數大小變為32位. ; 假設當前運行在32位模式,遇到0X66時,操作數大小變為16位. ; 假設當前運行在16位模式,遇到0X67時,尋址方式變為32位尋址 ; 假設當前運行在32位模式,遇到0X67時,尋址方式變為16位尋址. loop .go_on_read ret ``` ::: ## 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 dd if=../code/kernel/kernel.bin of=./sr_hd60m.img bs=512 count=200 seek=9 conv=notrunc ``` ## Result ![截圖 2025-06-25 上午10.52.59](https://hackmd.io/_uploads/SyspIyY4lx.png) # 特權等級 0:作業系統 1/2:系統程式 3:使用者程式 ## TSS 簡介 * TSS: Task State Segment * TSS 是一種資料結構,用於儲存工作的環境。 * 只有低特權等級到高特權等級才需要紀錄堆疊。 * TSS 是硬體支援的系統資料結構,由 TR 暫存器載入的。 * 當 CPU 從低特權往高特權跳轉時,會把當前低特權的堆疊存入目標高特權的堆疊中。 ## CPL 和 DPL 入門 * RPL : Selector 其中的一個欄位,準備要跳轉的位址。 * CPL : 當前的權限,存在 CS register 欄位中。 * DPL : 段描述符號所代表的記憶體區域的門檻許可權。 * Conforming code : 在轉移控制權到一個 conforming 的 segment 中的時候,還是維持呼叫者的 CPL,CPU 並沒有提權。 * Code section 有分 conforming/non-conforming;Data section 則都是 non-conforming。 >[!Note]可以搭配 GDT and LDT 一起了解 <iframe width="560" height="315" src="https://www.youtube.com/embed/EPHUBsTsvuQ?si=iAT3JLl2d7CmQNQm" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe> ## Gate >[!Note]ret : ring 0 -> ring 3 唯一一個可以從高特權跳轉低特權的方式。 低特權跳轉高特權有四種方式。 * Task Gate * Interrupt Gate * Trap Gate * Call Gate Call Gate 的說明 >[!Note] 這篇寫得很好~ >[利用Call Gate與TSS (Task-State Segment)實現特權等級的轉換](https://jasonblog.github.io/note/os/1417.html) ## RPL 存在的必要性 在 conforming 的情況下,可以保護高特權,因為縱使 CPL 變成高特權,RPL 也會保持在低特權。 * CPL 存在當前暫存器中。 * RPL 存在 selector 中。 * DPL 存在 段描述符中。 ## I/O 特權等級 * eflags:IOPL : 大開關,2 bits,用來表示 I/O privilege level(0-3)。 * I/O bit map : 小開關,65536 bits = 8KB,2。當 CPL 權限低於 IOPL 才有意義。 * bit map 最後用 0xFF 填充:確保至少含有一個 byte,CPU 讀取最少單位是一個 Byte。