2008年11月25日 星期二

Linux啓動分析(2)— bootsect.S、setup.S、head.S分析[轉貼]

bootsect.S,系統引導程序,一般不超過512字節。
在PC系統結構中,綫性地址0xA0000以上,即640K以上用于圖形接口卡和BIOS自身,640K以下為系統的基本内存。如果配置更多的内存,則0x100000,即1MB處開始稱為高内存。當BIOS引導一個系統時,總是把引導扇區讀入到基本内存地址為0x7c00的地方,然後跳轉到此執行引導扇區的代碼。這段代碼將自身搬運到0x90000處,並跳轉到那繼續執行,然後通過BIOS提供的讀磁盤調用“int 0x13”從磁盤上讀入setup和内核映像。其中setup的映像讀入到0x90200處,然後跳轉到setup的代碼中。

從0x90000到0xA0000一共64K,bootsect僅占512字節,所以setup大小理論上可到63.5KB。
在Linux2.4版本以前,在最前面的512字節裏保護了一個mini “boot loader”,只要拷貝啓動代碼運行就可從軟碟啓動;但在2.6版本中不再保護這様的”boot loader”,所以必須在第一個磁盤分區上存儲一個合適的boot loader才能從軟碟啓動,軟碟、硬碟和光驅啓動都是一様的過程。
setup進行映像的解壓縮,從BIOS收集一些數據,在控制台顯示一些信息。

基本内存中開頭一部分空間是保留給BIOS自己用的,另一方面對于Linux内核的引導也需要保留一些運行空間,一共保存了64K。基本内存中用于内核映像的就是8*64K=512K,其中頂端留4K用于引導命令行及從BIOS獲取需要傳遞給内核的數據。内核映像一般都經過壓縮,壓縮後的映像和引導扇區及輔助引導程序的映像拼接在一起,成為内核的引導映像。大小不超過508K的映像稱為小映像zImage,早期版本放在0x10000位置處,否則稱為大内核bzImage,放在0x100000位置處。

CPU在bootsect時處于16位實地址模式,然後在setup的執行過程中轉入32位保護模式。
Setup從BIOS中讀取系統數據(内存大小、顯卡模式、磁盤等參數),將數據保存在0x90000-0x901FF,覆蓋了bootsect的内容。設置32位運行方式:加載中斷描述表寄存器IDTR、全局描述表寄存器GDTR;臨時設置IDT表和GDT表,並在GDT表中設置内核代碼段和數據段的描述符,在Head.S中會根據内核的需要重新設置這些描述符表;開啓A20地址綫;重新設置兩個中斷控制器8259A,將硬件中斷號重新設置為0x20和0x2f;最後設置CPU的控制寄存器CR0(機器狀態字)的保護模式比特(PE)位,從而進入32位保護模式運行;然後跳轉到head.S中的startup_32執行。

對于小内核映像放在0x10000處,Setup會把system從0x10000移到0x0000開始處。對于大内核映像,vmlinux中普通内核代碼被編譯成以PAGE_OFFSET+1MB為起始地址,在Head.S中初始化代碼把虚擬地址减去PAGE_OFFSET就能得到以1MB為起始位置的物理地址,這也正是内核映像在物理内存中的存放位置。
Head.S中的startup_32主要用于開啓頁面單元。初始化工作在編譯過程中開始進行,它先定義一個稱為swapper_pg_dir的數組,使用鏈接器指示在地址0x00101000。然後分彆為兩個頁面pg0和pg1創建頁表項。第一組指向pg0和pg1的指針放在能覆蓋1~9MB内存的位置,第二組指針放在PAGE_OFFSET+1MB的位置。一旦開始頁機制,在上述頁表和頁表項指針建立後可以保證,在内核映像中不論是采用物理地址還是虚擬地址,都可以進行正確的頁面映射。内核其他部分的頁表初始化在paging_init()中完成。映射建立後,通過設置cr0寄存器中的某位開啓頁面映射,然後通過一個跳轉指令保證指令指針的正確性。

(1).Bootsect啓動過程:
假設用LILO啓動,啓動時用户可以選擇啓動哪個操作系統。LILO將boot loader分為兩部分,一部分放到啓動分區的第一個扇區;
  1. BIOS將MBR或啓動分區的第一個扇區的啓動部分加載到地址0x00007c00處;

  2. 該程序將自身移到0x00096a00,建立實模式棧(從0x00098000到0x000969ff),將LILO的第二部分加載到0x00096c00處,然後跳轉到此執行;

  3. 然後第二部分程序從磁盤讀取一個可啓動的操作系統列表讓用户選擇,最後用户選擇每個OS後,boot loader可以拷貝不啓動分區或者之間拷貝内核映像到RAM中去;

  4. 加載Linux内核映像時,LILO boot loader首先調用BIOS例程顯示”Loading …”信息;

  5. 調用BIOS例程加載内核映像的初始化部分到RAM上,内核映像的前512字節放在0x00090000位置,setup()函數代碼放在0x00090200位置;

  6. 接着調用BIOS例程裝載内核映像的其餘部分,映像可能放在低地址0x00010000(使用make zImage編譯的小内核映像)或者高地址0x00100000(使用make bzImage編譯的大内核映像)。

  7. 然後跳至剛剛setup部分。


