歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> 關於Linux >> linux0.11啟動與初始化

linux0.11啟動與初始化

日期:2017/3/1 16:35:27   编辑:關於Linux
linux0.11啟動與初始化 簡單描述Linux0.11的啟動與初始化過程。 www.2cto.com 啟動過程中需要關注:IDT, GDT, LDT, TSS, 頁表, 堆棧這些數據。 一:啟動過程 啟動的代碼文件為bootsect.s、setup.s、head.s bootsect.s也就是啟動扇區的代碼。這段代碼主要是將setup.s和head.s中的內容讀入內存的相應區域。然後開始執行setup.s www.2cto.com setup.s 1:使用BIOS中斷來獲得相關系統信息:內存大小,硬盤分區信息,顯示卡信息 2:將head.s代碼拷貝到內存地址為0X0000的地方。 3:加載idt表和gdt表地址 4:開啟A20地址線,只有開啟它了才能訪問高於1M地址的內存 5:重新設定中斷控制器。這之後以前的BIOS中斷號就沒用了。 6:置位CR0寄存器的最後一位進入保護模式, 然後用jmpi 0, 8指令跳轉到地址0x08:0x0000處開始執行,也就是head.s的起始代碼處。 這裡設定的idt表全部為空,也即這時並不處理任何中斷。 gdt表中有三個描述符:0--NULL, 1--內核代碼段, 2--內核數據段描述符。 此時內核代碼段與內核數據段:基地址為0x00000000, 限長為:8MB head.s 1:將堆棧設定在static_stack處,堆棧大小為1KB 2:重新設定設定IDT和GDT,此時全部IDT的都設置為ignore_int,即仍然忽略中斷。 GDT中包含256個描述符。 0--NULL, 1--內核代碼段, 2--內核數據段, 3--保留, 4--進程0的TSS段, 5--進程0的LDT段, 6--進程1的TSS段, 7--進程1的LDT段...... 可見系統的GDT中為每個進程都設定了一個TSS和LDT段。 內核代碼段和內核數據段:基地址(0x00000000),限長(16MB) 3:設定分頁(由於內存管理部分目前沒看到,因此關於進程的頁表如何設定暫不明白,這裡是內核的頁表) 在0x00000000處的第一頁存放“頁目錄”,隨後存放4頁“頁表”,每個頁表對應於頁目錄中的一項。 設定的頁表,要求線性地址等於物理地址。 4個頁表能映射16MB的物理空間,因此這16MB的物理內存地址與線性地址是相同的。(0.11的內核沒有PAGE_OFFSET) 4:開始執行init/main.c中的main函數。 方式如下: pushl $_main # 將main函數地址入棧 jmp setup_paging # 開始分頁 ...... setup_paging: ....... ret # 分頁完成後,用ret指令彈出堆棧中的main函數入口地址,並開始執行main函數。 好像內核代碼中常用這種彈出堆棧的方式來執行其他的函數 二:main函數 在啟動過程中設定好GDT和頁表後,開始在main函數中設定其他的內容。 主要是:設定IDT,設備初始化,創建進程0,fork出進程1,用進程1來執行init main.c中主要的初始化函數是:trap_init,sched_init trap_init中調用set_trap_gate來設定相應的中斷描述符表。 下面以0號中斷為例,描述其實現過程 1:調用set_trap_gate(0, &divide_error) 這個宏定義用來設定IDT表中的第0項的陷阱門描述符。 #define set_trap_gate(n, addr) _set_gate(&idt[n], 15, 0, addr) 在_set_gate中:&idt[n]為第n個描述符的地址, 15為描述符的類型(陷阱門),0為描述符的權限(最高權限),addr為要調用的代碼的地址。 _set_gate宏會調用相應的匯編指令,在&idt[n]處寫入8字節的描述符。 2:divide_error的實現 該函數是以匯編碼的形式實現。與該函數對應有一個處理函數do_divide_error(用C語言實現) 對其他的多數陷阱門處理方式也是如此,有一個匯編方式實現的,還有一個C語言實現的處理函數。 當中斷0發生時,先調用divide_error,該函數再調用do_divide_error。 void do_divide_error(long esp, long error_code) 上面為函數原型。第一個參數為出錯時的代碼地址的指針,第二個參數是錯誤碼。(有些中斷不產生錯誤碼,則錯誤碼設成0) 因此在divide_error的匯編代碼中,主要的功能就是將出錯地址的指針和錯誤碼這兩個參數傳遞給do_divide_error函數, 同時將目前的數據段設定為內核數據段。 sched_init函數 該函數設定了進程0的TSS和LDT描述符,並將它們的選擇子加載進了TR和LDTR寄存器。 另外該函數設定了時鐘中斷和系統調用 這裡主要說下系統調用的執行,以fork函數為例。 1:在sched_init中初始化系統調用 set_system_gate(0x80, &system_call) #define set_system_gate(0x80, &system_call) _set_gate(&idt[n], 15, 3, addr) 可見系統調用也是一個陷阱門。區別是權限值為3, 因此用戶進程能通過int 0x80的中斷進入內核,執行系統調用。 2:每個系統調用都有一個對應的編號,fork為第二個系統調用,因此fork的調用號為2。 當執行fork函數的時候,它會用int 0x80來調用system_call函數。 此時fork的調用號被放入eax寄存器中。 3:全部的系統調用函數指針都保存在數組sys_call_table中。 在system_call函數中會執行指令 call _sys_call_table(,%eax, 4)來跳轉到eax指定的系統函數代碼上,對fork來說就是sys_fork函數。 4:system_call i) system_call首先將相應的寄存器入棧。 pushl %edx pushl %ecx pushl %ebx 這三個寄存器對應了相應的系統調用函數的3個參數 因此0.11中,系統函數最多只能有3個參數。 ii)將ds和es設定為內核數據段,將fs設定為用戶進程的數據段,需要用戶進程的數據時,可使用fs來訪問 iii)用call _sys_call_table(,%eax, 4)來執行系統調用 iii)檢查當前進程是否處於可執行狀態,檢查當前進程的時間片是否用完,相應的執行schedule iv)最後是對進程信號的處理。(信號機制沒看完) 三:進入用戶態 在main函數中相關初始化後,main以進程0的身份進入用戶態。 然後調用fork函數,創建進程1,進程1調用init函數 init函數加載根文件系統,運行初始化配置命令,然後執行shell程序,這樣便進入了命令行窗口。 0.11內核中,每個進程都有一個TSS段和一個LDT段,它們保存在進程描述符strut task_struct結構中。 相應段的描述符保存在GDT表中。 在LDT段中,有3個LDT描述符,0--NULL, 1--進程代碼段, 2--進程數據段。 進程n的代碼段和數據段:基地址=n*64MB,限長=64MB。(進程0和1的限長為640KB) 因此系統中最多有64個進程。 進程0的task_struct為INIT_TASK,進程0的TSS和LDT描述符在sched_init中設定。 main函數調用move_to_user_mode函數來執行進程0,進入用戶態。 0.11內核中所有進程都是屬於用戶態,不像之後的Linux內核裡有內核線程。 move_to_user_mode函數 此函數使用iret返回的方式,從內核態進入用戶態。 +------------+ + ss + pushl $0x17 +------------+ + esp + pushl %%eax #eax中保存了esp +------------+ + eflags + pushfl +------------+ + cs + pushl $0x0f +------------+ + eip + pushl $1f #目的代碼的偏移地址 +------------+ 首先采用上面的push指令,將相關的數據壓入堆棧,然後執行iret將它們彈出堆棧。 於是進程0從堆棧中的cs:eip指向的代碼開始執行。 四:fork函數 進程0執行fork函數創建出進程1. 1:調用get_free_page為進程描述符分配內存。 p = (struct task_struct *) get_free_page(); 這一頁內存,前面保存task_struct內存, 頁尾為進程的內核棧, 當一個用戶程序調用系統函數進入內核態後,系統函數執行時使用的棧就是這個。 2:設定進程的task_struct結構體 3:內存拷貝,將父親進程的內存拷貝給新進程。 4:設定新進程在GDT中的TSS和LDT描述符 有關fork最主要的是弄明白了,為什麼它可以“返回”兩次。 1:調用fork時,CPU自動將父進程的返回地址入棧(即eip寄存器入棧) 2:創建子進程的task_struct後,將TSS段中的eax字段設成0,eip字段設成父進程的返回地址。 3:將子進程的狀態設成TASK_RUNNING(就緒狀態) 4:fork函數以子進程的pid返回。 5:等到執行schedule,調度到子進程時。會自動將子進程的TSS內容加載進寄存器。 因此這時CPU中eax寄存器值為0, eip為父進程的返回地址。所以子進程從fork函數的下一條指令開始執行,返回值在eax中,為0。
Copyright © Linux教程網 All Rights Reserved