当前位置:首页>Linux>第5篇04-Linux内核镜像的加载和启动

第5篇04-Linux内核镜像的加载和启动

  • 2026-09-02 23:09:32
第5篇04-Linux内核镜像的加载和启动

QEMU上的OP-TEE:普通世界的就绪之旅

本文是「QEMU上的OP-TEE——一次完整的启动之旅」系列第4篇,带你深入分析Linux内核镜像的加载和启动过程。


前言

在前面的文章中,我们分析了BL1、BL2、BL31和OP-TEE OS的启动过程。现在,安全世界已经准备就绪,接下来轮到普通世界登场了。

普通世界的核心是Linux内核,它运行在S-EL1(Secure Exception Level 1)级别,负责管理普通应用程序、文件系统、网络等资源。Linux内核的启动过程是整个系统启动链的最后一环,也是最复杂的一环。

本文将深入分析Linux内核镜像的加载和启动过程,带你了解普通世界是如何准备就绪的。


一、U-Boot:普通世界的引导者

1.1 U-Boot的启动

在OP-TEE QEMU环境中,U-Boot作为BL33运行在S-EL1级别。它是普通世界的引导程序,负责:

    1.2 U-Boot的启动流程

    U-Boot的启动过程分为多个阶段:

    U-Boot启动流程:┌─────────────────────────────────────────────────────────────┐│ SPL (Secondary Program Loader) ││ ││ 1. 初始化CPU ││ 2. 初始化DRAM ││ 3. 加载U-Boot主体 │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ U-Boot主体 ││ ││ 1. 初始化控制台 ││ 2. 初始化网络、存储等 ││ 3. 加载Linux内核 ││ 4. 启动Linux内核 │└─────────────────────────────────────────────────────────────┘

    二、加载Linux内核镜像

    2.1 bootz命令

    在U-Boot中,使用bootz命令来启动Linux zImage:

    # U-Boot命令行=> bootz 0x40008000 - 0x41000000

    参数说明:

    • 0x40008000:zImage在内存中的地址
    • -:没有initrd(初始内存盘)
    • 0x41000000:设备树(DTB)的地址

    2.2 bootz命令的实现

    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;}

    2.3 zImage格式

    zImage是Linux内核的压缩格式,它包含:

    zImage结构:┌─────────────────────────────┐│ 自举代码 │ ← 解压内核├─────────────────────────────┤│ 压缩的内核 │ ← gzip压缩├─────────────────────────────┤│ 内核信息 │ ← 头部信息└─────────────────────────────┘

    zImage的自举代码会:


      三、Linux内核的启动过程

      3.1 入口点:stext

      Linux内核的入口点定义在linux/arch/arm/kernel/head.S中:

      ENTRY(stext) @ 确保在ARM模式下 ARM_BE8(setend be) @ 确保在SVC模式下,禁用所有中断 safe_svcmode_maskall r9 @ 获取处理器ID mrc p15, 0, r9, c0, c0 bl __lookup_processor_type @ 计算物理地址偏移 adr_l r8, _text sub r8, r8, #TEXT_OFFSET @ 验证ATAGS/DTB bl __vet_atags @ 创建页表 bl __create_page_tables @ 处理器初始化 bl __cpu_flush @ 启用MMU bl __turn_mmu_on @ 跳转到C语言入口点 b __mmap_switchedENDPROC(stext)

      3.2 关键初始化步骤

      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函数。


      四、C语言入口点:start_kernel

      4.1 __mmap_switched函数

      ENTRY(__mmap_switched) str r9, [r2] @ 存储机器号 str r3, [r4] @ 存储DTB指针 @ 清零BSS段 adr r3, __bss_start mov r4, #01: cmp r3, r4 strcc r4, [r3], #4 bcc 1b @ 跳转到C语言入口点 ldr r2, =__mmap_switched_data ldmia r2, {r3, r4, sp} ldr r1, =__switch_data bx r1ENDPROC(__mmap_switched)

      4.2 start_kernel函数

      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();}

      4.3 关键初始化步骤

      setup_arch(&command_line);

      这个函数会:

      • 解析设备树(DTB)
      • 初始化内存管理
      • 设置中断控制器
      • 初始化平台特定的硬件
      mm_init();

      这个函数会:

      • 初始化物理内存管理器
      • 初始化虚拟内存管理器
      • 设置内存分配器
      sched_init();

      这个函数会:

      • 初始化进程调度器
      • 设置调度策略
      • 初始化运行队列
      console_init();

      这个函数会:

      • 初始化串口驱动
      • 注册控制台设备
      • 启用控制台输出
      rest_init();

      这个函数会:

      • 创建内核线程kernel_init
      • 创建内核线程kthreadd
      • 启动调度器

      五、设备树(DTB)

      5.1 设备树的作用

      设备树(Device Tree Blob)是一个描述硬件的数据结构,它告诉Linux内核:

      • 系统中有哪些硬件设备
      • 这些设备的寄存器地址
      • 这些设备的中断号
      • 这些设备的时钟配置

      5.2 QEMU的设备树

      在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

      5.3 设备树的传递

      U-Boot会将设备树地址传递给Linux内核:

      /* 在U-Boot中 */images->ft_addr = (void *)simple_strtoul(argv[2], NULL, 16);/* 传递给内核 */r2 = images->ft_addr; /* DTB地址 */

      六、根文件系统挂载

      6.1 内核命令行参数

      Linux内核通过命令行参数来配置根文件系统:

      # 内核命令行console=ttyAMA0 root=/dev/ram0 rdinit=/linuxrc

      参数说明:

      • console=ttyAMA0:使用串口作为控制台
      • root=/dev/ram0:使用RAM磁盘作为根文件系统
      • rdinit=/linuxrc:指定init程序

      6.2 initramfs/initrd

      在OP-TEE QEMU环境中,使用initramfs(初始内存盘)作为根文件系统:

      initramfs结构:├── bin/│ ├── sh│ ├── ls│ └── ...├── etc/│ ├── passwd│ ├── init.d/│ └── ...├── lib/│ ├── *.so│ └── ...├── sbin/│ ├── init│ └── ...└── usr/ ├── bin/ └── lib/

      6.3 根文件系统的挂载

      /* 在内核中 */void __init vfs_caches_init(void){ /* 挂载rootfs */ rootfs_init(); /* 挂载devtmpfs */ devtmpfs_init(); /* 挂载sysfs */ sysfs_init(); /* 挂载proc */ proc_init();}

      七、OP-TEE驱动加载

      7.1 OP-TEE驱动的作用

      Linux内核需要通过OP-TEE驱动与OP-TEE OS通信:

      Linux内核 ←→ OP-TEE驱动 ←→ SMC调用 ←→ OP-TEE OS

      OP-TEE驱动负责:

      • 发送SMC调用到OP-TEE OS
      • 接收OP-TEE OS的响应
      • 管理共享内存
      • 处理中断

      7.2 OP-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);

      7.3 SMC调用流程

      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调用 │└─────────────────────────────────────────────────────────────┘

      九、调试技巧

      9.1 启用Linux内核调试

      # 在内核配置中启用调试CONFIG_DEBUG_INFO=yCONFIG_GDB_SCRIPTS=yCONFIG_KGDB=y

      9.2 使用GDB调试Linux内核

      # 启动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

      9.3 查看内核日志

      # 在Linux系统中查看内核日志dmesg# 实时查看日志tail -f /var/log/kern.log

      9.4 内核命令行参数调试

      # 添加调试参数# 在U-Boot中设置setenv bootargs ”console=ttyAMA0 root=/dev/ram0 rdinit=/linuxrc debug”# 或者在设备树中修改chosen { bootargs = ”console=ttyAMA0 root=/dev/ram0 rdinit=/linuxrc debug”;};

      十、常见问题

      Q1:为什么Linux内核要运行在S-EL1?

      A:在OP-TEE环境中,Linux内核运行在S-EL1级别,而不是普通的EL1级别。这是因为:

      • OP-TEE OS运行在S-EL1
      • Linux内核需要通过SMC调用与OP-TEE OS通信
      • 安全世界和普通世界共享同一个EL1级别,但有不同的安全状态

      Q2:设备树(DTB)有什么作用?

      A:设备树描述了系统的硬件配置,包括:

      • CPU架构和核心数
      • 内存大小和地址
      • 外设的寄存器地址和中断号
      • 时钟和电源管理配置

      Q3:如何修改内核命令行参数?

      A:可以通过以下方式修改:

        Q4:为什么需要initramfs?

        A:initramfs(初始内存盘)的作用是:

        • 提供一个最小的根文件系统
        • 加载必要的驱动程序
        • 挂载真正的根文件系统
        • 启动init程序

        Q5:OP-TEE驱动是如何工作的?

        A:OP-TEE驱动通过SMC调用与OP-TEE OS通信:


          总结

          本文深入分析了Linux内核镜像的加载和启动过程,我们了解了:

            Linux内核的启动是整个系统启动链的最后一步,也是最复杂的一步。在下一篇文章中,我们将继续分析rootfs挂载与OP-TEE驱动加载的细节。


            下一篇预告:我们将分析rootfs挂载与OP-TEE驱动加载的详细过程,看看系统是如何完全就绪的。


            💡 小贴士:Linux内核的启动过程虽然复杂,但核心逻辑是:解压内核 → 初始化硬件 → 挂载文件系统 → 启动用户空间程序。理解了这个流程,就掌握了普通世界的核心。

            最新文章

            随机文章