Chapter 5:向核心邁進
===
:::info
這是讀書筆記

作者:鄭鋼
出版社:佳魁資訊股份有限公司
出版日期:2017/05/31
:::
---
# 取得實體記憶體資訊
>參考資料:[BIOS中斷呼叫](https://zh.wikipedia.org/zh-tw/BIOS%E4%B8%AD%E6%96%B7%E5%91%BC%E5%8F%AB)

## 利用 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

# 記憶體分頁機制
## 記憶體為何需要分頁?
允許實體位址不連續,增加記憶體管理的彈性,也避免不必要的與硬碟置換空間。
## 一級分頁
* 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

### 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


# 載入核心
```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
```
執行結果

## 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
```

```
xxd -u -a -g 1 -s 0 -l 300 kernel.bin
```

```
readelf -e kernel.bin
```

>[!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

# 特權等級
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。