Linux源代碼閱讀——內核引導【轉】


Linux源代碼閱讀——內核引導

目錄

  1. Linux 引導過程綜述
  2. BIOS
    • POST
    • 自舉過程
  3. Boot loader
    • 主引導扇區結構
    • GRUB stage1
    • GRUB stage2
  4. 內核初始化:體系結構相關部分
    • 內核映像結構
    • header.S
    • 初始化與保護模式
    • 自解壓內核
    • startup_32
  5. 內核初始化:體系結構無關部分
    • 核心數據結構初始化
    • 設備初始化

1 Linux引導過程綜述

  1. BIOS
    在 i386 平台中,由 BIOS 作最初的引導工作,執行加電自檢、初始化,讀取引導設備的主引導扇區並執行。
  2. Boot loader(以 GRUB 為例)
    MBR 中的、緊隨 MBR 后的 phase 1/1.5 boot loader 載入文件系統中的 phase 2 及其配置,顯示操作系統選擇菜單,執行用戶命令,載入選定的操作系統內核與 initrd。
  3. 內核初始化:體系結構相關部分
    從 header.S 開始,到 main.c 初始化參數,再到 pm.c 進入保護模式,然后載入 vmlinuz 並自解壓,在 startup_32.S 中開啟分頁機制、初始化中斷向量表、檢測 CPU 類型等,完成 x86 體系結構的保護模式初始化。這是本文重點。
  4. 內核初始化:體系結構無關部分
    分為核心數據結構初始化(start_kernel)和設備初始化兩個階段。
  5. 用戶態初始化
    以下內容超出了本文范圍。用戶態的 init 程序:
    • 獲取運行信息
    • 執行 /etc/rc[runlevel].d 中的啟動腳本
    • 加載內核模塊(/etc/modprobe.conf)
    • 執行 /etc/init.d 中的腳本
    • 執行 /bin/login,等待用戶登錄
    • 接受 shell 中的用戶控制

2 BIOS

BIOS的主要功能概括來說包括如下幾部分:

  • POST

    加電自檢,檢測 CPU 各寄存器、計時芯片、中斷芯片、DMA 控制器等

  • Initial

    枚舉設備,初始化寄存器,分配中斷、IO 端口、DMA 資源等

  • Setup

    進行系統設置,存於 CMOS 中。

  • 常駐程序

    INT 10h、INT 13h、INT 15h 等,提供給操作系統或應用程序調用。

  • 啟動自舉程序

    在POST過程結束后,將調用 INT 19h,啟動自舉程序,自舉程序將讀取引導記錄,裝載操作系統。

BIOS 的啟動主要由 POST 過程與自舉過程構成。

2.1 POST

當 PC 加電后,CPU 的寄存器被設為某些特定值。其中,指令指針寄存器(program counter)被設為 0xfffffff0。

CR1,一個32位控制寄存器,在剛啟動時值被設為0。CR1 的 PE (Protected Enabled,保護模式使能) 位指示處理器是處於保護模式還是實模式。由於啟動時該位為0,處理器在實模式中引導。在實模式中,線性地址與物理地址是等同的。

在實模式下,0xfffffff0 不是一個有效的內存地址,計算機硬件將這個地址指向 BIOS 存儲塊。這個位置包含一條跳轉指令,指向 BIOS 的 POST 例程。

POST(Power On Self Test,加電自檢)過程包括內存檢查、系統總線檢查等。如果發現問題,主板會蜂鳴報警。在 POST 過程中,允許用戶選擇引導設備。

POST 的最后一步是執行 INT 0x19 指令,開始自舉過程。

POST 過程在 AWARD BIOS 的源碼中在 BOOTROM.ASM 文件中 BootBlock_POST 函數過程中實現,主要步驟如下:

  1. 初始化各種主板芯片組
  2. 初始化鍵盤控制器
  3. 初始化中斷向量、中斷服務例程
  4. 初始化 VGA BIOS 控制器
  5. 顯示 BIOS 的版本和公司名稱
  6. 掃描各種介質容量並顯示
  7. 讀取 CMOS 的啟動順序配置
  8. 調用 INT 0x19 啟動自舉程序

2.2 自舉過程

自舉過程即為執行中斷 INT 0x19 的中斷服務例程 INT19_VECT 的過程 (Bootrom.asm)

主要功能為讀取引導設備第一個扇區的前 512 字節(MBR),將其讀入到內存 0x0000:7C00,並跳轉至此處執行。

3 Boot loader

3.1 主引導扇區結構

