内核启动流程
芯片上电之后会执行固化在芯片0地址处的Boot_ROM代码,这段代码:
根据BOOT_MODE引脚的电平决定启动方式
10 : 从内部启动
根据BOOT_CFG引脚的电平决定从哪个存储介质中读取代码
BOOT_CFG的引脚共有24个,其中的四个(BOOT_CFG1[7 : 4])决定设备类型,010X:SD; 011X:emmc; ......
然后读取前4K的uboot.imx放在OCRAM中(包含IVT、boot_data、DCD)
DCD:包含DDR内存的配置信息、PLL时钟初始化的信息
boot_data:包含uboot.bin的长度和目标地址等信息
IVT:包含entry地址(程序入口地址0X87800000)(start.S)
4. 然后Boot_ROM代码会根据DCD中的信息初始化DDR内存,根据boot_data中的信息读取uboot.bin全文放在DDR中,根据IVT跳转到程序入口地址0X87800000去执行start.S
至此,Boot_ROM将CPU的控制权交给uboot,uboot会从start.S开始进行下面的操作:
| 阶段 | 代码位置 | 主要任务 |
|---|---|---|
| 汇编初始化 | start.S | 设置向量表、关闭 MMU/Cache、初始化栈 |
| C 早期初始化 | board_init_f | 初始化串口、规划内存布局 |
| 重定位 | relocate_code | 搬移自身、修正地址、清空 .bss |
| 完整初始化 | board_init_r | 初始化驱动、加载环境变量、进入命令行 |
| 引导内核 | bootz/bootm | 加载内核与 DTB、跳转到 Linux 入口 |
第一阶段:汇编级初始化(start.S)
这是 U-Boot 的入口,全部由汇编代码完成,主要做三件关键事:
设置异常向量表:在
0x87800000开头放置 ARM 中断向量表,并把 VBAR 寄存器指向这里,为后续中断处理做准备。切换到 SVC(管理模式):确保 CPU 运行在最高特权级,并禁用 FIQ 和 IRQ 中断,防止早期阶段被意外打断。
关闭 MMU 和 Cache:此时 DDR 刚初始化完成,MMU 和 Cache 尚未建立,直接关闭它们是安全的。这一操作也确保后续代码执行在“平坦”物理地址上,避免缓存一致性问题。
初始化栈指针(SP):为 C 语言环境准备临时栈,通常指向
0x87800000上方的一段空间。
第二阶段:C 语言环境初始化(board_init_f)
汇编完成后,跳转到 C 语言入口board_init_f,执行早期初始化:
初始化串口(UART),打印第一行启动信息(如
U-Boot 2020.04 ...)。检测 DDR 大小,并规划内存布局,决定将 U-Boot 自身重定位到哪个地址(通常就是当前的
0x87800000,或更高的0x8xxxxxxx)。初始化时钟、GPIO 等基础外设,但此时设备驱动模型尚未完全建立。
第三阶段:重定位(relocate_code)
这是 U-Boot 启动中最核心的一步,虽然我们已经在0x87800000运行,但 U-Boot 可能还会进行一次自我搬移(取决于配置):
将 U-Boot 镜像从当前地址(
0x87800000)复制到规划好的“最终运行地址”(可能是0x8FF00000等高端地址)。修正
.rel.dyn段中的重定位表,将所有绝对地址跳转修正到新地址。清空
.bss段(未初始化的全局变量区域)。跳转到新地址的入口,继续执行。
第四阶段:完整初始化与命令行(board_init_r)
重定位完成后,进入board_init_r,这是 U-Boot 的“完全体”:
初始化设备驱动模型(DM),扫描总线上的所有设备(如 eMMC、网络、USB 等)。
加载环境变量(
bootcmd),从存储介质(eMMC/SD)或默认配置中读取。初始化网络协议栈,支持 DHCP/TFTP 等网络引导方式。
进入命令行交互(
=>提示符),等待用户输入命令;或者自动执行bootcmd中的指令,开始引导 Linux 内核。
最终:引导 Linux 内核
当用户执行bootz或bootm命令时,U-Boot 会:
从指定地址(如
0x80008000)加载内核镜像(zImage)。从指定地址(如
0x83000000)加载设备树(DTB)。设置内核启动参数(
r0=0, r1=machine_id, r2=dtb_addr)。关闭 MMU 和 Cache,跳转到内核入口(
0x80008000),将控制权交给 Linux。
此后,U-Boot 的使命彻底完成,Linux 内核开始启动。
当U-Boot执行bootz或bootm,跳转到0x80008000这个地址后,Linux内核就正式接管了CPU的控制权。
第一步:内核入口与解压(汇编阶段)
0x80008000这个地址,是U-Boot和Linux内核约定的“碰头地点”。U-Boot在跳转前,会确保内核镜像(zImage或uImage)已经被加载到这里。
约定俗成的地址:
0x80008000是ARM架构下非常常用的内核加载地址。这个值在制作内核镜像时,通过mkimage工具的-e(入口点)参数指定,并写入了镜像头部。U-Boot执行bootm命令时,会解析这个头部,最终跳转到这个地址。真正的入口:这个地址的代码通常是
arch/arm/boot/compressed/head.S。这是一段位置无关的汇编代码,主要负责检查自身是否在正确位置,如果不是则进行自我搬运,然后解压真正的大内核映像(vmlinux),最后跳转到解压后的内核入口(通常叫stext)。
第二步:内核主体初始化(汇编转C,开启MMU)
跳转到解压后的stext(定义在arch/arm/kernel/head.S)后,内核开始执行自身的初始化。这部分任务主要是:
验证硬件信息:检查U-Boot传入的设备树(DTB)或ATAGS是否合法。
创建临时页表:为开启MMU(内存管理单元)做准备。
开启MMU:使能虚拟地址,从此内核运行在虚拟地址空间。
跳转到C世界:最后,它会跳转到
__mmap_switched,这个函数会完成数据段和BSS段的初始化,然后执行最后一条汇编指令,跳转到start_kernel(),从此进入纯C语言的世界,并且不再返回。
第三步:start_kernel()—— Linux的“大脑”开始运转
start_kernel()(在init/main.c中)是整个Linux内核初始化的核心,它像一个大总管,会按顺序调用数百个初始化函数,搭建起系统的骨架。
其中几个关键函数包括:
setup_arch():进行架构相关的特定设置,比如解析U-Boot传递过来的设备树,获取内存信息等。mm_init():初始化内存管理子系统,这是内核最核心的模块之一。sched_init():初始化进程调度器,从此内核有了管理多任务的能力。init_IRQ():初始化中断处理系统。console_init():初始化控制台,在此之前,所有的printk打印信息都只能缓存在内存里,这个函数执行后我们才能在串口上看到启动日志。
第四步:rest_init()—— 第一个“用户”进程诞生
start_kernel()的最后一步是调用rest_init(),它完成了从内核态到用户态的转变。
创建
kernel_init线程(PID=1):这个内核线程最终会执行/init程序(或者由init=参数指定的程序),变成所有用户空间进程的祖先——init进程。创建
kthreadd线程(PID=2):这是内核的守护进程,专门负责创建和管理其他的内核线程。进入空闲循环:完成以上创建后,最初的CPU核心会进入
cpu_idle()函数,变成系统的空闲进程,在没有其他任务需要执行时运行。
至此,Linux内核的启动核心流程就完成了。init进程会依据配置文件(如/etc/inittab或/etc/systemd/system/)启动系统服务,最终呈现一个可操作的控制台或图形界面。
需要注意的是,该流程就是一套完整的SPL->uboot->kernal的启动流程,因为,在NXP i.MX这类芯片中,Boot_ROM内部集成了SPL的功能。它直接通过读取
uboot.imx头部的DCD(设备配置数据)来初始化DDR,并把整个U-Boot拽进内存。