Windows XP Windows 7 Windows 2003 Windows Vista Windows 綜合教學 Linux 系統教學
Windows 10 Windows 8 Windows 2008 Windows NT Windows Server 電腦軟體教學
您现在的位置: Windows教程網 >> Windows教程 >  >> Linux 系統教學 >> Linux 教學

ARM Linux 啟動流程深度剖析:從 Reset 向量到 start_kernel

編輯:Linux 教學

ARM Linux 內核啟動是一場精密的接力賽:從 Bootloader 傳遞參數,到內核解壓、建立頁表、啟用 MMU、跳轉 C 語言環境,最終進入 start_kernel() 展開子系統初始化。本文以 ARMv7 (ARMv7-A) 架構為例,結合連結腳本、彙編入口、頁表建立、MMU 啟用等關鍵環節,帶你完整走完內核進入之路。

1. 連結腳本與映射佈局

內核映像 vmlinux 由 arch/arm/kernel/vmlinux.lds 定義佈局:

ENTRY(stext)                 /* 入口點符號 */
SECTIONS {
    . = 0xC0008000;          /* 內核虛擬基址 (PAGE_OFFSET) */
    .text : { *(.text) }     /* 代碼段 */
    .data : { *(.data) }     /* 已初始化數據 */
    .bss  : { *(.bss) }      /* 未初始化數據 */
    /* ... */
}
  • 虛擬基址 0xC0008000:對應物理記憶體起始 + 0x8000(如物理 0x40000000 → 虛擬 0xC0008000)。
  • MMU 啟用前:CPU 運行於物理地址模式,代碼需使用物理地址或經位置無關代碼 (PIC) 訪問。
  • MMU 啟用後:跳轉至虛擬地址運行,所有符號解析為虛擬地址。

2. 彙編入口:arch/arm/kernel/head.S

ENTRY(stext)
    mov r12, r0              @ 保存 Bootloader 傳入的 r0 (通常為 0)
    mrc p15, 0, r9, c0, c0   @ 讀取 CPUID
    bl  __lookup_processor_type   @ 識別處理器類型
    bl  __lookup_architecture_type @ 識別機器類型
    bl  __create_page_tables      @ 建立初始頁表
    adr lr, __ret
    add pc, r10, #12         @ 跳轉至處理器特定初始化 (__proc_info.init)

關鍵子流程

  • __lookup_processor_type:遍歷 .proc.info 段,匹配 CPUID,取得處理器描述符 struct proc_info_list,包含 __init 函式指標。
  • __create_page_tables:建立臨時頁表(swapper_pg_dir),映射內核代碼/數據段、外設 I/O 區域,為啟用 MMU 鋪路。
  • 跳轉處理器初始化:add pc, r10, #12 跳至 proc_info_list.__init(如 __arm920_setup),配置快取、寫緩衝、TLB 等處理器特定功能。

3. 返回通用流程:__ret -> __mmap_switched

__ret:
    ldr lr, =__mmap_switched
    mcr p15, 0, r0, c1, c0, 0  @ 啟用 MMU (設定 CP15 c1 控制寄存器 M 位)
    mov r0, r0                 @ 指令流水線清空 (CP15 指令後需 ISB/DSB)
    mov r0, r0
    mov pc, lr                 @ 跳轉至 __mmap_switched (此時已在虛擬地址空間)

此處 mcr p15 指令啟用 MMU,完成「物理 → 虛擬」地址翻譯的切換。隨後跳轉至 __mmap_switched,此標號在連結腳本中定義於虛擬地址空間。

4. C 語言世界:start_kernel()

asmlinkage void __init start_kernel(void) {
    lock_kernel();                 // 大核心鎖 (舊版 BKL)
    printk(linux_banner);          // 印出版本橫幅
    setup_arch(&command_line);     // 架構相關初始化 (記憶體、時鐘、中斷控制器)
    parse_options(command_line);   // 解析啟動參數
    trap_init();                   // 陷阱/異常向量初始化
    init_IRQ();                    // 中斷子系統初始化
    sched_init();                  // 調度器初始化
    time_init();                   // 時間子系統初始化
    // ... 更多子系統初始化 ...
    rest_init();                   // 創建 init 線程,啟動調度
}

5. 關鍵里程碑:rest_init() 與用戶空間誕生

static void __init rest_init(void) {
    kernel_thread(init, NULL, CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
    // 創建 PID 1 的 init 任務 (內核線程)
    unlock_kernel();
    schedule();  // 觸發首次調度,切換到 init 任務
    cpu_idle();  // 當前線程 (idle 線程) 進入空閒迴圈
}
  • init 線程 (PID 1) 在 kernel_init() 中執行:掛載根文件系統、執行 /sbin/init (或 /etc/init, /bin/init, /bin/sh)。
  • 若內核命令行指定 init=/bin/sh,則直接進入 Shell 環境。
  • Systemd/SysVinit 進程接管後,用戶空間正式啟動。

6. 內核初始化調用鏈:__initcall 機制

內核使用 __initcall 系列宏將初始化函式註冊至 .initcall.init 段,do_initcalls() 依序執行:

#define __initcall(fn) \
    static initcall_t __initcall_##fn __attribute__((__section__(".initcall.init"))) = fn

// 使用範例:
static int __init my_driver_init(void) { ... }
module_init(my_driver_init);  // 靜態編譯進內核時展開為 __initcall(my_driver_init)

驅動若為 built-in (y),則透過 __initcall 自動調用;若為模組 (m),則由 insmod 觸發 module_init。

總結:啟動流程關鍵節點速查

階段關鍵動作關鍵代碼/文件
Bootloader載入內核、設定啟動參數、跳轉 stextU-Boot: bootz/bootm
彙編入口CPU/機型識別、建立頁表、啟用 MMUhead.S, __create_page_tables
MMU 啟用切換至虛擬地址空間mcr p15, c1 設定 M 位
C 入口子系統逐級初始化start_kernel() in init/main.c
休眠初始化創建 init 線程、啟動調度器rest_init() -> kernel_thread(init)
用戶空間執行 init 程序 (systemd/sysvinit)/sbin/init -> 服務啟動

ARM Linux 啟動是軟硬體協同的精密編排。理解每個階段的「誰在跑、地址空間是什麼、MMU 狀態如何」,是解決帶板不開機、內核掛起、驅動探針失敗等疑難雜症的鑰匙。

Copyright © Windows教程網 All Rights Reserved