硬盤第一個扇區的前 512 個字節是主引導扇區,由 446 字節的 MBR、64 字節的分區表和 2 字節的結束標志組成。

  • MBR(Master Boot Record)是 446 字節的引導代碼,被 BIOS 加載到 0x00007C00 並執行。
  • 硬盤分區表占據主引導扇區的 64 個字節(0x01BE -- 0x01FD),可以對四個分區的信息進行描述,其中每個分區的信息占據 16 個字節。

    一個分區記錄有如下域:

    • 1字節 文件系統類型
    • 1字節 可引導標志
    • 6字節 CHS格式描述符
    • 8字節 LBA格式描述符

    LBA和CHS兩種描述符指示相同的信息,但是指示方式有所不同:LBA (邏輯塊尋址,Logical Block Addressing)指示分區的起始扇區和分區長度, 而CHS(柱面 磁頭 扇區)指示首扇區和末扇區。

  • 結束標志字 55,AA(0x1FEH -- 0x1FFH)是主引導扇區的最后兩個字節,是檢驗主引導記錄是否有效的標志。

3.2 GRUB stage1

Linux 的啟動方式包括 LILO、GRUB 等。這里結合 GRUB 源代碼分析其引導過程。

GRUB 的引導過程分為 stage1、stage 1.5 和 stage 2。其中 stage1 和可能存在的 stage1.5 是為 stage2 做准備,stage2 像一個微型操作系統。

  1. BIOS 加載 GRUB stage1(如果安裝到 MBR)到 0x00007C00.

  2. stage1 位於 stage1/stage1.S,匯編后形成 512 字節的二進制文件,寫入硬盤的0面0道第1扇區。

    stage1 將0面0道第2扇區上的 512 字節讀到內存中的0x00007000處,然后調用 COPY_BUFFER 將其拷貝到 0x00008000 的位置上,然后跳至 0x00008000 執行。這 512 字節代碼來自 stage2/start.S,作用是 stage1_5 或者 stage2(編譯時決定加載哪個)的加載器。

    /* start.S */
    blocklist_default_start:
    .long 2	 /* 從第3扇區開始*/
    blocklist_default_len:
    /* 需要讀取多少個扇區 */
    #ifdef STAGE1_5
    .word 0	 /* 如果是 STAGE1_5,則不讀入 */
    #else
    .word (STAGE2_SIZE + 511) >> 9 /* 讀入 Stage2 所占的所有扇區 */
    #endif
    blocklist_default_seg:
    #ifdef STAGE1_5
    .word 0x220 /* 將 stage1.5 加載到 0x2200 */
    #else
    .word 0x820	/* 將 stage2 加載到 0x8200 */
    #endif
    
  3. 由於 stage1 和 start 不具備文件系統識別功能,stage 1.5 只能被存放在固定的扇區中。例如 e2fs_stage1_5 就被存放在0面0道第3扇區開始的一段連續空間里。(第一個主分區是從1面0道第1扇區開始的,stage 1.5 不會覆蓋主分區內容)

    stage 1.5 能夠讀取文件系統,負責從文件系統中載入並執行 stage 2,即 GRUB 的核心映像。由於系統引導過程中不需要修改文件系統,因此只實現了文件系統的讀取。

    可以說,stage 1.5 是 stage 1 與 stage 2 之間的橋梁,解決了文件系統這個“先有雞還是先有蛋”的問題。

3.3 GRUB stage2

stage2 將系統切換到保護模式,設置 C 運行環境,尋找 config 文件,執行 shell 接受用戶命令,載入選定的操作系統內核。

  1. stage2 的入口點是 asm.s
    #ifdef STAGE1_5
    # define	ABS(x)	((x) - EXT_C(main) + 0x2200)
    #else
    # define	ABS(x)	((x) - EXT_C(main) + 0x8200)
    #endif
    
    1. 初始化一些變量
    2. 跳轉到 code_start
    3. 關中斷,設置段寄存器和堆棧起始地址
    4. 從實模式切換到保護模式
    5. 清空 bss 段
    6. init_bios_info()
  2. 隨后進入 stage2.c,執行 GRUB 的主要功能。

    • cmain(): 主函數,載入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功載入就進入 run_menu(),顯示菜單,進入循環倒計時,如果超時就進入第一個,如果用戶按了鍵就停止倒計時。用戶作出選擇后,跳轉到 boot_entry(),清空屏幕、獲取入口,通過 find_command 找到的函數指針調用相應的命令。

    • 如果沒有成功載入配置文件,就 enter_cmdline(),也是通過 find_command 調用相應的命令。

  3. 每個 GRUB 命令都要在 stage2/builtin.c 的 builtin_table 數組中登記:
    struct builtin
    {
        char *name;			/* 命令名稱 */
        int (*func) (char *, int);	/* 命令執行時調用的函數指針 */
        int flags;			/* 標志,似乎未用到 */
        char *short_doc;		/* 短幫助 */
        char *long_doc;		/* 詳細幫助 */
    };
    struct builtin *builtin_table[];
    
  4. 常用 GRUB 命令:
    • root:掛載分區並設為根分區。
      root_func (char *arg, int flags)
    • kernel:對傳進來的參數逐個解析,獲得 linux 內核映像路徑,通過 load_image() 載入內核。
      kernel_func (char *arg, int flags)
    • boot:根據操作系統類型調用不同的啟動函數,將控制權轉交給操作系統。支持 BSD、linux、chain loader、multi boot 等方式。
      boot_func (char *arg, int flags)
  5. stage2 中的文件系統驅動:

    每種文件系統都要按照 stage2/filesys.h 的定義在 stage2/disk_io.c 的 fsys_table 數組中登記:

    /* stage2/filesys.h */
    struct fsys_entry
    {
        char *name;                                         //文件系統名稱
        int (*mount_func) (void);                           //掛載
        int (*read_func) (char *buf, int len);              //讀文件
        int (*dir_func) (char *dirname);                    //打開文件
        void (*close_func) (void);                          //關閉文件
        int (*embed_func) (int *start_sector, int needed_sectors);  //不清楚
    };
    

    GRUB 調用 grub_open() 打開文件。grub_open 在 fsys_table 數組中逐個調用 fsys_entry::mount_func(),找到當前已掛載的文件系統,再用 fsys_entry::dir_func() 方法打開文件。

4 內核初始化:體系結構相關部分

4.1 內核映像結構

根據 Linux/I386 啟動協議(Documentation/i386/boot.txt),x86 體系結構大內核內存使用如下:

For a modern bzImage kernel with boot protocol version >= 2.02, a
memory layout like the following is suggested:

        ~                        ~   
        |  Protected-mode kernel |
100000  +------------------------+
        |  I/O memory hole       |   
0A0000  +------------------------+
        |  Reserved for BIOS     |      Leave as much as possible unused
        ~                        ~   
        |  Command line          |      (Can also be below the X+10000 mark)
X+10000 +------------------------+
        |  Stack/heap            |      For use by the kernel real-mode code.
X+08000 +------------------------+    
        |  Kernel setup          |      The kernel real-mode code.
        |  Kernel boot sector    |      The kernel legacy boot sector.
X       +------------------------+
        |  Boot loader           |      <- Boot sector entry point 0000:7C00
001000  +------------------------+
        |  Reserved for MBR/BIOS |
000800  +------------------------+
        |  Typically used by MBR |
000600  +------------------------+ 
        |  BIOS use only         |   
000000  +------------------------+

根據 arch/x86/boot/Makefile,bzImage 大內核映像由 setup.elf 和 vmlinux 組成,而 vmlinux 又由 setup.bin 和 vmlinux.bin 組成。vmlinux.bin 會進行壓縮存儲,變成 vmlinux.bin.gz。因此 bzImage 由 setup.elf、setup.bin、vmlinux.bin.gz 三部分組成。

Line 28: targets         := vmlinux.bin setup.bin setup.elf zImage bzImage
Line 29: subdir-         := compressed
Line 30: 
Line 31: setup-y         += a20.o cmdline.o copy.o cpu.o cpucheck.o edd.o
Line 32: setup-y         += header.o main.o mca.o memory.o pm.o pmjump.o
Line 33: setup-y         += printf.o string.o tty.o video.o video-mode.o version.o

其中 setup-y 就是 setup.elf,其中引用的 header.o 是從 header.S 匯編而來的。

Line 77: $(obj)/bzImage: IMAGE_OFFSET := 0x100000
Line 86: $(obj)/zImage $(obj)/bzImage: $(obj)/setup.bin \
Line 87:                               $(obj)/vmlinux.bin $(obj)/tools/build FORCE
Line 88:         $(call if_changed,image)
Line 89:         @echo 'Kernel: $@ is ready' ' (#'`cat .version`')'
Line 90:
Line 91: OBJCOPYFLAGS_vmlinux.bin := -O binary -R .note -R .comment -S

大內核情況下的內存分布圖:

        |  vmlinux               |   
100000  +------------------------+
        |  setup.elf的setup部分   |
090200  +------------------------+
        |  setup.elf的啟動扇區     |
090000  +------------------------+
        |  BootLoader            |
007c00  +------------------------+
        |                        |
000000  +------------------------+

在進入源代碼的世界之前,我們先看看用於控制 arch/x86/boot 下代碼進行鏈接的 setup.ld。

ld 文件用於控制 ld 的鏈接過程:

  • 描述輸入文件的各節如何對應到輸出文件的各節
  • 控制輸入文件各節及符號的內存布局

每個對象文件有一個節(section)列表、一個符號列表,一個符號可以是已定義或未定義的。每個已定義的符號有地址。未定義的符號則要在鏈接時從其他文件中尋找其定義。

  1. 指定輸出文件格式
    OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
  2. 指定目標體系結構
    OUTPUT_ARCH(i386)
  3. 設置入口點
    ENTRY(_start)
  4. 輸入文件各節到輸出文件的映射
    SECTIONS
    {
    . = 0				// 從 0 開始
    .bstext : { *(.bstext) }	// 所有輸入文件的 .bstext 節組合成輸出文件的 .bstext 節
    .bsdata : { *(.badata) }	// 所有輸入文件的 .bsdata 節...
    . = 497				// 填充 512 字節的 bootloader(見4.2節 header.S)
    .header : { *(.header) }
    

    在每一部分(header、rodata、data、bss、end)之間,對齊 16 字節內存邊界:

    . = ALIGN(16);

    最后用斷言保證鏈接后的目標文件不太大,且偏移量正確。

4.2 header.S

start2:
	movw	%cs, %ax        # CS = 0x7c00
	movw	%ax, %ds	# 初始化段寄存器
	movw	%ax, %es
	movw	%ax, %ss
	xorw	%sp, %sp
	sti			# 開中斷
	cld			# di++, si++
................................
msg_loop:			# 打印字符例程
................................
bs_die:				# 錯誤處理例程
        .ascii  "Direct booting from floppy is no longer supported.\r\n"
        .ascii  "Please use a boot loader program instead.\r\n"
        .ascii  "\n"
        .ascii  "Remove disk and press any key to reboot . . .\r\n"
        .byte   0

這段代碼編譯鏈接后,會生成 512 字節的 bootsector,其中 .section ".header", "a" 中的變量共 15 字節。注意到 setup.ld (Linker script for the i386 setup code) 中加入了 497 字節的空白,事實上恰好湊夠 512 字節。

事實上,上一節我們提到,MBR 是由 GRUB 寫入的,因此這里的 bootsector 對於硬盤啟動是用不到的。GRUB 等 boot loader 將 setup.elf 讀到 0x90000 處,將 vmlinux 讀到 0x100000 處,然后跳轉到 0x90200 開始執行,恰好跳過了 512 字節的 bootsector。

有意思的是,從軟盤啟動時,header.S 生成的 bootsector 做的惟一一件事就是打印錯誤信息(bs_die),不支持從軟盤啟動。

下面就是 0x90200(_start)了,目的就是跳到 start_of_setup。

         # Part 2 of the header, from the old setup.S
................................
# End of setup header #####################################################

上面這兩行之間的代碼是一個龐大的數據結構,與 include/asm/bootparam.h 中的 struct setup_header 一一對應。這個數據結構定義了啟動時所需的默認參數,其中一些參數可以通過命令選項 overwrite。下表列出了一些參數的意義。

名稱 偏移 大小(字節) 意義
root_flags 0x1f2 2 根目錄是否只讀,可用 ro 或 rw 選項指定
root_dev 0x1fc 2 默認的 root 設備,即 /boot 所在目錄,可用 root= 選項指定
boot_flag 0x1fe 2 0xAA55,即主引導扇區結束標志
header 0x202 4 HdrS (0x53726448),內核標志
version 0x206 2 啟動協議版本號: major * 64 + minor
kernel_version 0x20e 2 內核版本號
type_of_loader 0x210 1 Boot loader ID: Boot loader ID * 64 + Version No.
Boot loader IDs:
0 LILO
1 Loadlin
2 bootsect-loader
3 SYSLINUX
4 EtherBoot
5 ELILO
7 GRuB
8 U-BOOT
9 Xen
A Gujin
B Qemu
loadflags 0x211 1 啟動選項的掩碼。
  • Bit 0: LOADED_HIGH (1表示保護模式代碼加載到 0x100000)
  • Bit 7: CAN_USE_HEAP (為1表示 heap_end_ptr 有效)
code32_start 0x214 4 內核解壓縮前立即跳轉到的 32 位 flat-mode 入口
ramdisk_image 0x218 4 initramfs 的 32 位線性地址
cmd_line_ptr 0x228 4 內核命令行的 32 位線性地址

下面我們迎來了真正的起點(start_of_setup),主要流程為:

  1. 復位硬盤控制器
  2. 如果 %ss 無效,重新計算棧指針
  3. 初始化棧,開中斷
  4. 將 cs 設置為 ds,與 setup.elf 的入口地址一致
  5. 檢查主引導扇區末尾標志,如果不正確則跳到 setup_bad
  6. 清空 bss 段
  7. 跳到 main(定義在 boot/main.c)

4.3 初始化與保護模式

我們終於暫時離開了匯編代碼,走進 “主要” 的啟動部分。這一部分在 arch/x86/boot/main.c 中。

main() 中的幾個函數調用都有比較詳細的注釋,主要作用是初始化 boot_params,將來會經常被用到。

include/asm/bootparam.h 中定義的 boot_params 結構體 (即 zeropage) 在此完成初始化:

  • copy_boot_params() 初始化 boot_params.hdr (將 hdr 復制過來)
  • detect_memory() 初始化 boot_params.e820_map 和 boot_params.e820_entries
  • query_apm_bios() 初始化 apm_bios_info、screen_info

go_to_protected_mode() 進入保護模式,代碼在 boot/pm.c。

  1. realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,記錄了 hook 函數地址,如果有的話就執行之
  2. reset_coprecessor(): 重啟協處理器
  3. make_all_interrupts(): 關閉所有舊 PIC 上的中斷。其中的 io_delay 等待 I/O 操作完成。
  4. setup_idt(): 初始化中斷描述符表 (空的)
  5. setup_gdt(): 初始化 GDT:
    • GDT_ENTRY_BOOT_CS
    • GDT_ENTRY_BOOT_DS
    • GDT_ENTRY_BOOT_TSS

    其中 GDT_ENTRY_BOOT_CS 和 GDT_ENTRY_BOOT_DS 基地址都為零,段限長都是 4G。

    下面是 GDT 數據結構示意:

  6. protected_mode_jump(): 匯編代碼,下面分析。傳參說明:進入保護模式后將采用段訪問內存地址,因此要將傳入的參數轉換為線性地址。

下面進入 boot/pmjump.S 中的 protected_mode_jump。

 29 protected_mode_jump:
 30         movl    %edx, %esi              # Pointer to boot_params table
 31 
 32         xorl    %ebx, %ebx
 33         movw    %cs, %bx                # 將實模式的代碼段放入 bx
 34         shll    $4, %ebx                # 轉換為線性地址
 35         addl    %ebx, 2f                # 將 in_pm32 的實模式地址轉換為線性地址
 36 
 37         movw    $__BOOT_DS, %cx         # ds 段選擇子
 38         movw    $__BOOT_TSS, %di        # tss 段選擇子
 39 
 40         movl    %cr0, %edx
 41         orb     $X86_CR0_PE, %dl        # Protected mode
 42         movl    %edx, %cr0              # 將 cr0 的0位置0是進入保護模式的標志
 43         jmp     1f                      # Short jump to serialize on 386/486
 44 1:
 45         # 下面這段作用是跳轉到 in_pm32,由於已經在保護模式,所以需要考慮段的問題
 46         # Transition to 32-bit mode
 47         .byte   0x66, 0xea              # ljmpl opcode
 48 2:      .long   in_pm32                 # offset
 49         .word   __BOOT_CS               # segment
 50 
 51         .size   protected_mode_jump, .-protected_mode_jump
 52 
 53         .code32
 54         .type   in_pm32, @function
 55 in_pm32:        # 下面的注釋挺清楚,就不翻譯了
 56         # Set up data segments for flat 32-bit mode
 57         movl    %ecx, %ds
 58         movl    %ecx, %es
 59         movl    %ecx, %fs
 60         movl    %ecx, %gs
 61         movl    %ecx, %ss
 62         # The 32-bit code sets up its own stack, but this way we do have
 63         # a valid stack if some debugging hack wants to use it.
 64         addl    %ebx, %esp
 65 
 66         # Set up TR to make Intel VT happy
 67         ltr     %di                     # 這個比較有意思
 68 
 69         # Clear registers to allow for future extensions to the
 70         # 32-bit boot protocol
 71         xorl    %ecx, %ecx
 72         xorl    %edx, %edx
 73         xorl    %ebx, %ebx
 74         xorl    %ebp, %ebp
 75         xorl    %edi, %edi
 76 
 77         # Set up LDTR to make Intel VT happy
 78         lldt    %cx                     # 又是一個騙 CPU 的東西

 79         # eax 是 protected_mode_jump 的第一個參數,即 header.S 中定義的 boot_params.hdr.code32_start,即 vmlinux 的入口地址
 80         jmpl    *%eax                   # Jump to the 32-bit entrypoint
 81 
 82         .size   in_pm32, .-in_pm32

4.4 自解壓內核

上節末尾的 jmpl 指令把我們帶入了 vmlinux 的世界。注意到,vmlinux 是壓縮存儲的,因此內核首先的工作就是把真正的內核解壓出來。

根據 Makefile,linux 內核文件有以下幾種:

  • vmlinux: 原始的 linux 內核
  • zImage: 經過 gzip 壓縮后的 vmlinux,解壓到 640KB 內存位置
  • bzImage: 大內核版的 zImage,解壓到 1MB 內存位置,現在我們一般都用這個
  • vmlinuz: 指向 zImage 或 bzImage 的鏈接
  • initrd: init ram disk,用於引導 vmlinuz

循着 Makefile 的蹤跡,我們找到了 arch/x86/boot/compressed/head_32.S,這就是大內核模式下 0x100000 開始的內存內容。

  1. 找到 vmlinux 的入口地址,並將其存入 ebp。
  2. 如果設置了可重入內核,就將 ebp 按照 kernel_alignment 對齊,放入 ebx。
  3. 確定解壓內核的內存地址
  4. 設置棧
  5. 將 vmlinux 復制到安全地區(ebx 指定的地方):保存 esi 到棧中,首先計算出需要復制的字節數目,然后4個字節為一組地復制過去,再從棧中恢復 esi。
  6. 進入 relocated,清空 BSS,初始化解壓函數所用的棧
  7. 將 decompress_kernel 所用的參數入棧:內核加載地址、內核長度、壓縮內核安全地址、堆地址、啟動參數結構體指針。
  8. 調用 decompress_kernel 解壓內核
  9. 如果設置了可重入內核,進行一些 relocate
  10. 跳轉到解壓后的內核。

至此,arch/x86/boot 下的流程基本分析完畢。

4.5 startup_32

vmlinux 是從哪里來的呢?不知道是否是 Linus 有意為我們增加難度 (其實是我對 make 不熟悉),生成 vmlinux 的命令在源碼根目錄的隱藏文件 .vmlinux.cmd 中。

md_vmlinux := ld -m elf_i386 --build-id -o vmlinux -T arch/x86/kernel/vmlinux.lds arch/x86/kernel/head_32.o arch/x86/kern
el/head32.o arch/x86/kernel/init_task.o  init/built-in.o --start-group  usr/built-in.o  arch/x86/mach-generic/built-in.o
arch/x86/kernel/built-in.o  arch/x86/mm/built-in.o  arch/x86/mach-default/built-in.o  arch/x86/crypto/built-in.o  arch/x86
/vdso/built-in.o  kernel/built-in.o  mm/built-in.o  fs/built-in.o  ipc/built-in.o  security/built-in.o  crypto/built-in.o 
block/built-in.o  lib/lib.a  arch/x86/lib/lib.a  lib/built-in.o  arch/x86/lib/built-in.o  drivers/built-in.o  sound/built 
-in.o  arch/x86/pci/built-in.o  arch/x86/oprofile/built-in.o  arch/x86/power/built-in.o  net/built-in.o --end-group .tmp_k
allsyms2.o

真正的內核入口是 arch/x86/kernel/head_32.S (為什么也叫 head_32.S?)

匯編函數 startup_32 依次完成以下動作:

  1. 初始化參數

    • 初始化 GDT。boot_gdt_descr 在數據區中記載了 GDT 表首地址。
      lgdt pa(boot_gdt_descr)
    • 清空 BSS 段
    • 復制實模式中的 boot_params 結構體
    • 復制命令行參數到 boot_command_line (供 init/main.c 使用)
    • 有關虛擬環境的一些配置
  2. 開啟分頁機制

    盡管我們已經在保護模式中,但只有段機制而沒有啟用頁機制。這里設置全局頁目錄與頁表項,並開啟分頁機制。

    下圖示意了 Linux 的分頁機制(From ULK)。

    • 如果啟用了 PAE,即物理地址擴展到 64G 的機制,不作分析。

    • 不然,就是通常的 4G 線性地址空間。__PAGE_OFFSET 是內核編譯時配置的內核地址空間偏移,默認為 3G。默認配置下,進程的用戶態地址空間為 0~3G,高 1G 是內核地址空間。

      全局頁目錄大小為 4KB,每項大小為 4B,可以表示 4MB 的線性范圍,因此頁目錄的大小是 __PAGE_OFFSET >> 20。

      page_pde_offset = (__PAGE_OFFSET >> 20);
    • 初始化頁表首地址 %edi、全局頁目錄地址 %edx、PTE 屬性(頁目錄和頁表的每項 4 Byte 中后 12 位是屬性,這里預先填充 0x67)

      230         movl $pa(pg0), %edi
      231         movl $pa(swapper_pg_dir), %edx
      232         movl $PTE_ATTR, %eax
      
    • 下面是一個雙層循環,外層循環填充頁目錄,內層循環填充頁表。

      233 10:
      		# %edi: 頁表首地址
      234         leal PDE_ATTR(%edi),%ecx                /* Create PDE entry */
      		# 將頁目錄項填充到頁目錄中,%edx 為頁目錄地址
      235         movl %ecx,(%edx)                        /* Store identity PDE entry */
      236         movl %ecx,page_pde_offset(%edx)         /* Store kernel PDE entry */
      		# 填充下一個頁目錄項
      237         addl $4,%edx
      238         movl $1024, %ecx
      239 11:		# 內層循環,填充 4KB 的 PTD
      240         stosl				# es:edi= eax,edi++
      		# 表面上看是將 0x1000 加到屬性上,事實上是 %eax 的后 12 位屬性不變,前面的 20 位頁地址加 1。
      241         addl $0x1000,%eax
      		# 繼續內層循環
      242         loop 11b
      243         /*
      244          * End condition: we must map up to and including INIT_MAP_BEYOND_END
      245          * bytes beyond the end of our own page tables; the +0x007 is
      246          * the attribute bits
      247          */
      		# 計算何時應停止
      248         leal (INIT_MAP_BEYOND_END+PTE_ATTR)(%edi),%ebp
      		# 如果 %eax < %ebp,繼續外層循環
      249         cmpl %ebp,%eax
      250         jb 10b
      
    • 添加頁目錄項的最后一項,頁表地址為 swapper_pg_fixmap,用於 fixmap area

      251         movl %edi,pa(init_pg_tables_end)
      252 
      253         /* Do early initialization of the fixmap area */
      254         movl $pa(swapper_pg_fixmap)+PDE_ATTR,%eax
      255         movl %eax,pa(swapper_pg_dir+0xffc)
      
    • 有關對稱多處理器(SMP)的處理

    • 一些 CPU 參數相關的判斷和處理

    • 開啟分頁機制

      		# 將頁表首地址(swapper_pg_dir)放入 cr3
      331         movl $pa(swapper_pg_dir),%eax
      332         movl %eax,%cr3          /* set the page table pointer.. */
      		# 設置 cr0 的 paging 位,打開 cr0 的分頁機制
      333         movl %cr0,%eax
      334         orl  $X86_CR0_PG,%eax
      335         movl %eax,%cr0          /* ..and set paging (PG) bit */
      		# 目前已經開啟分頁機制,完全進入保護模式。
      336         ljmp $__BOOT_CS,$1f     /* Clear prefetch and normalize %eip */
      
  3. 初始化 Eflags

  4. 初始化中斷向量表

    在實模式中,已經初始化了 IDT,不過現在我們要對保護模式再做一次這樣的工作。由於這段代碼比較長,放在了單獨的函數里。

    485 setup_idt:
    		# 默認中斷處理例程,后面有定義,做一件事情:如果開啟了 CONFIG_PRINTK,就通過 printk 輸出內核信息。
    486         lea ignore_int,%edx
    		# 這里是內核代碼段,注意已經是保護模式了,所以要用代碼段選擇子
    487         movl $(__KERNEL_CS << 16),%eax
    488         movw %dx,%ax            /* selector = 0x0010 = cs */
    489         movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */
    490 
            # 載入 IDT 表的首地址
    491         lea idt_table,%edi
            # 共有 256 個中斷向量
    492         mov $256,%ecx
    493 rp_sidt:
            # 這是一個循環,用默認中斷處理例程初始化 256 個中斷向量
    494         movl %eax,(%edi)
    495         movl %edx,4(%edi)
    496         addl $8,%edi
    497         dec %ecx
    498         jne rp_sidt
    499 
    		# 設置幾個已定義的中斷向量
    		# 宏定義
    500 .macro  set_early_handler handler,trapno
    501         lea \handler,%edx
    502         movl $(__KERNEL_CS << 16),%eax
    503         movw %dx,%ax
    504         movw $0x8E00,%dx        /* interrupt gate - dpl=0, present */
    505         lea idt_table,%edi
    506         movl %eax,8*\trapno(%edi)
    507         movl %edx,8*\trapno+4(%edi)
    508 .endm
    509 		# 預先設置的中斷向量
    510         set_early_handler handler=early_divide_err,trapno=0			# 被零除
    511         set_early_handler handler=early_illegal_opcode,trapno=6		# 操作碼異常
    512         set_early_handler handler=early_protection_fault,trapno=13		# 保護錯誤
    513         set_early_handler handler=early_page_fault,trapno=14		# 缺頁異常
    514		# 后面一段代碼定義了這四個中斷向量的中斷處理例程。
    		# 它們都調用了 early_fault,即將當前狀態、中斷向量號等信息通過 early_printk 或 printk 輸出。
    515         ret
    
  5. 檢查處理器類型

    • 檢查是 486 還是 386
    • get vendor info
    • 如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP
    • save PG, PE, ET
    • check ET for 287/387
  6. 載入 GDT、IDT

    • 重新載入修改 GDT 后的段寄存器
    • DS/ES 包含着默認用戶段
    • 清除 GS、LDT
  7. i386_start_kernel

    如果是 SMP 架構,則由第一個 CPU 調用 start_kernel,其余 CPUs 調用 initialize_secondary

    跳轉到 i386_start_kernel(在 arch/x86/kernel/head32.c)

head_32.S 中的其余代碼是 BSS 段、數據段。

其中,下面這段數據描述了發生未知異常時內核輸出的調試信息。

655 int_msg:
656         .asciz "Unknown interrupt or fault at EIP %p %p %p\n"
657 
658 fault_msg:
659 /* fault info: */
660         .ascii "BUG: Int %d: CR2 %p\n"
661 /* pusha regs: */
662         .ascii "     EDI %p  ESI %p  EBP %p  ESP %p\n"
663         .ascii "     EBX %p  EDX %p  ECX %p  EAX %p\n"
664 /* fault frame: */
665         .ascii "     err %p  EIP %p   CS %p  flg %p\n"
666         .ascii "Stack: %p %p %p %p %p %p %p %p\n"
667         .ascii "       %p %p %p %p %p %p %p %p\n"
668         .asciz "       %p %p %p %p %p %p %p %p\n"

下圖為 x86 體系結構下的段描述符格式(From ULK)。

arch/x86/kernel/head32.c 中的 i386_start_kernel 只有一條語句 start_kernel(),將跳轉到體系結構無關部分的 init/main.c line 534,執行核心數據結構初始化。

5 內核初始化:體系結構無關部分

5.1 核心數據結構初始化

start_kernel 為什么值得開啟新的一章呢?因為我們已經跳出了體系結構相關部分,離開了復雜的匯編代碼,可以在 C 語言的世界里自由翱翔了。

本節摘抄自參考文獻:Linux啟動過程綜述

start_kernel()中調用了一系列初始化函數,以完成kernel本身的設置。這些動作有的是公共的,有的則是需要配置的才會執行的。

  • 輸出Linux版本信息(printk(linux_banner))
  • 設置與體系結構相關的環境(setup_arch())
  • 頁表結構初始化(paging_init())
  • 使用"arch/alpha/kernel/entry.S"中的入口點設置系統自陷入口(trap_init())
  • 使用alpha_mv結構和entry.S入口初始化系統IRQ(init_IRQ())
  • 核心進程調度器初始化(包括初始化幾個缺省的Bottom-half,sched_init())
  • 時間、定時器初始化(包括讀取CMOS時鍾、估測主頻、初始化定時器中斷等,time_init())
  • 提取並分析核心啟動參數(從環境變量中讀取參數,設置相應標志位等待處理,(parse_options())
  • 控制台初始化(為輸出信息而先於PCI初始化,console_init())
  • 剖析器數據結構初始化(prof_buffer和prof_len變量)
  • 核心Cache初始化(描述Cache信息的Cache,kmem_cache_init())
  • 延遲校准(獲得時鍾jiffies與CPU主頻ticks的延遲,calibrate_delay())
  • 內存初始化(設置內存上下界和頁表項初始值,mem_init())
  • 創建和設置內部及通用cache("slab_cache",kmem_cache_sizes_init())
  • 創建uid taskcount SLAB cache("uid_cache",uidcache_init())
  • 創建文件cache("files_cache",filescache_init())
  • 創建目錄cache("dentry_cache",dcache_init())
  • 創建與虛存相關的cache("vm_area_struct","mm_struct",vma_init())
  • 塊設備讀寫緩沖區初始化(同時創建"buffer_head"cache用戶加速訪問,buffer_init())
  • 創建頁cache(內存頁hash表初始化,page_cache_init())
  • 創建信號隊列cache("signal_queue",signals_init())
  • 初始化內存inode表(inode_init())
  • 創建內存文件描述符表("filp_cache",file_table_init())
  • 檢查體系結構漏洞(對於alpha,此函數為空,check_bugs())
  • SMP機器其余CPU(除當前引導CPU)初始化(對於沒有配置SMP的內核,此函數為空,smp_init())
  • 啟動init過程(創建第一個核心線程,調用init()函數,原執行序列調用cpu_idle() 等待調度,init())

至此,基本的核心環境已經建立起來了。

5.2 設備初始化

本節摘抄自參考文獻:Linux啟動過程綜述

init()函數作為核心線程,首先鎖定內核(僅對SMP機器有效),然后調用 do_basic_setup()完成外設及其驅動程序的加載和初始化。過程如下:

  • 總線初始化(比如pci_init())
  • 網絡初始化(初始化網絡數據結構,包括sk_init()、skb_init()和proto_init()三部分,在proto_init()中,將調用protocols結構中包含的所有協議的初始化過程,sock_init())
  • 創建bdflush核心線程(bdflush()過程常駐核心空間,由核心喚醒來清理被寫過的內存緩沖區,當bdflush()由kernel_thread()啟動后,它將自己命名為kflushd)
  • 創建kupdate核心線程(kupdate()過程常駐核心空間,由核心按時調度執行,將內存緩沖區中的信息更新到磁盤中,更新的內容包括超級塊和inode表)
  • 設置並啟動核心調頁線程kswapd(為了防止kswapd啟動時將版本信息輸出到其他信息中間,核心線調用kswapd_setup()設置kswapd運行所要求的環境,然后再創建 kswapd核心線程)
  • 創建事件管理核心線程(start_context_thread()函數啟動context_thread()過程,並重命名為keventd)
  • 設備初始化(包括並口parport_init()、字符設備chr_dev_init()、塊設備 blk_dev_init()、SCSI設備scsi_dev_init()、網絡設備net_dev_init()、磁盤初始化及分區檢查等等,device_setup())
  • 執行文件格式設置(binfmt_setup())
  • 啟動任何使用__initcall標識的函數(方便核心開發者添加啟動函數,do_initcalls())
  • 文件系統初始化(filesystem_setup())
  • 安裝root文件系統(mount_root())

至此do_basic_setup()函數返回init(),在釋放啟動內存段(free_initmem())並給內核解鎖以后,init()打開/dev/console設備,重定向stdin、stdout和stderr到控制台,最后,搜索文件系統中的init程序(或者由init=命令行參數指定的程序),並使用 execve()系統調用加載執行init程序。

init()函數到此結束,內核的引導部分也到此結束了,這個由start_kernel()創建的第一個線程已經成為一個用戶模式下的進程了。此時系統中存在着六個運行實體:

  • start_kernel()本身所在的執行體,這其實是一個"手工"創建的線程,它在創建了init()線程以后就進入cpu_idle()循環了,它不會在進程(線程)列表中出現
  • init線程,由start_kernel()創建,當前處於用戶態,加載了init程序
  • kflushd核心線程,由init線程創建,在核心態運行bdflush()函數
  • kupdate核心線程,由init線程創建,在核心態運行kupdate()函數
  • kswapd核心線程,由init線程創建,在核心態運行kswapd()函數
  • keventd核心線程,由init線程創建,在核心態運行context_thread()函數


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM