歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
您现在的位置: Linux教程網 >> UnixLinux >  >> Linux基礎 >> 關於Linux

linux0.11啟動與初始化

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