作業系統核心載入流程圖
%include "boot6_3.inc"
section loader vstart=LOADER_BASE_ADDR ;loader寫入的地址
LOADER_STACK_TOP equ LOADER_BASE_ADDR
;------------------------------- 記憶體安排 --------------------------------
;建立虛擬機器時在build\bochsrc7_3檔案中設定的機器記憶體大小為megs: 32,即32MB
;生成的loader.bin寫入硬碟第2個扇區。第0個扇區是MBR,第1個扇區是空的未使用。
;BIOS呼叫mbr,mbr的地址是0x7c00,mbr呼叫loader,loader的地址是0x900。
;這兩個地址是固定的,也就是說,目前的方法是很不靈活的,呼叫方需要提前和被呼叫方約定呼叫地址。
;-------------------------------------------------------------------------------
; 程式名稱 | 磁碟扇區 | 實體記憶體起始地址
; mbr | 第0個 | 0x7c00
; loader | 第2個 | 0x900
; gdt | 第2個 | 0x900
; loader_start | 第2個 | 0xc00
; kernel | 第9個 | 0x70000
; kernel.segment | 第9個 | 0x1500
; 頁目錄表 | 程式碼生成 | 0x100000
; 棧頂esp | 程式碼生成 | LOADER_STACK_TOP = 0x900,啟動分頁後0xc0000900,最終0xc0000900
;-------------------------------------------------------------------------------
; 0x7c00 = 7*16*16*16 + 12*16*16 = 7*2^12 + 12*2^8 = 7*4k + 3*4*2^8 = 28k + 3k
;---------------------- 大小:4*8 + 60*8 = 32 + 480 = 512byte ---------------------
;dd是偽指令,意為define double-word,即定義雙字變數,一個字是2位元組,所以雙字就是4位元組資料。
;dd偽指令經編譯後會在loader.bin檔案中留出4位元組的空間儲存dd偽指令的運算元。
;構建gdt及其內部的描述符,將被載入到記憶體0x900處
GDT_BASE: dd 0x00000000 ; 程式編譯後的地址是從上到下越來越高的,所以,
dd 0x00000000 ; 下面用dd定義的資料地址要高於上面的dd所定義的資料地址
;段基址在8位元組的段描述符中存在3處,它們在每處都會是0。
CODE_DESC: dd 0x0000FFFF
dd DESC_CODE_HIGH4
;type欄位中的e要麼是0(向上擴充套件),要麼是1(向下擴充套件)。
DATA_STACK_DESC: dd 0x0000FFFF
dd DESC_DATA_HIGH4
; 段基址0-15位 段界限0-15位
;0x80000007 = 10000000_00000000_00000000_00000111
;0xb8000 = 00001011_10000000_00000000
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個描述符的空位, 60*8=480byte
;total_mem_bytes用於儲存記憶體容量,以位元組為單位,此位置比較好記
; 當前偏移loader.bin檔案頭0x200位元組,loader.bin的載入地址是0x900
; 故total_mem_bytes記憶體中的地址是0xb00,將來在核心中咱們會引用此地址
total_mem_bytes dd 0 ;4個位元組
; --------------------- 相當於宏,不計入到磁碟空間嗎? ------------------------
; 相當於(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
;0x0001 = 00000000_00000001; 0x0001<<3 = 00000000_00001000
SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ;全域性描述符表GDT中,第二個描述符也就是下標為1的描述符
;0x0002 = 00000000_00000010; 0x0002<<3 = 00000000_00010000
SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0 ;全域性描述符表GDT中,第三個描述符也就是下標為2的描述符
;0x0003 = 00000000_00000011; 0x0003<<3 = 00000000_00011000
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0 ; 同上
;將全域性描述符表的基地址和界限寫到此處,以下是gdt的指標,此處應該位於記憶體512 + 4 + 0x900 = 0xb04的位置。
;前2位元組是gdt界限,後4位元組是gdt起始地址。
;此載入器loader將描述符表載入到記憶體0x900處,那麼GDT_BASE的值=0x900處
gdt_ptr dw GDT_LIMIT
dd GDT_BASE
loadermsg db '2 loader in real.I am yangfan (:' ;32個位元組
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;人工對齊:total_mem_bytes 4 + gdt_ptr 6 + loadermsg 32 + ards_buf 212 + ards_nr 2 ,共256位元組
ards_buf times 212 db 0
ards_nr dw 0 ;用於記錄ARDS結構體數量
;--------------- 此處載入到的記憶體位置 768 + 0x900 = 0xc00 ---------------
; 512 + 4 + 6 + 32 + 212 + 2 = 768,上面的描述符表、輸出資訊、ARDS結構體等佔用了768個位元組。
;讓loader_start的地址剛好是0xc00,這都是安排好的。載入完後從mbr.s中跳到地址0xc00處執行函式loader_start。
;此函式功能有四: 1 檢測記憶體大小存入地址ards_buf處;2 載入全域性描述符;3 進入保護模式;4 顯示一些字元
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記憶體範圍描述結構
;所以每次執行int前都要更新為子功能號
mov eax, 0x0000e820 ;執行int 0x15後,eax值變為0x534d4150,
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暫存器中為低16MB,在bx和dx暫存器中為16MB到4GB
.e820_failed_so_try_e801:
mov ax,0xe801
int 0x15
jc .e801_failed_so_try88 ;若當前e801方法失敗,就嘗試0x88方法
;1 先算出低15MB的記憶體
; ax和cx中是以KB為單位的記憶體數量,將其轉換為以byte為單位
mov cx,0x400 ;cx和ax值一樣,cx用作乘數
mul cx ;固定的運算元AX
shl edx,16 ;16位運算元乘法,積的高16位在DX暫存器,低16位在AX暫存器
and eax,0x0000FFFF
or edx,eax ;將EDX左移16位後再與AX做或運算便得到了完整32位的積
add edx, 0x100000 ;ax只是15MB,故要加1MB
mov esi,edx ;先把低16MB的記憶體容量存入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,由於此方法只能測出4GB以內的記憶體,故32位eax足夠了
add esi, eax
mov edx, esi ;edx肯定為0,只加eax便可,edx為總記憶體大小
jmp .mem_get_ok
;----- int 15h ah = 0x88 獲取記憶體大小,只能獲取64MB之內 -----
.e801_failed_so_try88:
;int 15後,ax存入的是以KB為單位的記憶體容量
mov ah, 0x88
int 0x15
jc .e820_mem_get_failed
and eax,0x0000FFFF
mov cx, 0x400 ;0x400等於1024,將ax中的記憶體容量換為以byte為單位
mul cx ;16位乘法,被乘數是ax,積為32位。積的高16位在dx中;積的低16位在ax中
shl edx, 16 ;把dx移到高16位
or edx, eax ;把積的低16位組合到edx,為32位的積
add edx,0x100000 ;0x88子功能只會返回1MB以上的記憶體,故實際記憶體大小要加上1MB
.e820_mem_get_failed:
mov byte [gs:160], 'f'
mov byte [gs:162], 'a'
mov byte [gs:164], 'i'
mov byte [gs:166], 'l'
mov byte [gs:168], 'e'
mov byte [gs:170], 'd'
.mem_get_ok:
mov [total_mem_bytes], edx ;將記憶體換為byte單位後存入 total_mem_bytes 處
;------------------------------------------------------------
;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, 32 ; CX = 字串長度
mov ax, 0x1301 ; AH = 13, AL = 01h
mov bx, 0x001f ; 頁號為0(BH = 0) 藍底粉紅字(BL = 1fh)
mov dx, 0x1800 ; 將中斷號存入dx
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] ; gdt_ptr本身是個地址0xb04,所以要用中括號[]括起來,表示在地址處取值。
;----------------- cr0第0位置1 ----------------
mov eax, cr0
or eax, 0x00000001
mov cr0, eax
jmp dword SELECTOR_CODE:p_mode_start ; 重新整理流水線
;在loader中初始化全域性描述符表進入保護模式後,把段暫存器ds、es、ss都指定為相同的資料段選擇子啦
[bits 32]
p_mode_start: ; 將載入到記憶體0xd00處
mov ax, SELECTOR_DATA ;SELECTOR_DATA = 00000000_00010000 = 0x0010
mov ds, ax ;機器碼8ED8
mov es, ax
mov ss, ax
mov esp,LOADER_STACK_TOP ;LOADER_STACK_TOP = 0x900
mov ax, SELECTOR_VIDEO ;SELECTOR_VIDEO = 00000000_00011000 = 0x0018
mov gs, ax ;機器碼8EE8
;------------------------- 載入kernel映象 ----------------------
mov eax, KERNEL_START_SECTOR ; kernel.bin所在的扇區號
mov ebx, KERNEL_BIN_BASE_ADDR ; kernel.bin從磁碟讀出後,寫入到ebx指定的實體記憶體的地址0x70000 = 7*2^16 = 7*64k = 448k
mov ecx, 200 ; 讀入的扇區數
call rd_disk_m_32 ;載入核心原始檔,eax、ebx、ecx是函式rd_disk_m_32的三個引數,為呼叫下面的函式做準備。
mov byte [gs:160], 'T'
;-------------------------- 啟用分頁的三步曲 -------------------------
; 第一步:呼叫setup_page,建立頁目錄及頁表並初始化頁記憶體點陣圖, 之後才能重新載入lgdt [gdt_ptr]
call setup_page
;透過sgdt指令把GDT放在核心的地址空間,將GDT的起始地址和偏移量資訊dump(像倒水一樣)出來,依然存放到gdt_ptr處
sgdt [gdt_ptr] ;一會兒待條件成熟時,我們再從地址gdt_ptr處重新載入GDT。
mov ebx, [gdt_ptr + 2] ;從gdt_ptr中取出高4位元組的描述符表GDT的起始地址
;影片段是第3個段描述符,每個描述符是8位元組,故0x18 = 24。段描述符的高4位元組的最高位是段基址的第31~24位
;這裡要將段描述符的基地址修改為3GB以上,所以在原有地址的基礎上要加上0xc0000000。
;段描述符中記錄段基址最高位的部分是在段描述符的高4位元組的最高1位元組,所以ebx不僅要加上0x18,還要加上0x4。
;為了省事,我們直接將整個4位元組做或運算。最後就是第157行的指令“or dword [ebx + 0x18 + 4], 0xc0000000”
;
; 0xc00b8000 = 1100_0000_00|00_1011_1000|0000_0000_0000
;頁部件會根據1100_0000_00去頁目錄表中下標為768的頁目錄項中取出頁表地址,再根據00_1011_1000, 1011_1000 = 184
;到該頁表中下標為0xB8=184的頁表項中取出頁實體地址+(加上)0000_0000_0000,
;得到虛擬地址0xc00b8000對映的實體地址
or dword [ebx + 0x18 + 4], 0xc0000000 ;將gdt描述符中影片段描述符中的段基址 + 0xc0000000 = 0xc00b8000
;修改完了視訊記憶體段描述符後,現在可以修改GDT基址啦,我們把GDT也移到核心空間中。
;描述符表gdt在記憶體地址0x900處,將加上0xc0000000使其成為核心所在的高地址, 0xc0000000 = 3G
;0xc0000000 = 1100_0000_00|00_0000_0000|0000_0000_0000
;頁部件會根據1100_0000_00去頁目錄表中下標為768的頁目錄項中
add dword [gdt_ptr + 2], 0xc0000000 ; 此時描述符表GDT的地址是虛擬地址0xc0000900
; 全域性描述符表GDT的起始地址是0x900往上增長的,esp棧幀是往下增長的,所以不會覆蓋全域性描述符表。
add esp, 0xc0000000 ; 將棧指標同樣對映到核心地址, 0x900 + 0xc0000000
;第二步:
;將頁目錄表實體地址賦值給cr3暫存器,分頁機制開啟前要將頁表地址載入到控制暫存器cr3中
mov eax, PAGE_DIR_TABLE_POS ; PAGE_DIR_TABLE_POS = 0x100000 = 1*2^20 = 1M
mov cr3, eax
;第三步:
;開啟cr0的pg位(第31位),將頁目錄表實體地址賦值給cr3暫存器後,隨後啟用cr0暫存器的pg位。
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
;在開啟分頁後,用gdt新的地址重新載入
lgdt [gdt_ptr] ; 重新載入
;在分頁後,GDT的基址會變成3GB之上的虛擬地址,視訊記憶體段基址也變成了3GB這上的虛擬地址。
;影片段段基址已經被更新,列印字元virtual Addr
mov byte [gs:160], 'V'
mov byte [gs:162], 'i'
mov byte [gs:164], 'r'
mov byte [gs:166], 't'
mov byte [gs:168], 'u'
mov byte [gs:170], 'a'
mov byte [gs:172], 'l'
mov byte [gs:174], ' '
mov byte [gs:176], 'A'
mov byte [gs:178], 'd'
mov byte [gs:180], 'd'
;--------------------- 此時不重新整理流水線也沒問題 ------------------
;由於一直處在32位下,原則上不需要強制重新整理
;經過實際測試沒有以下這兩句也沒問題
;但以防萬一,還是加上啦,免得將來出來莫名其妙的問題
jmp SELECTOR_CODE:enter_kernel ;強制重新整理流水線,更新gdt
enter_kernel:
call kernel_init ;初始化核心
;在進入核心之後,棧也要重新規劃,棧起始地址不能再用之前的0xc0000900啦。為了方便編寫程式,0x900 = 9 * 16^2 = 9 * 2^8
;我們在進入核心前將棧指標改成我們期待的值,我們將esp改成了0xc009f000。
;0xc09f000 = 12*2^24 + 9*2^16 + 15*2^12,9f000 = 636KB = 159 * 4KB = 159頁,
mov esp, 0xc009f000
mov byte [gs:184], 'r'
; 跳到核心0xc0001500執行,此時已開啟了分頁。0xc0001500 = 12*2^28 + 1*2^12 + 5*2^8 = 3G + 8K + 1K + 256
jmp KERNEL_ENTRY_POINT
jmp $ ; 透過死迴圈使程式懸停在此
; 0x10 = 16, 0x100 = 256, 0x1000 = 4k, 0x10000 = 64k, 0x100000 = 1m
;------------- setup_page函式中建立頁目錄及頁表 ---------------
;先把頁目錄佔用的空間逐位元組清0,再在0x10000 = 1M 地址的地方建立頁目錄表
setup_page:
mov ecx, 4096
mov esi, 0
.clear_page_dir: ; 清空4k記憶體空間
mov byte [PAGE_DIR_TABLE_POS + esi], 0 ; PAGE_DIR_TABLE_POS = 0x100000 = 1MB
inc esi ; 將暫存器esi加1
loop .clear_page_dir ; 迴圈4096次
;開始建立頁目錄項(PDE)
.create_pde: ; 建立Page Directory Entry
mov eax, PAGE_DIR_TABLE_POS
; 此時eax為第一個頁表的位置及屬性, 0x1000 = 1*16^3 = 1*(2^12) = 4096 = 4KB
; eax = eax + 0x1000 = 0x101000,就是說頁目錄表起始地址是0x100000佔用4k,0x101000就是第一個頁表的起始地址。
add eax, 0x1000
mov ebx, eax ; 此處為ebx賦值,是為.create_pte做準備,ebx為基址
;下面將頁目錄項0和0xc00都存為第一個頁表的地址,每個頁表表示1024*4KB=4MB記憶體
;這樣虛擬地址0xc0000000 ~ 0xc03fffff之間共4M的地址空間和虛擬地址0x0 ~ 0x003fffff之間的地址都指向相同的頁表
;這是為將地址對映為核心地址做準備。0xc00 = 12*16^2 = 2^2*3*2^8 = 3*2^10 = 3072, 3072/4 = 768
;頁目錄項的屬性RW和P位為1,US為1,表示使用者屬性,所有特權級別都可以訪問
or eax, PG_US_U | PG_RW_W | PG_P
; 頁目錄項代表一個頁表,也就是說,下標為0和下標為768這兩個頁目錄項都是指向同一個頁表。
; 它們共同所指向的這個頁表地址是0x101000,它將來要指向的實體地址範圍是0~0xfffff,只是1MB的空間,其餘3MB並未分配。
mov [PAGE_DIR_TABLE_POS + 0x0], eax ;將第1個頁表(也就是頁目錄表)的地址放入第一個頁目錄項中
;第768(核心空間的第一個)個頁目錄項與第一個頁目錄項相同,這樣第一個和768個都指向低端4MB空間
; 0xc00表示第768個頁表佔用的目錄項,0xc00 = 768 * 4 以上的目錄項用於核心空間
;也就是頁表的0xc0000000~0xffffffff共計1G屬於核心
mov [PAGE_DIR_TABLE_POS + 0xc00], eax
;一個頁表項佔用4位元組
;0x0~0xbfffffff共計3G屬於使用者程序
;在頁目錄的最後一個頁目錄項中寫入頁表自己的實體地址。
;
;也許您在想,為什麼使用屬性PG_US_U,而不是PG_US_S?原因是這樣的,PG_US_U和PG_US_S是PDE或PTE的屬性,
;它用來限制某些特權級的任務對此記憶體空間的訪問(無論該記憶體空間中存放的是指令,還是普通資料)。PG_US_U表示PDE或PTE的US位為1,
;這說明處理器允許任意4個特權級的任務都可以訪問此PDE或PDE指向的記憶體。
;PG_US_S表示PDE或PTE的US位為0,這說明處理器允許除特權級3外的其他特權級任務訪問此PDE或PDE指向的記憶體。此時若使用
;屬性PG_US_S也沒問題,不過將來會實現init程序,它是使用者級程式,而它位於核心地址空間,也就是說將來會在特權級3的情
;況下執行init,這會訪問到核心空間,這就是此處用屬性PG_US_U的目的。
sub eax, 0x1000 ; eax = eax - 0x1000 = 0x100007
; 使最後一個目錄項指向頁目錄表自己的地址
mov [PAGE_DIR_TABLE_POS + 4092], eax
;在第一個頁表中建立256個頁表項(PTE), 第一個頁表的實體地址為0x101000 = 1M零4K
mov ecx, 256 ;建立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
; edx是能對映到4M實體地址的關鍵,第一個頁表項指向4k實體地址空間,
;第一個頁表項(也就是下標為0的頁表項)指向的普通物理頁起始地址是0x0000,地址範圍是0x0000 ~ 0x3fff
;第二個頁表項指向的普通物理頁起始地址是0x4000,地址範圍是0x4000 ~ 0x4fff,以此類推。
;448k / 4k = 111,111 * 4k = 6f000, 768 * 4M = 0xc0000000
;核心起始實體地址是0x70000 = 448k,剛好下標為111的頁表項指向它。因此,可以透過虛擬地址0x0006fxxx和0xc006fxxx訪問到。
mov [ebx + esi * 4], edx ;此時的ebx已經在上面透過eax賦值為0x101000,也就是第一個頁表的地址
add edx,4096 ; 4096 = 0x1000 = 1 0000 0000 0000,頁表項每增加一個,指向的框實體地址增加4k
inc esi
loop .create_pte
;建立核心其他頁表的PDE,從第769個頁目錄項開始建立並指向相應的頁表地址
mov eax, PAGE_DIR_TABLE_POS
add eax, 0x2000 ; 此時eax為第二個頁表的位置, 地址為0x102000 = 1M零8K
or eax, PG_US_U | PG_RW_W | PG_P ; 頁目錄項的屬性US、RW和P位都為1
mov ebx, PAGE_DIR_TABLE_POS
; 範圍為第769~1022的所有目錄項數量,最後一個頁目錄項指向的頁表地址是0x100000,也就是指向自己所在的頁目錄表的起始地址
mov ecx, 254
mov esi, 769
.create_kernel_pde: ; 建立核心頁目錄項,254 + 769 = 1023
mov [ebx + esi * 4], eax
inc esi
add eax, 0x1000 ;頁目錄項每增加一個,指向的頁表實體地址增加4k
loop .create_kernel_pde ;迴圈0x944 = 2372次
ret
;--------------------------------- 核心載入位置說明 -------------------------------
;將來核心肯定是越來越大,為了多預留出生長空間,咱們要將核心檔案kernel.bin載入到地址較高的空間,而核心映像要放置到較低的地址。
;核心檔案經過loader解析後就沒用啦,這樣核心映像將來往高地址處擴充套件時,也可以覆蓋原來的核心檔案kernel.bin。
;所以咱們的結論是在0x7e00~0x9fbff這片區域的高地址中找一畝地給kernel.bin,這裡我擅自做主啦,幫大家選的是0x70000。
;為什麼?隨意選的,取了個整而已,就是覺得0x70000~0x9fbff有0x2fbff=190KB位元組的空間,而我們的核心不超過100KB,夠用就行。
;-------------------------------------------------------------------------------
;功能:讀取硬碟n個扇區
; eax=LBA扇區號
; bx=將資料寫入的記憶體地址
; cx=讀入的扇區數
;-------------------------------------------------------------------------------
; kernel.bin位於第9扇區
rd_disk_m_32: ;將載入到0xe50處
mov byte [gs:160], 'V'
;eax、ebx、ecx在p_mode_start函式中rd_disk_m_32呼叫之前就設定好了。
mov esi, eax ;備份eax
mov di, cx ;備份cx
;讀寫硬碟:
;第1步:設定要讀取的扇區數
mov dx, 0x1f2 ; out指令中dx暫存器是用來儲存埠號的
mov al, cl
out dx, al ;讀取的扇區數
mov eax, esi ;恢復ax
;第2步:將LBA地址存入0x1f3 ~ 0x1f6
mov dx, 0x1f3 ;LBA地址7~0位寫入埠0x1f3
out dx, al
mov cl, 8
shr eax, cl
mov dx, 0x1f4 ;LBA地址15~8位寫入埠0x1f4
out dx, al
shr eax, cl
mov dx, 0x1f5 ;LBA地址23~16位寫入埠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 ; di為要讀取的扇區數,一個扇區有512位元組,每次讀入一個字
mov dx, 256 ; 共需di*512/2次,所以di*256
mul dx
mov cx, ax
mov dx, 0x1f0
.go_on_read:
in ax, dx ; 將埠中的資料讀入到ax中
; 由於在真實模式下偏移地址為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位定址.
mov [ebx], ax ;寫入到記憶體0x70000處,ebx在p_mode_start函式中設定為了0x70000
add ebx, 2 ; ax暫存器大小為2位元組,所以這裡+2
loop .go_on_read
mov byte [gs:186], 'A'
ret
5.3.3 elf格式的二進位制檔案
程式中最重要的部分就是段(segment)和節(section)。
段和節的資訊也是用header來描述的,程式頭是program header,節頭是section header。
程式中段的大小和數量是不固定的,節的大小和數量也不固定,因此需要為它們專門找個資料結構來描述它們,這個描述結構就是程式頭表(program header table)和節頭表(section header table)。
既然程式頭表和節頭表都稱為表,這說明裡面儲存的是多個程式頭program header和多個節頭section header的資訊,故這兩個表相當於陣列,陣列元素分別是程式頭program header和節頭section header。
程式頭表(program header table)中的元素全是程式頭(program header),而節頭表(section header table)中的元素全是節頭(section header)。元素全是單一的,不會在程式頭表中存在節頭資訊。
在表中,每個成員(陣列元素)都統稱為條目,即entry,一個條目代表一個段或一個節的頭描述資訊。
對於程式頭表,它本質上就是用來描述段(segment)的,所以也可以稱它為段頭表。
由於程式中段和節的數量不固定,程式頭表和節頭表的大小自然也就不固定了,而且各表在程式檔案中的儲存順序自然也要有個先後,故這兩個表在檔案中的位置也不會固定。因此,必須要在一個固定的位置,用一個固定大小的資料結構來描述程式頭表和節頭表的大小及位置資訊,這個資料結構便是ELF header,它位於檔案最開始的部分,並具有固定大小,一會兒咱們看elf header的資料結構就知道了。
ELF檔案格式依然分為檔案頭和檔案體兩部分,elf的任何定義,包括變數、常量及取值範圍,都可在Linux系統的/usr/include/elf.h中找到。
表 5-8 elf header 中的資料型別
資料型別名稱 | 位元組大小 | 對 齊 | 意 義 |
---|---|---|---|
Elf32_Half | 2 | 2 | 無符號中等大小的整數 |
Elf32_Word | 4 | 4 | 無符號大整數 |
Elf32_Addr | 4 | 4 | 無符號程式執行地址 |
Elf32_Off | 4 | 4 | 無符號的檔案偏移量 |
程式頭表告訴系統如何建立一個程序映像.它是從載入執行的角度來看待elf檔案.從它的角度看.
elf檔案被分成許多段,elf檔案中的程式碼、連結資訊和註釋都以段的形式存放。每個段都在程式
頭表中有一個表項描述,包含以下屬性:段的型別,段的駐留位置相對於檔案開始處的偏移,
段在記憶體中的首位元組地址,段的實體地址,段在檔案映像中的位元組數.段在記憶體映像中的位元組數,
段在記憶體和檔案中的對齊標記。可用"readelf -l filename"察看程式頭表中的內容。
elf檔案頭與程式頭表的結構如下:
typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off
typedef uint16_t Elf32_Half;;
typedef struct elf32_hdr{
//Magic number和其它資訊 7f45 4c46 0101 0100 0000 0000 0000 0000
unsigned char e_ident[EI_NIDENT];
Elf32_Half e_type; //目標檔案型別 0200
Elf32_Half e_machine; //硬體平臺 0300
Elf32_Word e_version; //elf頭部版本 0100 0000
Elf32_Addr e_entry; //程式進入點 0015 00c0
Elf32_Off e_phoff; //程式頭表偏移量 3400 0000
Elf32_Off e_shoff; //節頭表偏移量 805a 0100
Elf32_Word e_flags; //處理器特定標誌 0000 0000
Elf32_Half e_ehsize; //elf頭部長度 3400
Elf32_Half e_phentsize; //程式頭表中一個條目的長度 2000
Elf32_Half e_phnum; //程式頭表條目數目 0300
Elf32_Half e_shentsize; //節頭表中一個條目的長度 2800
Elf32_Half e_shnum; //節頭表條目個數 0a00
Elf32_Half e_shstrmdx; //節頭表字元索引 0900
}Elf32_Ehdr;
typedef struct elf32_phdr{
Elf32_Word p_type; //段的型別
Elf32_Off p_offset; //段的位置相對於檔案開始處的偏移
Elf32_Addr p_vaddr; //段在記憶體中的首位元組地址
Elf32_Addr p_paddr; //段的實體地址
Elf32_Word p_filesz; //段在檔案映像中的位元組數
Elf32_Word p_memsz; //段在記憶體映像中的位元組數
Elf32_Word p_flags; //段的標記
Elf32_Word p_align; //段在記憶體中的對齊標記
)Elf32_Phdr;
kernel.bin中的elf頭和程式頭表資料
p_vaddr + p_filesz = 0xc0001000 + 0x11ed0 = 0xc0012ed0
程式的起始地址是 0xc0001500,該段又是程式碼段(從該段的段標誌 p_flags 值為 5 看出:可讀可執行)這說明該段的實際程式碼長度是:
0xc0012ed0 - 0xc0001500 = 0x119D0 = 72144
位元組。
0x119D0 = 1*2^16 + 1*2^12 + 9*2^8 + 13*16 = 64k + 4k + 2k+256 + 208 = 70k + 464
第二行p_offset - 第一行p_filesz = 0x12000 - 0x119D0 = 0x130
00119a0: 41c5 0c04 0400 0000 2800 0000 181e 0000 A.......(.......
00119b0: 348e ffff ea04 0000 0041 0e08 8502 420d 4........A....B.
00119c0: 0548 8703 8304 03dc 04c3 41c7 41c5 0c04 .H........A.A...
00119d0: 0400 0000 2800 0000 441e 0000 f292 ffff ....(...D.......
00119e0: 1402 0000 0041 0e08 8502 420d 0548 8703 .....A....B..H..
00119f0: 8304 0306 02c3 41c7 41c5 0c04 0400 0000 ......A.A.......
0011a00: 1c00 0000 701e 0000 da94 ffff 2b00 0000 ....p.......+...
0011a10: 0041 0e08 8502 420d 0567 c50c 0404 0000 .A....B..g......
0011a20: 1c00 0000 901e 0000 e594 ffff c900 0000 ................
0011a30: 0041 0e08 8502 420d 0502 c5c5 0c04 0400 .A....B.........
有個猜測:第一個程式段包含elf檔案頭和程式頭表以及,第一個程式段的程式碼段。也就是說從kernel.bin檔案開頭0位元組到偏移0x119d0處。
起始虛擬地址只是個對應於記憶體中的地址,程式在記憶體中才用得著它,現在我們透過虛擬地址來計算它在檔案內的位置,也就是需要將其轉換成在檔案中的偏移量。
- 程式的入口虛擬地址 e_entry 是0xc0101500。
- 第一個段的起始虛擬地址 p_vaddr 為 0xc0001000。
- 並且第一個段在檔案內的偏移量為 0,參見上圖p_offset欄位。
故,起始虛擬地址 e_entry 對應在檔案中的偏移量為 0xc0001500 - 0xc0001000 + 0 = 0x500。將其換算成十進位制為
1280。還是小心起見,這個偏移量肯定不能超過檔案大小。
那麼,來看看從bernel.bin檔案頭偏移0x500的位置程式碼是什麼。
00004d0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00004e0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
00004f0: 0000 0000 0000 0000 0000 0000 0000 0000 ................
0000500: 5589 e583 e4f0 83ec 10c7 0424 5cdf 00c0 U..........$\...
0000510: e81b 0f00 00e8 dd02 0000 e8fc 0f00 00c7 ................
0000520: 0424 69df 00c0 e816 3900 00e8 8006 0000 .$i.....9.......
0000530: ebfe 5589 e583 ec28 e862 4800 0098 8945 ..U....(.bH....E
0000540: f48b 45f4 8944 2404 c704 247e df00 c0e8 ..E..D$...$~....
0000550: ab4e 0000 837d f400 742b 8d45 ec89 0424 .N...}..t+.E...$
0x500處前3位元組是pushl%ebp的機器碼55、movl %esp,%ebp的機器碼89e5。
核心被載入到記憶體後,loader還要透過分析其 elf 結構將其展開到新的位置,所以說,核心在記憶體中有兩份複製,一份是 elf 格式的原檔案 kernel.bin,另一份是 loader 解析 elf 格式的 kernel.bin 後在記憶體中生成的核心映像(也就是將程式中的各種段 segment 複製到記憶體後的程式體),這個映像才是真正執行的核心。
5.3.5 將核心載入記憶體
函式 kernel_init 的作用是將 kernel.bin 中的段( segment)複製到各段自己被編譯的虛擬地址處。
核心被載入到記憶體後,loader還要透過分析其elf結構將其展開到新的位置,所以說,核心在記憶體中有兩份複製,一份是elf格式的原檔案kernel.bin,另一份是loader解析elf格式的kernel.bin後在記憶體中生成的核心映像(也就是將程式中的各種段segment複製到記憶體後的程式體),這個映像才是真正執行的核心。
核心檔案kernel.bin是elf格式的二進位制可執行檔案,初始化核心就是根據elf規範將核心檔案中的段(segment)展開到(複製到)記憶體中的相應位置。
預計loader.bin的大小不會超過2000位元組。所以可選的起始實體地址是0x900+2000=0x10d0(不要把注意力放在這個奇怪的數上,偶然得出的)。
記憶體很大,但也儘量往低了選,於是湊了個整數,選了0x1500作為核心映像的入口地址。根據咱們的頁表,低端1MB的虛擬記憶體與實體記憶體是一一對應的,所以實體地址是0x1500,對應的虛擬地址是0xc0001500。
對於可執行程式,我們只對其中的段(segment)感興趣,它們才是程式執行的實質指令和資料的所在地,所以我們要找出程式中所有的段。
當呼叫kernel_init函式時,當時的棧指標是0xc00008fc,KERNEL_BIN_BASE_ADDR = 0x70000
初始化核心:需要在分頁後,將載入進來的elf核心檔案安置到相應的虛擬記憶體地址,然後跳過去執行。
為什麼不是0xc0070000呢?因為下標為0的頁目錄項和下標為768的頁目錄項指向的是同一個頁表。
核心在函式rd_disk_m_32中已經被載入到KERNEL_BIN_BASE_ADDR地址處即0x70000處,該處是檔案頭elf_header。
;---------------------- 將kernel.bin中的segment複製到編譯的地址 -----------------------
kernel_init:
xor eax, eax ;xor如果兩個值不相同,則異或結果為1;如果兩個值相同,則異或結果為0。
xor ebx, ebx ;ebx記錄程式頭表地址
xor ecx, ecx ;cx記錄程式頭表中的program header數量
xor edx, edx ;dx 記錄program header尺寸,即e_phentsize
; 偏移檔案42位元組處的屬性是e_phentsize,表示program header大小
mov dx, [KERNEL_BIN_BASE_ADDR + 42]
;表示第1 個program header在檔案中的偏移量,其實該值是0x34,不過還是謹慎一點,這裡來讀取實際值
mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移檔案開始部分28位元組的地方是e_phoff
;由於此時的ebx還只是儲存著程式頭表在檔案內的偏移量,所以要將其加上核心的載入地址,這樣才是程式頭表的實體地址。
add ebx, KERNEL_BIN_BASE_ADDR ; e_phoff 的值加上KERNEL_BIN_BASE_ADDR, 0x34 + 0x70000
; 偏移檔案開始部分44位元組的地方是e_phnum,表示有幾個program header也就是段的數量
mov cx, [KERNEL_BIN_BASE_ADDR + 44]
.each_segment:
;PT_NULL是在boot/include/boot.inc中定義的宏,其值為0,該意義表示空段型別。
; (PT_NULL也可以在Linux系統的/usr/include/elf.h中找到其定義:#define PT_NULL 0。)
cmp byte [ebx + 0], PT_NULL ; 若p_type等於 PT_NULL,說明此program header未使用
je .PTNULL ;如果發現該段是空段型別的話,就跨過該段不處理,跳到.PTNULL處
;---------------為函式memcpy壓入引數,引數是從右往左依然壓入。函式原型類似於 memcpy(dst,src,size)----------------
;壓入函式memcpy的第三個引數:size
push dword [ebx + 16] ; program header中偏移16位元組的地方是p_filesz。
;壓入函式memcpy的第二個引數:src
mov eax, [ebx + 4] ; 取出p_offse的值。一般程式頭表中的第一個表項的p_offset=0x0000 0000。
add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被載入到的實體地址0x70000,eax為該段的實體地址
push eax
;壓入函式memcpy的第一個引數:dst,偏移程式頭8位元組的位置是p_vaddr = 0xc000 1000,這就是目的地址。
push dword [ebx + 8]
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 ;控制重複執行字串指令時的[e]si 和[e]di的遞增方式,為movs[bwd]服務
push ebp ;push操作的原理是先進行sub esp,4,再mov dword [esp],運算元,所以棧底處是空的
mov ebp, esp
push ecx ; rep指令用到了ecx。但ecx對於外層段的迴圈還有用,故先入棧備份
;棧中地址0xc00008f8處的內容是提供給函式mem_cpy的第三個引數,即size。
;地址較低的0xc00008f4處是它的第二個引數,即src地址,0xc00008f0處是它的第一個引數,即dst。
mov edi, [ebp + 8] ; dst 棧是向下擴充套件的
mov esi, [ebp + 12] ; src
mov ecx, [ebp + 16] ; size
;rep指令是repeat重複的意思,該指令是按照ecx暫存器中指定的次數重複執行後面的指定的指令,每執行一次,ecx自減1,直到ecx等於0時為止
rep movsb ;將資料從esi指向的記憶體地址複製到edi指向的地址
;恢復環境
pop ecx
pop ebp
ret
kernel_init此函式執行的時候,核心已經載入到記憶體0x70000的位置,0x70000 + 0x28是e_phoff欄位,儲存著程式頭表在檔案中的偏移地址。呼叫men_cpy函式,將程式頭表和段對映覆制到0xc000 1000處,然後跳到0xc000 1500處執行。實際,仍是將kernel.bin檔案整個複製到記憶體0xc000 1000處,只不過是為了配合0xc000 1500的程式入口地址罷了。為什麼這麼說,因為核心在0x70000處時,0xc000 1500處沒有程式碼。唯一能想到這麼做的解釋是靈活性。
為什麼要繞一大圈呢?0xc00 1500 - 0xc000 1000 = 0x500,那為什麼不直接跳到0x70000 + 0x500的記憶體處執行呢?或者直接將核心kernel.bin檔案載入到0xc000 1000的記憶體處呢?這樣的話直接跳到0xc00 1500就可以執行了還少一道複製。唯一能想到的解釋是靈活性。
上圖是在ubuntu中編譯32位程式程式頭表與二進位制可執行程式.out檔案對比圖,可見程式入口不是0x804800也不是0x1500而是0x1070,與上面編譯的核心kernel.bin的解析與載入完全不同。
;--------------------------------------------------------------------------------------------
; 程式名稱 | 磁碟扇區 | 實體記憶體起始地址 |頁目錄項(下標)|頁表項(下標)|對應虛擬地址
; mbr | 第0個 | 0x7c00 = 31k | 0, 768 | 6 | 0xC000 6000
; loader | 第2個 | 0x900 = 2k+256 | 0, 768 | 0 | 0xC000 0000
; gdt | 第2個 | 0x900 = 2k+256 | 0, 768 | 0 | 0xC000 0000
; loader_start | 第2個 | 0xc00 = 3k | 0, 768 | 0 | 0xC000 0000
; 頁目錄表 | | 0x100000 = 1M | 0, 768 | 255 | 0xC00F F000
; kernel | 第9個 | 0x70000 = 448k | 0, 768 | 111 | 0xC006 F000
; kernel.segment | 第9個 | 0x1500 = 5k+256 | 0, 768 | 1 | 0xC000 1000
;---------------------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
; 低端1MB中可用記憶體,可用的部分打√
;-------------------------------------------------------------------------------
; | 9FC00 | 9FFFF | 1K | EBDA(Extended BIOS Data Area)擴充套件bios資料區
; √ | 7E00 | 9FBFF | 622080B約608K | 可用區域
; √ | 7C00 | 7DFF | 512B | MBR被BIOS載入到此處,共512位元組
; √ | 500 | 7BFF | 30454B約30K | 可用區域 7bff - 500 = 76ff, 76ff - 7000 = 16ff, 29k+791
; | 400 | 4FF | 256B | BIOS Data Area (BOIS 資料區)
; | 000 | 3FF | 1K | Interrupt Vector Table(中斷向量表)
;-------------------------------------------------------------------------------
;-------------------------------------------------------------------------------
; 連結檢視 | 執行檢視
;-------------------------------------------------------------------------------
; ELF header(elf頭) | ELF header(elf頭)
; Programheadertable(程式頭表)可選 | Programheadertable(程式頭表)
;-------------------------------------------------------------------------------
; Section1(節1) | Segment1(段1)
; .... |
;-------------------------------------------------------------------------------
; Sectionn(節n) | Segment2(段2)
; .... |
;-------------------------------------------------------------------------------
; Sectionheadertable(節頭表) | Sectionheadertable(節頭表)可選
; 待重定位檔案體 | 可執行檔案體
;-------------------------------------------------------------------------------
; 表 2-1 真實模式下的記憶體佈局,可用的部分打√
;-------------------------------------------------------------------------------
; 起始 | 結 束 | 大小 | 用 途
;-------------------------------------------------------------------------------
; FFFFO | FFFFF | 16B | BIOS入口地址,此地址也屬於BIOS程式碼,同樣屬於頂部的640KB位元組。只是為了強調其入口地址才單獨貼出來。此處16位元組的內容是跳轉指令impf0o:e05b
; F0000 | FFFEF | 64KB-16B | 系統BIOS範圍是F0000~FFFFF共640KB,為說明入口地址,將最上面的16位元組從此處去掉了,所以此處終止地址是0XFFFEF
; C8000 | EFFFF | 160KB | 對映硬體介面卡的ROM或記憶體對映式I/O
; C0000 | C7FFF | 32KB | 顯示介面卡BIOS
; B8000 | BFFFF | 32KB | 用於文字模式顯示介面卡
; B0000 | B7FFF | 32KB | 用於黑白顯示介面卡
; A0000 | AFFFF | 64KB | 用於彩色顯示介面卡
; 9FC00 | 9FFFF | 1KB | EBDA(Extended BIOS Data Area)擴充套件BIOS資料區
; 7E00 | 9FBFF |622080B約608KB | 可用區域
; 7C00 | 7DFF | 512B | MBR被BIOS載入到此處,共512位元組
; 500 | 7BFF | 30464B約30KB | 可用區域
; 400 | 4FF | 256B | BIOS Data Area(BIOS資料區)
; 000 | 3FF | 1KB | Interrupt Vector Table(中斷向量表)
;-------------------------------------------------------------------------------
;表3-17 硬碟控制器主要埠暫存器
;-------------------------------------------------------------------------------
; IO埠 | 埠用途
;-------------------------------------------------------------------------------
; Primary通道 | Secondary通道 | 讀操作時 | 寫操作時
;-------------------------------------------------------------------------------
; Command Block registers
;-------------------------------------------------------------------------------
; 0x1F0 | 0x170 | Data | Data
; 0x1F1 | 0x171 | Error | Features
; 0x1F2 | 0x172 | Sector count | Sector count
; 0x1F3 | 0x173 | LBA low | LBA low
; 0x1F4 | 0x174 | LBA mid | LBA mid
; 0x1F5 | 0x175 | LBA high | LBA high
; 0x1F6 | 0x176 | Device | device
; 0x1F7 | 0x177 | Status | Command
;-------------------------------------------------------------------------------
; Control Block registers
;-------------------------------------------------------------------------------
; 0x3F6 | 0x376 | Alternate status | Device Control
;-------------------------------------------------------------------------------
Linux 中任務切換不使用 call 和 jmp 指令。
4096 * 8 * 4k = 2^12 * 2^3 * 2^12 = 2^27