(2).Setup.S分析
setup()匯編函數被連接器放在内核映像文件中的0x200偏移處。Setup函數必須初始化計算機中的硬件設備並為内核程序的執行建立環境。
  1. 在ACPI兼容的系統中,調用BIOS例程建立描述系統物理内存布局的表。在早期系統中,它調用BIOS例程返回系統可以的RAM大小;

  2. 設置鍵盤的重復延遲和速率;

  3. 初始化顯卡;

  4. 檢測IBM MCA總綫、PS/2鼠標設備、APM BIOS支持等;

  5. 如果BIOS支持Enhanced Disk Drive Services (EDD),將調用正確的BIOS例程建立描述系統可用硬碟的表;

  6. 如果内核加載在低RAM地址0x00010000,則把它移動到0x00001000處;如果映像加載在高内存1M位置,則不動;

  7. 啓動位於8042鍵盤控制器的A20 pin。

  8. 建立一個中斷描述表IDT和全局描述表GDT表;

  9. 如果有的話,重啓FPU單元;

  10. 對可編程中斷控制器進行重新編程,屏蔽所以中斷,級連PIC的IRQ2不需要;

  11. 設置CR0狀態寄存器的PE位使CPU從實模式切换到保護模式,PG位清0,禁止分頁功能;

  12. 跳轉到startup_32()匯編函數, jmpi 0x100000, __BOOT_CS,終于進入内核Head.S;


(3).Head.S分析
有兩個不同的startup_32()函數,一個在arch/i386/boot/compressed/head.S文件中,setup結束後,該函數被放在0x00001000或者0x00100000位置,該函數主要操作:
  1. 首先初始化段寄存器和臨時堆棧;

  2. 清除eflags寄存器的所有位;

  3. 將_edata和_end區間的所有内核未初始化區填充0;

  4. 調用decompress_kernel( )函數解壓内核映像。首先顯示"Uncompressing Linux..."信息,解壓完成後顯示 "OK, booting the kernel."。内核解壓後,如果時低地址載入,則放在0x00100000位置;否則解壓後的映像先放在壓縮映像後的臨時緩存裏,最後解壓後的映像被放置到物理位置0x00100000處;

  5. 跳轉到0x00100000物理内存處執行;


解壓後的映像開始于arch/i386/kernel/head.S 文件中的startup_32()函數,因為通過物理地址的跳轉執行該函數的,所以相同的函數名並没有什麽問題。該函數未Linux第一個進程建立執行環境,操作如下:
  1. 初始化ds,es,fs,gs段寄存器的最終值;

  2. 用0填充内核bss段;

  3. 初始化swapper_pg_dir數組和pg0包含的臨時内核頁表:

    • 將swapper_pg_dir(0x1000)和pg0(0x2000)清空,swapper_pg_dir作為整個系統的頁目録;

    • 將pg0作為第一個頁表,將其地址賦到swapper_pg_dir的第一個32位字中。

    • 同時將該頁表項也賦給swapper_pg_dir的第3072個入口,表示虚擬地址0xc0000000也指向pg0。

    • 將pg0這個頁表填滿指向内存前4M。

    • 在cr3寄存器中存放PGD的地址,並設置cr0寄存器中的PG位,啓用分頁支持。

  4. 建立進程0idle進程的内核模式的堆棧;

  5. 再次清除eflags寄存器的所有位;

  6. 調用setup_idt()用非空的中斷處理函數填充IDT表;

  7. 將從BIOS獲取的系統參數傳遞到操作系統的第一個頁面幀;

  8. 識別處理器的模式;

  9. 將GDT和IDT表的地址加載到gdtr和idtr寄存器中;

  10. 跳轉到start_kernel函數,這個函數是第一個C編制的函數,内核又有了一個新的開始。


(4).start_kernel()分析:
  1. 調度器初始化,調用sched_init();

  2. 調用build_all_zonelists函數初始化内存區;

  3. 調用page_alloc_init()和mem_init()初始化夥伴系統分配器;

  4. 調用trap_init()和init_IRQ()對中斷控制表IDT進行最後的初始化;

  5. 調用softirq_init() 初始化TASKLET_SOFTIRQ和HI_SOFTIRQ;

  6. Time_init()對系統日期和時間進行初始化;

  7. 調用kmem_cache_init()初始化slab分配器;

  8. 調用calibrate_delay()計算CPU時鐘頻率;

通過調用kernel_thread()啓動進程1init進程的内核綫程,然後該綫程再創建其他的内核綫程執行/sbin/init程序。

資料來源:
http://blog.csdn.net/cxylaf/archive/2007/05/26/1626513.aspx

沒有留言: