ARM Linux 內核啟動是一場精密的接力賽:從 Bootloader 傳遞參數,到內核解壓、建立頁表、啟用 MMU、跳轉 C 語言環境,最終進入 start_kernel() 展開子系統初始化。本文以 ARMv7 (ARMv7-A) 架構為例,結合連結腳本、彙編入口、頁表建立、MMU 啟用等關鍵環節,帶你完整走完內核進入之路。
內核映像 vmlinux 由 arch/arm/kernel/vmlinux.lds 定義佈局:
ENTRY(stext) /* 入口點符號 */
SECTIONS {
. = 0xC0008000; /* 內核虛擬基址 (PAGE_OFFSET) */
.text : { *(.text) } /* 代碼段 */
.data : { *(.data) } /* 已初始化數據 */
.bss : { *(.bss) } /* 未初始化數據 */
/* ... */
}
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)
.proc.info 段,匹配 CPUID,取得處理器描述符 struct proc_info_list,包含 __init 函式指標。swapper_pg_dir),映射內核代碼/數據段、外設 I/O 區域,為啟用 MMU 鋪路。add pc, r10, #12 跳至 proc_info_list.__init(如 __arm920_setup),配置快取、寫緩衝、TLB 等處理器特定功能。__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,此標號在連結腳本中定義於虛擬地址空間。
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 線程,啟動調度
}
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 環境。內核使用 __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 | 載入內核、設定啟動參數、跳轉 stext | U-Boot: bootz/bootm |
| 彙編入口 | CPU/機型識別、建立頁表、啟用 MMU | head.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 狀態如何」,是解決帶板不開機、內核掛起、驅動探針失敗等疑難雜症的鑰匙。