一步步编写操作系统 49 加载内核2
內核文件kernel.bin是elf格式的二進制可執行文件,初始化內核就是根據elf規范將內核文件中的段(segment)展開到(復制到)內存中的相應位置。在分頁模式下,程序是靠虛擬地址來運行的,無論是內核還是用戶程序,它們對cpu來說都是指令或數據、沒什么區別,交給cpu的指令或數據的地址一律被認為是虛擬地址。坦白說,內核文件中的地址是在編譯階段確定的,里面都是虛擬地址,程序也是靠這些虛擬地址來運行。但這些虛擬地址實際上是我們在初始化內核階段規劃好的,即想安排內核在哪片虛擬內存中,就將內核地址編譯成對應的虛擬地址。而目前我們初始化的是內核,它在物理低端1MB內存中,初始化工作取決于這1MB物理內存中哪塊空間可用,所以,現在還要看前面的內存分布圖從中找塊合適的內存空間來容納內核映像。
其實大家早已經知道內核的入口虛擬地址是0xc0001500啦。但現在大家要假裝不知道^_^,配合一下啊,咱們說一下0xc0001500是怎么來的。
物理內存中0x900處是loader.bin加載的地址,在loader.bin的開始部分是GDT,它可是必須要保留下來的,可不能覆蓋,我們不打算在內核中重新定義它,以后都要指望它了。正如偉大領袖雖然仙逝了,但威望猶在,雖然loader的工作結束啦,但loader所完成的工作成果咱們還得繼續發揚繼續用。預計loader.bin的大小不會超過2000字節。所以咱們可選的起始物理地址是0x900+2000=0x10d0(不要把注意力放在這個奇怪的數上,偶然得出的)。內存很大,但也盡量往低了選,于是湊了個整數,選了0x1500做為內核映像的入口地址。
根據咱們的頁表,低端1MB的虛擬內存與物理內存是一一對應的,所以物理地址是0x1500對應的虛擬地址是0xc0001500。這就解釋了在5.3.1節中,鏈接命令ld中用-Ttext指定了代碼段的起始虛擬地址,再把命令搬過來給大家看下:
ld kernel/main.o -Ttext 0xc0001500 -e main -o kernel/kernel.bin
好,現在咱們得說一下初始化內核的代碼,見代碼:
193 ;---------- 將kernel.bin中的segment拷貝到編譯的地址 ----------- 194 kernel_init: 195 xor eax, eax 196 xor ebx, ebx ;ebx記錄程序頭表地址 197 xor ecx, ecx ;cx記錄程序頭表中的program header數量 198 xor edx, edx ;dx 記錄program header尺寸,即e_phentsize 199 200 mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件42字節處的屬性是e_phentsize,表示program header大小 201 mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件開始部分28字節的地方是e_phoff, ;表示第1 個program header在文件中的偏移量 202 ; 其實該值是0x34,不過還是謹慎一點,這里來讀取實際值 203 add ebx, KERNEL_BIN_BASE_ADDR 204 mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件開始部分44字節的地方是e_phnum,表示有幾個program header 205 .each_segment: 206 cmp byte [ebx + 0], PT_NULL ; 若p_type等于 PT_NULL,說明此program header未使用。 207 je .PTNULL 208 209 ;為函數memcpy壓入參數,參數是從右往左依然壓入.;函數原型類似于 memcpy(dst,src,size) 210 push dword [ebx + 16] ; program header中偏移16字節的地方是p_filesz, ;壓入函數memcpy的第三個參數:size 211 mov eax, [ebx + 4] ; 距程序頭偏移量為4字節的位置是p_offset 212 add eax, KERNEL_BIN_BASE_ADDR ; 加上kernel.bin被加載到的物理地址,eax為該段的物理地址 213 push eax ; 壓入函數memcpy的第二個參數:源地址 214 push dword [ebx + 8] ; 壓入函數memcpy的第一個參數:目的地址;偏移程序頭8字節的位置是p_vaddr,這就是目的地址 215 call mem_cpy ; 調用mem_cpy完成段復制 216 add esp,12 ; 清理棧中壓入的三個參數 217 .PTNULL: 218 add ebx, edx ; edx為program header大小,即e_phentsize,;在此ebx指向下一個program header 219 loop .each_segment 220 ret 221 222 ;---------- 逐字節拷貝 mem_cpy(dst,src,size) ------------ 223 ;輸入:棧中三個參數(dst,src,size) 224 ;輸出:無 225 ;--------------------------------------------------------- 226 mem_cpy: 227 cld 228 push ebp 229 mov ebp, esp 230 push ecx ; rep指令用到了ecx,; 但ecx對于外層段的循環還有用,故先入棧備份 231 mov edi, [ebp + 8] ; dst 232 mov esi, [ebp + 12] ; src 233 mov ecx, [ebp + 16] ; size 234 rep movsb ; 逐字節拷貝 235 236 ;恢復環境 237 pop ecx 238 pop ebp 239 ret對于可執行程序,我們只對其中的段(segment)感興趣,它們才是程序運行的實質指令和數據的所在地,所以我們要找出程序中所有的段。
函數kernel_init的作用是將kernel.bin中的段(segment)拷貝到各段自己被編譯的虛擬地址處,將這些段單獨提取到內存中,這就是平時所說的內存中的程序映像。kernel_init的原理是分析程序中的每個段(segment),如果段類型不是PT_NULL(空程序類型),就將該段拷貝到編譯的地址中。
現在內核已經被加載到KERNEL_BIN_BASE_ADDR地址處,該處是文件頭elf_header。在我們的程序中,遍歷段的方式是指向第一個程序頭后,每次增加一個段頭的大小,即e_phentsize。該屬性位于偏移程序開頭42字節處。為了以后遍歷段時方便,避免了頻繁的訪問內存,在第200行,我們用寄存器dx來存儲段頭大小,這樣,每遍歷一個段頭時,就直接從dx中獲取段頭大小,這將在第218行體現。
為了找到程序中所有的段,必須要獲取程序頭表。在文件開頭偏移28字節處是屬性e_phoff,該屬性表示程序頭表在文件中的偏移量,程序頭表是程序頭program header的數組,所以e_phoff也就是第1 個program header在文件中的偏移量。第201行,在內存e_phoff處取值,將得到的程序頭表偏移量存入寄存器ebx。
我們需要的是程序頭表的物理地址,由于此時的ebx還是程序頭表文件內的偏移量,所以要將其加上內核的加載地址,這樣才是程序頭表的物理地址。所以在第203行為ebx加上了內核文件的加載地址KERNEL_BIN_BASE_ADDR。最終ebx寄存器做為程序頭表的基址,用它來遍歷每一個段,此時ebx指向程序中的第1 個program header。
我們已經知道,段是由程序頭(program header)來描述的,一個程序頭代表一個段。在知道了第一個程序頭的地址后,為了遍歷所有的程序頭,還需要知道程序中程序頭的數量,也就是段的數量,這是由elf_header中的屬性e_phnum決定,它在elf_header中偏移為44。我們通常用cx寄存器來做循環計數器,所以在第204行,匯編語句“mov cx, [KERNEL_BIN_BASE_ADDR + 44]”將段的數量賦值給寄存器cx。
現在程序頭表地址在寄存器ebx中,而且又知道了程序頭表中段的數量,所以現在可以遍歷每一個段的信息啦,其工作在代碼第205~220行中完成。
在第206行,程序先判斷下段的類型是不是PT_NULL,PT_NULL是在boot/include/boot.inc中定義的宏,其值為0,該意義表示空段類型。(PT_NULL也可以在linux系統的/usr/include/elf.h中找到其定義:#define PT_NULL 0)
在207行,如果發現該段是空段類型的話,就跨過該段不處理,跳到.PTNULL處,也就是第217行。
指定下一個段是通過在程序頭表地址處加上一個段的大小e_phentsize來實現的,e_phentsize的值咱們已經將其存儲在dx寄存器啦,所以在第218行,直接將ebx,也就是當前program header地址,加上edx,ebx便指向了下一個段的program header。edx的高16位為0,所以這里用add ebx, edx沒有問題。
第209~216行,程序中的段通過mem_cpy函數復制到段自身的虛擬地址處。在這里,我們涉及到了函數調用約定的知識,不過為了敘述的更清楚,在這里我不想簡單地說,在下一章中我們專門拿出一節來說這事兒。在此我還是本著夠用的原則,把用到的部分給您說明白。
我們在此實現的函數是mem_cpy,不是c標準庫中的memcpy函數,將來我們會在內核中實現memcpy。memcpy原型是void *memcpy(void *dest, const void *src, size_t n),功能是將src指向的地址空間處的連續n個字節拷貝到dest指向的地址空間。我們的學習它的用法,在匯編語言中用mem_cpy函數實現了它,此函數的原型相當于mem_cpy(void* dst, void* src, int size)。所以我們也要提供三個參數才能使用它。這三個參數都在程序頭program header中,所以它們都可以基于ebx再增加適當的偏移量來得到。program header結構,很容易理解210~214行的代碼。
第215行是調用 mem_cpy,這涉及到為該函數傳入參數的問題。在匯編語言中傳遞參數的方法太多了,原因是匯編語言太靈活了,不怎么受約束,咱們可以訪問到的資源太多了。所以,主調函數可以把參數放在寄存器中,也可以放在棧中,而棧就是內存,所以只要大家高興,也可以把參數直接放到某塊內存中,類似共享內存的方式來傳遞參數。主調函數以上面任意一種方式傳遞參數,被調函數都可以輕松地拿到參數。
總結
以上是生活随笔為你收集整理的一步步编写操作系统 49 加载内核2的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 浦发淘票票信用卡需要面签吗
- 下一篇: 二季度十大经济体的GDP史上最惨?多国跌