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