本文是「QEMU上的OP-TEE——一次完整的启动之旅」系列第4篇,带你深入分析Linux内核镜像的加载和启动过程。
在前面的文章中,我们分析了BL1、BL2、BL31和OP-TEE OS的启动过程。现在,安全世界已经准备就绪,接下来轮到普通世界登场了。
普通世界的核心是Linux内核,它运行在S-EL1(Secure Exception Level 1)级别,负责管理普通应用程序、文件系统、网络等资源。Linux内核的启动过程是整个系统启动链的最后一环,也是最复杂的一环。
本文将深入分析Linux内核镜像的加载和启动过程,带你了解普通世界是如何准备就绪的。
在OP-TEE QEMU环境中,U-Boot作为BL33运行在S-EL1级别。它是普通世界的引导程序,负责:
U-Boot的启动过程分为多个阶段:
U-Boot启动流程:┌─────────────────────────────────────────────────────────────┐│ SPL (Secondary Program Loader) ││ ││ 1. 初始化CPU ││ 2. 初始化DRAM ││ 3. 加载U-Boot主体 │└─────────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ U-Boot主体 ││ ││ 1. 初始化控制台 ││ 2. 初始化网络、存储等 ││ 3. 加载Linux内核 ││ 4. 启动Linux内核 │└─────────────────────────────────────────────────────────────┘
在U-Boot中,使用bootz命令来启动Linux zImage:
# U-Boot命令行=> bootz 0x40008000 - 0x41000000
参数说明:
bootz命令的实现在u-boot/cmd/bootz.c中:
static int bootz_start(struct cmd_tbl *cmdtp, int flag, int argc,char *const argv[], bootm_headers_t *images){int ret;ulong zi_start, zi_end;/* 设置内核入口点 */if (!argc) {images->ep = image_load_addr;} else {images->ep = simple_strtoul(argv[0], NULL, 16);}/* 验证zImage格式 */ret = bootz_setup(images->ep, &zi_start, &zi_end);if (ret != 0)return 1;/* 查找其他镜像(initrd、DTB) */if (bootm_find_images(flag, argc, argv, images->ep, zi_end - zi_start))return 1;return 0;}int do_bootz(struct cmd_tbl *cmdtp, int flag, int argc, char *const argv[]){int ret;/* 调用bootz_start */if (bootz_start(cmdtp, flag, argc, argv, &images))return 1;/* 禁用中断 */bootm_disable_interrupts();/* 设置操作系统类型 */images.os.os = IH_OS_LINUX;/* 执行启动状态机 */ret = do_bootm_states(cmdtp, flag, argc, argv,BOOTM_STATE_RAMDISK |BOOTM_STATE_OS_PREP |BOOTM_STATE_OS_FAKE_GO |BOOTM_STATE_OS_GO,&images, 1);return ret;}
zImage是Linux内核的压缩格式,它包含:
zImage结构:┌─────────────────────────────┐│ 自举代码 │ ← 解压内核├─────────────────────────────┤│ 压缩的内核 │ ← gzip压缩├─────────────────────────────┤│ 内核信息 │ ← 头部信息└─────────────────────────────┘
zImage的自举代码会:
Linux内核的入口点定义在linux/arch/arm/kernel/head.S中:
ENTRY(stext)@ 确保在ARM模式下ARM_BE8(setend be)@ 确保在SVC模式下,禁用所有中断safe_svcmode_maskall r9@ 获取处理器IDmrc p15, 0, r9, c0, c0bl __lookup_processor_type@ 计算物理地址偏移adr_l r8, _textsub r8, r8, #TEXT_OFFSET@ 验证ATAGS/DTBbl __vet_atags@ 创建页表bl __create_page_tables@ 处理器初始化bl __cpu_flush@ 启用MMUbl __turn_mmu_on@ 跳转到C语言入口点b __mmap_switchedENDPROC(stext)
safe_svcmode_maskall r9这行代码确保CPU在SVC模式下运行,并禁用所有中断。
bl __lookup_processor_type这个函数会根据处理器ID查找对应的处理器信息结构。
bl __create_page_tables这个函数会创建初始页表,为启用MMU做准备。
bl __turn_mmu_on这个函数会启用MMU,开始使用虚拟内存。
b __mmap_switched跳转到arch/arm/kernel/head-common.S中的__mmap_switched函数。
ENTRY(__mmap_switched)str r9, [r2] @ 存储机器号str r3, [r4] @ 存储DTB指针@ 清零BSS段adr r3, __bss_startmov r4, #01: cmp r3, r4strcc r4, [r3], #4bcc 1b@ 跳转到C语言入口点ldr r2, =__mmap_switched_dataldmia r2, {r3, r4, sp}ldr r1, =__switch_databx r1ENDPROC(__mmap_switched)
start_kernel是Linux内核的C语言入口点,定义在linux/init/main.c中:
asmlinkage __visible void __init __no_sanitize_address start_kernel(void){set_task_stack_end_magic(&init_task);smp_setup_processor_id();debug_objects_early_init();/* 设置本地IRQ禁用 */local_irq_disable();/* 设置早期控制台 */setup_arch(&command_line);setup_command_line(command_line);setup_nr_cpu_ids();setup_per_cpu_areas();/* 建立内存管理 */build_all_zonelists(NULL);page_address_init();/* 打印内核版本 */pr_notice(”%s”, linux_banner);/* 初始化各种子系统 */setup_arch(&command_line);trap_init();mm_init();/* 初始化调度器 */sched_init();/* 初始化中断 */init_IRQ();time_init();/* 初始化控制台 */console_init();/* 初始化进程管理 */pid_init();files_init();/* 挂载根文件系统 */vfs_caches_init();/* 启动init进程 */rest_init();}
setup_arch(&command_line);这个函数会:
mm_init();这个函数会:
sched_init();这个函数会:
console_init();这个函数会:
rest_init();这个函数会:
设备树(Device Tree Blob)是一个描述硬件的数据结构,它告诉Linux内核:
在QEMU环境中,设备树由QEMU自动生成,描述了模拟的硬件:
设备树结构:├── /│ ├── chosen/│ │ └── bootargs = ”console=ttyAMA0 root=/dev/ram0”│ ├── cpus/│ │ └── cpu@0/│ │ └── compatible = ”arm,cortex-a15”│ ├── memory@│ │ └── reg = <0x40000000 0x40000000>│ ├── gic@...│ │ └── interrupt-controller│ ├── serial@...│ │ └── ARM AMBA PL011 UART│ └── virtio@...│ └── VirtIO MMIO
U-Boot会将设备树地址传递给Linux内核:
/* 在U-Boot中 */images->ft_addr = (void *)simple_strtoul(argv[2], NULL, 16);/* 传递给内核 */r2 = images->ft_addr; /* DTB地址 */
Linux内核通过命令行参数来配置根文件系统:
# 内核命令行console=ttyAMA0 root=/dev/ram0 rdinit=/linuxrc
参数说明:
在OP-TEE QEMU环境中,使用initramfs(初始内存盘)作为根文件系统:
initramfs结构:├── bin/│ ├── sh│ ├── ls│ └── ...├── etc/│ ├── passwd│ ├── init.d/│ └── ...├── lib/│ ├── *.so│ └── ...├── sbin/│ ├── init│ └── ...└── usr/├── bin/└── lib/
/* 在内核中 */void __init vfs_caches_init(void){/* 挂载rootfs */rootfs_init();/* 挂载devtmpfs */devtmpfs_init();/* 挂载sysfs */sysfs_init();/* 挂载proc */proc_init();}
Linux内核需要通过OP-TEE驱动与OP-TEE OS通信:
Linux内核 ←→ OP-TEE驱动 ←→ SMC调用 ←→ OP-TEE OSOP-TEE驱动负责:
OP-TEE驱动在内核启动时自动加载:
/* 在设备树中 */optee@0 {compatible = ”linaro,optee-tz”;method = ”smc”;reg = <0x0 0x0 0x0 0x2000000>;};/* 在内核中 */static int __init optee_driver_init(void){/* 注册平台驱动 */return platform_driver_register(&optee_driver);}subsys_initcall(optee_driver_init);
SMC调用流程:┌─────────────────────────────────────────────────────────────┐│ Linux内核 ││ ││ 1. 调用optee_smc_call() ││ 2. 设置SMC参数 ││ 3. 执行SMC指令 │└─────────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ BL31安全监控器 ││ ││ 1. 接收SMC调用 ││ 2. 切换到安全世界 ││ 3. 调用OP-TEE OS │└─────────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ OP-TEE OS ││ ││ 1. 处理SMC请求 ││ 2. 执行安全操作 ││ 3. 返回结果 │└─────────────────────────────────────────────────────────────┘
让我们用一张图来总结Linux内核的启动流程:
┌─────────────────────────────────────────────────────────────┐│ U-Boot启动 ││ ││ 1. 初始化硬件 ││ 2. 加载Linux内核(zImage) ││ 3. 加载设备树(DTB) ││ 4. 跳转到内核入口点 │└─────────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ Linux内核启动 ││ ││ 1. stext: 入口点 ││ - CPU模式设置 ││ - 处理器类型查找 ││ - 页表创建 ││ - MMU启用 ││ ││ 2. start_kernel: C语言入口 ││ - 架构初始化 ││ - 内存管理初始化 ││ - 调度器初始化 ││ - 中断初始化 ││ - 控制台初始化 ││ ││ 3. rest_init: 启动init进程 ││ - 创建内核线程 ││ - 启动调度器 │└─────────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ 根文件系统挂载 ││ ││ 1. 挂载rootfs ││ 2. 挂载devtmpfs ││ 3. 挂载sysfs ││ 4. 挂载proc │└─────────────────────────────────────────────────────────────┘│▼┌─────────────────────────────────────────────────────────────┐│ OP-TEE驱动加载 ││ ││ 1. 注册平台驱动 ││ 2. 初始化共享内存 ││ 3. 配置SMC调用 │└─────────────────────────────────────────────────────────────┘
# 在内核配置中启用调试CONFIG_DEBUG_INFO=yCONFIG_GDB_SCRIPTS=yCONFIG_KGDB=y
# 启动QEMU(会等待GDB连接)make run-only QEMU_EXTRA_ARGS=”-S -s”# 在另一个终端启动GDBarm-linux-gnueabihf-gdb# 加载内核符号文件file vmlinux# 连接到QEMUtarget remote localhost:1234# 在入口点设置断点break stext# 在start_kernel设置断点break start_kernel
# 在Linux系统中查看内核日志dmesg# 实时查看日志tail -f /var/log/kern.log
# 添加调试参数# 在U-Boot中设置setenv bootargs ”console=ttyAMA0 root=/dev/ram0 rdinit=/linuxrc debug”# 或者在设备树中修改chosen {bootargs = ”console=ttyAMA0 root=/dev/ram0 rdinit=/linuxrc debug”;};
A:在OP-TEE环境中,Linux内核运行在S-EL1级别,而不是普通的EL1级别。这是因为:
A:设备树描述了系统的硬件配置,包括:
A:可以通过以下方式修改:
A:initramfs(初始内存盘)的作用是:
A:OP-TEE驱动通过SMC调用与OP-TEE OS通信:
本文深入分析了Linux内核镜像的加载和启动过程,我们了解了:
Linux内核的启动是整个系统启动链的最后一步,也是最复杂的一步。在下一篇文章中,我们将继续分析rootfs挂载与OP-TEE驱动加载的细节。
下一篇预告:我们将分析rootfs挂载与OP-TEE驱动加载的详细过程,看看系统是如何完全就绪的。
💡 小贴士:Linux内核的启动过程虽然复杂,但核心逻辑是:解压内核 → 初始化硬件 → 挂载文件系统 → 启动用户空间程序。理解了这个流程,就掌握了普通世界的核心。