Linux源代碼閱讀——內核引導
目錄
- Linux 引導過程綜述
- BIOS
- POST
- 自舉過程
- Boot loader
- 主引導扇區結構
- GRUB stage1
- GRUB stage2
- 內核初始化:體系結構相關部分
- 內核映像結構
- header.S
- 初始化與保護模式
- 自解壓內核
- startup_32
- 內核初始化:體系結構無關部分
- 核心數據結構初始化
- 設備初始化
1 Linux引導過程綜述
- BIOS
在 i386 平台中,由 BIOS 作最初的引導工作,執行加電自檢、初始化,讀取引導設備的主引導扇區並執行。 - Boot loader(以 GRUB 為例)
MBR 中的、緊隨 MBR 后的 phase 1/1.5 boot loader 載入文件系統中的 phase 2 及其配置,顯示操作系統選擇菜單,執行用戶命令,載入選定的操作系統內核與 initrd。 - 內核初始化:體系結構相關部分
從 header.S 開始,到 main.c 初始化參數,再到 pm.c 進入保護模式,然后載入 vmlinuz 並自解壓,在 startup_32.S 中開啟分頁機制、初始化中斷向量表、檢測 CPU 類型等,完成 x86 體系結構的保護模式初始化。這是本文重點。 - 內核初始化:體系結構無關部分
分為核心數據結構初始化(start_kernel)和設備初始化兩個階段。 - 用戶態初始化
以下內容超出了本文范圍。用戶態的 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 函數過程中實現,主要步驟如下:
- 初始化各種主板芯片組
- 初始化鍵盤控制器
- 初始化中斷向量、中斷服務例程
- 初始化 VGA BIOS 控制器
- 顯示 BIOS 的版本和公司名稱
- 掃描各種介質容量並顯示
- 讀取 CMOS 的啟動順序配置
- 調用 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 像一個微型操作系統。
-
BIOS 加載 GRUB stage1(如果安裝到 MBR)到 0x00007C00.
-
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
- 由於 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 接受用戶命令,載入選定的操作系統內核。
- stage2 的入口點是 asm.s
#ifdef STAGE1_5 # define ABS(x) ((x) - EXT_C(main) + 0x2200) #else # define ABS(x) ((x) - EXT_C(main) + 0x8200) #endif
- 初始化一些變量
- 跳轉到 code_start
- 關中斷,設置段寄存器和堆棧起始地址
- 從實模式切換到保護模式
- 清空 bss 段
- init_bios_info()
-
隨后進入 stage2.c,執行 GRUB 的主要功能。
-
cmain(): 主函數,載入配置文件 menu.lst(GRUB 1)或 grub.cfg(GRUB 2),如果成功載入就進入 run_menu(),顯示菜單,進入循環倒計時,如果超時就進入第一個,如果用戶按了鍵就停止倒計時。用戶作出選擇后,跳轉到 boot_entry(),清空屏幕、獲取入口,通過 find_command 找到的函數指針調用相應的命令。
-
如果沒有成功載入配置文件,就 enter_cmdline(),也是通過 find_command 調用相應的命令。
-
- 每個 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[];
- 常用 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)
- root:掛載分區並設為根分區。
- 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)列表、一個符號列表,一個符號可以是已定義或未定義的。每個已定義的符號有地址。未定義的符號則要在鏈接時從其他文件中尋找其定義。
- 指定輸出文件格式
OUTPUT_FORMAT("elf32-i386", "elf32-i386", "elf32-i386")
- 指定目標體系結構
OUTPUT_ARCH(i386)
- 設置入口點
ENTRY(_start)
- 輸入文件各節到輸出文件的映射
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 | 啟動選項的掩碼。
|
code32_start | 0x214 | 4 | 內核解壓縮前立即跳轉到的 32 位 flat-mode 入口 |
ramdisk_image | 0x218 | 4 | initramfs 的 32 位線性地址 |
cmd_line_ptr | 0x228 | 4 | 內核命令行的 32 位線性地址 |
下面我們迎來了真正的起點(start_of_setup),主要流程為:
- 復位硬盤控制器
- 如果 %ss 無效,重新計算棧指針
- 初始化棧,開中斷
- 將 cs 設置為 ds,與 setup.elf 的入口地址一致
- 檢查主引導扇區末尾標志,如果不正確則跳到 setup_bad
- 清空 bss 段
- 跳到 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。
- realmode_switch_hook():boot_params.hdr 中有 realmode_swtch,記錄了 hook 函數地址,如果有的話就執行之
- reset_coprecessor(): 重啟協處理器
- make_all_interrupts(): 關閉所有舊 PIC 上的中斷。其中的 io_delay 等待 I/O 操作完成。
- setup_idt(): 初始化中斷描述符表 (空的)
- 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 數據結構示意:
- 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 開始的內存內容。
- 找到 vmlinux 的入口地址,並將其存入 ebp。
- 如果設置了可重入內核,就將 ebp 按照 kernel_alignment 對齊,放入 ebx。
- 確定解壓內核的內存地址
- 設置棧
- 將 vmlinux 復制到安全地區(ebx 指定的地方):保存 esi 到棧中,首先計算出需要復制的字節數目,然后4個字節為一組地復制過去,再從棧中恢復 esi。
- 進入 relocated,清空 BSS,初始化解壓函數所用的棧
- 將 decompress_kernel 所用的參數入棧:內核加載地址、內核長度、壓縮內核安全地址、堆地址、啟動參數結構體指針。
- 調用 decompress_kernel 解壓內核
- 如果設置了可重入內核,進行一些 relocate
- 跳轉到解壓后的內核。
至此,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 依次完成以下動作:
-
初始化參數
- 初始化 GDT。boot_gdt_descr 在數據區中記載了 GDT 表首地址。
lgdt pa(boot_gdt_descr)
- 清空 BSS 段
- 復制實模式中的 boot_params 結構體
- 復制命令行參數到 boot_command_line (供 init/main.c 使用)
- 有關虛擬環境的一些配置
- 初始化 GDT。boot_gdt_descr 在數據區中記載了 GDT 表首地址。
-
開啟分頁機制
盡管我們已經在保護模式中,但只有段機制而沒有啟用頁機制。這里設置全局頁目錄與頁表項,並開啟分頁機制。
下圖示意了 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 */
-
-
初始化 Eflags
-
初始化中斷向量表
在實模式中,已經初始化了 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
-
檢查處理器類型
- 檢查是 486 還是 386
- get vendor info
- 如果是 486,就 set AM, WP, NE, MP;如果是 386,就 set MP
- save PG, PE, ET
- check ET for 287/387
-
載入 GDT、IDT
- 重新載入修改 GDT 后的段寄存器
- DS/ES 包含着默認用戶段
- 清除 GS、LDT
-
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()函數