面试高频题:请描述嵌入式 Linux 系统从上电到 Shell 登录的完整启动流程。
本文以 iMX6ULL 为例,覆盖四个阶段:BootROM → U-Boot → Linux Kernel → init。
整体流程概览
上电/复位
│
▼
【第一阶段】BootROM(片内 ROM,固化代码)
- CPU 复位,PC 指向片内 ROM 所在地址(硬件地址译码映射)
- 检测启动模式(BOOT_MODE 引脚 / eFuse)
- 初始化最小时钟(供自身运行),从启动设备读取 U-Boot 镜像头
- 执行 DCD 序列:完整初始化 DDR 控制器(400MHz 全速)
- 将完整 U-Boot 加载到 DDR,跳转到 U-Boot 入口
│
▼
【第二阶段】U-Boot
- 汇编阶段:切换到 SVC32 模式、关 MMU/Cache、在 OCRAM 建栈
- board_init_f:串口、时钟、DDR 完整初始化
- 代码重定位:将自身从 DDR 低地址搬到顶端,修正所有绝对地址
- board_init_r:网卡、Flash、环境变量等外设初始化
- main_loop:倒计时,超时后执行 bootcmd
- TFTP/MMC 加载 zImage + DTB 到 DDR 指定地址
- bootz:解压内核,传递启动参数,跳转内核入口
│
▼
【第三阶段】Linux Kernel
- head.S:确认 SVC 模式、关 MMU、建临时页表、开 MMU
- start_kernel():内核 C 语言入口,各子系统初始化
- rest_init():创建 kernel_init 线程(PID=1)
- kernel_init:挂载 rootfs,exec /sbin/init
│
▼
【第四阶段】init 进程(用户空间)
- BusyBox init / systemd:读取 inittab/unit 文件
- 挂载 /proc、/sys、/dev
- 启动各种服务和 Shell
│
▼
Shell 登录提示符
第一阶段:BootROM
作用
BootROM 是芯片出厂固化在片内 ROM 里的启动代码,用户无法修改。它是整个启动链的信任根(Root of Trust)。
iMX6ULL BootROM 流程
1. 上电复位,CPU 从片内 ROM 开始执行
ARM Cortex-A7 复位后,PC 指向固定地址(由芯片硬件配置决定),开始执行 BootROM 代码。
❓ 疑问一:CPU 从 0 地址开始执行,”映射到内部 ROM”是什么意思?
这里的”映射”指的是芯片内部的硬件地址译码(Address Decode)机制,与软件设置无关。
芯片设计时,内部总线上有一个地址译码器,负责将 CPU 发出的物理地址路由到对应的存储器或外设:
CPU 发出物理地址 │ ▼ 地址译码器(硬件电路) │ ├─ 0x0000_0000 ~ 0x0001_7FFF ──→ 片内 BootROM(96KB) ├─ 0x0090_0000 ~ 0x0091_7FFF ──→ 片内 OCRAM(96KB) ├─ 0x8000_0000 ~ 0xFFFF_FFFF ──→ DDR(需初始化后才有效) └─ 0x02000000+ ──→ 各外设寄存器上电时 DDR 尚未初始化,不可访问。但 BootROM 是片内硅,不需要外部时序,通电即可读取。所以 CPU 复位后取第一条指令,地址落在 BootROM 的区间,硬件直接把 ROM 内容返回给 CPU,启动就从这里开始。
“映射”本质就是:该地址范围在芯片出厂时就被硬连接到了 ROM 单元上,软件无法改变,上电自动生效。
2. 读取启动模式
通过 BOOT_MODE[1:0] 引脚和 eFuse 配置决定从哪里启动:
| BOOT_MODE[1:0] | 模式 |
|---|---|
| 00 | Boot from Fuses(根据 eFuse 配置) |
| 01 | Serial Download(USB OTG,用于烧录) |
| 10 | Internal Boot(从 eFuse 指定的设备启动) |
3. 初始化最小时钟,读取镜像头
BootROM 只初始化自身运行所需的最基本时钟(CPU 时钟 + 启动设备控制器时钟),然后从启动设备读取 U-Boot 镜像的前几 KB(IVT + Boot Data + DCD)。
4. 执行 DCD,完整初始化 DDR
BootROM 将 U-Boot 的镜像头解析如下:
- IVT(Image Vector Table):告诉 BootROM 镜像的入口地址、DCD 地址
- DCD(Device Configuration Data):DDR 初始化寄存器序列
❓ 疑问二:”最小化初始化 DDR”是什么意思?DDR 以很低的频率工作?
不是的,这里需要纠正一个误解。BootROM 本身不会以”低频”初始化 DDR,DDR 始终在全速(400 MHz)下运行。
准确的流程是:
- BootROM 先做自身运行所需的最小时钟初始化(不包含 DDR),使用片内 OCRAM 作为工作内存
- 从启动设备读出 DCD 数据
- 逐条执行 DCD 中的寄存器写序列,完整配置 DDR 控制器(MMDC):包括 PLL 倍频、时序参数(tRCD/tRP/tCL)、阻抗校准等
- DDR 配置完成后,BootROM 将完整 U-Boot 加载到 DDR
BootROM 自己只懂"按序写寄存器",不懂 DDR ↓ DCD 才是真正的 DDR 初始化配置 ↓ DCD 由 BSP 工程师在 imximage.cfg 中编写 对应芯片实际的 DDR 型号和 PCB 走线iMX6ULL 配合 DDR3L 典型运行频率:400 MHz(DDR3L-800,数据率 800 MT/s)。
“最小化”描述的是 BootROM 自身做的初始化工作很少(它把复杂的 DDR 配置外包给了 DCD),而不是说 DDR 跑在低频。
5. 加载完整 U-Boot,跳转入口
DDR 就绪后,BootROM 将完整 U-Boot 搬到 0x8780_0000,然后跳转到 IVT 中指定的入口地址执行。
面试要点
Q:BootROM 阶段做了什么?
A:执行芯片固化代码,检测启动模式,读取 U-Boot 镜像头中的 DCD 数据配置 DDR,DDR 就绪后加载完整 U-Boot 到 DDR,跳转执行。整个过程用户不可干预,DDR 初始化的具体参数由镜像头中的 DCD 决定。
第二阶段:U-Boot
详细代码分析见 U-Boot启动流程分析,本节重点解释各步骤的原理。
核心流程(8步)
_start(汇编)
│
├─① CPU 切换到 SVC32 模式,关中断
├─② cpu_init_crit:关 MMU/DCache,无效 TLB/ICache
│
└─ _main(crt0.S)
├─③ 设置初始栈指针(OCRAM 中)
├─④ board_init_f_alloc_reserve:为 gd 结构体分配空间
├─⑤ board_init_f:串口、时钟、DDR 完整初始化
├─⑥ relocate_code:将 U-Boot 自身搬到 DDR 高地址
├─⑦ board_init_r:外设初始化(网卡/Flash/环境变量)
└─⑧ main_loop:倒计时 → 执行 bootcmd → bootz 启动内核
① CPU 模式切换(SVC32)
❓ 疑问三:汇编阶段 CPU 模式切换是什么意思?为什么要切换?
ARM 处理器有多种运行模式,每种模式有独立的栈指针(SP)和部分独立寄存器:
| 模式 | 缩写 | 用途 |
|---|---|---|
| User | USR | 普通应用程序,权限最低 |
| Supervisor | SVC | OS/Bootloader 默认模式,全权限 |
| IRQ | IRQ | 处理普通中断 |
| FIQ | FIQ | 处理快速中断 |
| Abort | ABT | 内存访问错误处理 |
| Undefined | UND | 未定义指令处理 |
| Hyp | HYP | 虚拟化扩展(ARMv7) |
| Monitor | MON | TrustZone 安全监控 |
U-Boot 需要全权限访问所有硬件,必须在 SVC(Supervisor)模式下运行。
问题在于:芯片在复位后不一定就处于 SVC 模式。如果芯片支持 TrustZone 且启用了安全启动,CPU 可能从 Monitor 模式启动;某些平台可能在 HYP 模式下进入 U-Boot。
所以 U-Boot _start 的第一件事就是强制切换到 SVC32(32 位 SVC 模式)并关闭所有中断:
/* arch/arm/cpu/armv7/start.S */
ENTRY(_start)
/* 切换到 SVC32 模式,关中断(CPSR.I=1, CPSR.F=1) */
safe_svcmode_maskall r9
safe_svcmode_maskall 会读取当前 CPSR,判断是否需要切换,保证后续代码在 SVC 模式、中断关闭的状态下运行。
② 关闭 MMU 和 Cache
❓ 疑问四:为什么要关闭 MMU 和 Cache?
关闭 MMU 的原因:
MMU 负责把 CPU 发出的虚拟地址翻译成物理地址。翻译依据是页表,页表存在内存里。
U-Boot 刚启动时:
- 页表还不存在(没人建过)
- 即使有残留页表,映射关系也不可信
如果这时开着 MMU,CPU 访问任何地址都会经过页表翻译:
- 翻译到了错误的物理地址 → 访问到错误的寄存器/内存
- 页表本身所在地址也被翻译 → 陷入混乱,直接崩溃
关闭 MMU 后,VA = PA(虚拟地址 = 物理地址),代码直接用物理地址访问,链接地址是多少就访问哪里,简单可靠。
关闭 DCache(数据缓存)的原因:
Cache 是 CPU 和 DDR 之间的高速缓冲。硬件寄存器(MMIO)的访问不能被缓存,否则:
CPU 写 → 写进 Cache(硬件看不到) → 外设寄存器没被配置 → 初始化失败
CPU 读 → 读 Cache 里的旧值(硬件实际值已变) → 读到错误状态
关闭 DCache 后,所有内存访问都直达物理目标,MMIO 寄存器的读写立即生效。
无效 TLB 和 ICache 的原因:
TLB(Translation Lookaside Buffer)是 MMU 的翻译缓存,ICache 是指令缓存。即使关了 MMU,TLB 里可能有上一次开机留下的脏数据;ICache 里可能有不一致的旧指令。全部无效化,确保干净的执行环境。
/* arch/arm/cpu/armv7/start.S: cpu_init_crit */
cpu_init_crit:
/* 无效 L1 ICache */
mov r0, #0
mcr p15, 0, r0, c7, c5, 0 /* ICIALLU:无效所有 ICache */
/* 无效 TLB */
mcr p15, 0, r0, c8, c7, 0 /* TLBIALL:无效所有 TLB */
/* 关闭 MMU 和 DCache(清 SCTLR 的 M 位和 C 位) */
mrc p15, 0, r0, c1, c0, 0
bic r0, r0, #(1 << 0) /* 清 M 位:关 MMU */
bic r0, r0, #(1 << 2) /* 清 C 位:关 DCache */
mcr p15, 0, r0, c1, c0, 0
bx lr
③ 初始化栈
❓ 疑问五:栈的初始化流程是怎样的?
U-Boot 启动过程中,栈会经历两次迁移:
第一阶段:OCRAM 栈(DDR 初始化之前)
上电时 DDR 还没配置,无法使用。iMX6ULL 有 96KB 片内 OCRAM(物理地址 0x0090_0000),上电即可用,BootROM 用过它,U-Boot 早期也用它做栈:
OCRAM 布局(示意)
0x0091_FFFF ┬────────────────────────
│ 初始栈(向下增长)
│ ↓
0x0091_EFFF ┼────────────────────────
│ gd(global_data 结构体)
0x0091_EE00 ┴────────────────────────
/* crt0.S */
_main:
/* ① 设置初始 SP 到 OCRAM 高地址(CONFIG_SYS_INIT_SP_ADDR) */
ldr sp, =(CONFIG_SYS_INIT_SP_ADDR)
bic sp, sp, #7 /* 8 字节对齐(ABI 要求) */
/* ② 在栈顶下方为 gd 分配空间,返回新 SP */
bl board_init_f_alloc_reserve /* r0 = 新 SP */
mov sp, r0
/* ③ 初始化 gd 内存(清零),r9 = gd 指针 */
bl board_init_f_init_reserve
/* ④ 用这个 OCRAM 栈执行 board_init_f */
bl board_init_f
第二阶段:DDR 栈(重定位之后)
board_init_f 计算出 U-Boot 重定位后的目标地址,也在 gd 中记录了新栈的位置(DDR 顶端附近)。重定位完成后切换到 DDR 中的新栈:
/* board_init_f 计算的内存布局(DDR 高端) */
DDR 顶端
├─ 栈区(向下增长,通常 16KB)
├─ gd 结构体
├─ U-Boot 代码段(重定位目标)
└─ ...
/* relocate 之后,切换到 DDR 中的新栈 */
ldr sp, [r9, #GD_START_ADDR_SP] /* 从 gd->start_addr_sp 读新栈地址 */
⑥ 代码重定位
❓ 疑问六:代码为什么要重定位?如何重定位?
为什么重定位?
BootROM 把 U-Boot 加载到固定的低地址(iMX6ULL 约 0x8780_0000)。这个地址处于 DDR 低区,正好是后续加载 Linux 内核的目标区间。如果 U-Boot 留在这里,加载内核时会把自己覆盖掉,系统崩溃。
解决方案:把 U-Boot 自身搬到 DDR 最顶端,把低地址空间全部让给内核和 DTB 使用。
重定位前 重定位后
┌─────────────────┐ ┌─────────────────┐ ← DDR 顶端
│ │ │ U-Boot(新位置)│
│ 可用空间 │ ├─────────────────┤
│ │ │ 栈、gd │
├─────────────────┤ ├─────────────────┤
│ U-Boot(旧) │ ──搬──→ │ │
│ 0x8780_0000 │ │ 全部释放给内核 │
├─────────────────┤ ├─────────────────┤
│ DDR 起始 │ │ DDR 起始 │
└─────────────────┘ └─────────────────┘
如何重定位?
仅仅”搬代码”不够,因为 C 程序里有函数指针、全局变量地址等绝对地址引用,搬到新位置后这些地址还指向旧地址,调用会出错。
编译器在链接时生成了 .rel.dyn 重定位表,记录了所有需要修正的地址位置。重定位流程:
① board_init_f 计算:
reloc_off = 新地址 - 旧地址
gd->relocaddr = DDR_TOP - UBOOT_SIZE(对齐后)
② relocate_code(arch/arm/lib/relocate.S):
memcpy(新地址, 旧地址, U-Boot 大小) // 拷贝代码
③ 遍历 .rel.dyn 重定位表:
for each entry:
*(entry->offset) += reloc_off // 修正绝对地址
④ relocate_vectors:
修正异常向量表地址
⑤ 跳转到新地址继续执行
重定位之后,U-Boot 在 DDR 顶端运行,旧地址处的数据不再使用。
加载 zImage 和 DTB 到 DDR
❓ 疑问七:如何把 zImage 和 DTB 加载到指定地址?
bootcmd 中的加载命令本质是 U-Boot 驱动调用,以下是两种主要方式:
方式一:TFTP 网络加载
tftp 0x80800000 zImage
执行流程:
- 初始化 FEC(Fast Ethernet Controller)网卡驱动
- 通过 DHCP 获取 IP(或使用
ipaddr、serverip环境变量) - 向 TFTP 服务器发送 GET 请求
- 接收 UDP 数据包,将每个数据块直接写入 DDR 目标地址
- 传输完成后报告文件大小
方式二:MMC/eMMC 加载
load mmc 0:2 0x80800000 /boot/zImage
执行流程:
- 初始化 USDHC 控制器
- 读取 FAT/ext4 文件系统目录项,找到文件
- 按文件大小读取数据,写入 DDR 目标地址
方式三:NAND Flash 裸读
nand read 0x80800000 0x400000 0x800000 # 从偏移 4MB 读 8MB 到 DDR
地址为什么选 0x80800000?
这是 BSP 工程师人为规划的地址,需满足:
- 不与 U-Boot 重定位后的区域重叠
- 不与 DTB 加载地址(通常
0x8300_0000)重叠 - 给内核解压留足空间(压缩内核约 7MB,解压后约 20MB)
DDR 地址空间规划示意(iMX6ULL,256MB DDR)
0x8000_0000 ├─ DDR 起始
0x8080_0000 ├─ zImage 加载地址(约 7MB)
0x8300_0000 ├─ DTB 加载地址(约 50KB)
│ ...空闲区...
0x8F80_0000 ├─ U-Boot 重定位区
0x8FFF_FFFF └─ DDR 顶端(256MB)
bootcmd 到内核入口
bootcmd = "tftp 0x80800000 zImage; tftp 0x83000000 imx6ull.dtb; bootz 0x80800000 - 0x83000000"
│ │ │
└── 内核镜像加载地址 └── DTB 加载地址 └── 解压并启动
bootz 最终调用 do_bootm_linux,通过 boot_selected_os 跳转到内核入口,同时将 DTB 地址传给内核(通过 r2 寄存器)。
关键数据结构:gd(global data)
struct global_data 是贯穿整个 U-Boot 的全局结构体,存放 DDR 大小、波特率、重定位偏移等信息。地址固定存放在 r9 寄存器中。
/* arch/arm/include/asm/global_data.h */
#define DECLARE_GLOBAL_DATA_PTR register volatile gd_t *gd asm ("r9")
面试要点
Q:U-Boot 为什么要做代码重定位?
A:U-Boot 被 BootROM 加载到 DDR 低地址,与后续内核加载地址冲突。重定位将 U-Boot 自身复制到 DDR 最顶端,释放低地址空间给内核使用,同时遍历 .rel.dyn 表修正所有绝对地址引用(加上 reloc_off 偏移)。
第三阶段:Linux Kernel 启动
内核入口(head.S)
U-Boot 跳转到 zImage 解压后的内核入口 arch/arm/kernel/head.S:
ENTRY(stext)
/* 1. 确认 CPU 模式为 SVC,关中断 */
safe_svcmode_maskall r9
/* 2. 读取 CPU ID,查找支持的处理器类型 */
mrc p15, 0, r9, c0, c0
bl __lookup_processor_type
/* 3. 建立临时页表(Section 映射) */
bl __create_page_tables
/* 4. 开启 MMU */
b __enable_mmu
/* 5. 跳转到 start_kernel */
b start_kernel
ENDPROC(stext)
❓ 疑问八:为什么要先关 MMU,建页表,再开 MMU?
这是一个”鸡生蛋”问题:开 MMU 需要页表,建页表需要内存访问,内存访问需要地址正确。以下逐步解释:
第一步:为什么先关 MMU?
U-Boot 跳转内核时,MMU 是关闭的(U-Boot 自己关的,或者 bootz 调用前会关)。内核从物理地址开始执行,此时:
- 没有任何页表存在(刚进内核,什么都没建)
- 即使有残留的 U-Boot 页表,那是 U-Boot 的地址空间,与内核虚拟地址完全不同
如果这时贸然开启 MMU,CPU 会用不存在的/错误的页表翻译地址 → 立刻产生 Translation Fault,系统崩溃。
所以必须在 MMU 关闭、VA = PA 的前提下建好页表。
第二步:为什么要建页表?
Linux 内核全部用虚拟地址编译。内核代码里所有函数调用、全局变量都在虚拟地址空间(ARM 32 位典型布局:内核虚拟地址从
0xC000_0000开始)。开启 MMU 之后,CPU 发出的地址要经过页表翻译才能访问到正确的物理内存。如果没有页表,或者页表是错的,内核第一次访问内存就会 Fault。
__create_page_tables建立的是临时初始页表,使用 1MB 大页(Section 映射),只建立以下最小集合:物理地址 → 物理地址(恒等映射,供 MMU 开启瞬间继续执行用) 物理地址 → 内核虚拟地址(0xC0000000+,供开启后正常运行用)第三步:为什么建完再开 MMU?
页表就绪后,才能安全地设置 TTB(Translation Table Base)寄存器并开启 MMU:
__enable_mmu: /* 把页表物理地址写入 TTB0 寄存器 */ mcr p15, 0, r4, c2, c0, 0 /* TTBR0 = 页表基址 */ /* 设置域访问控制 */ mov r5, #DOMAIN_CLIENT mcr p15, 0, r5, c3, c0, 0 /* DACR */ /* 开启 MMU(SCTLR.M = 1) */ mrc p15, 0, r0, c1, c0, 0 orr r0, r0, #CR_M mcr p15, 0, r0, c1, c0, 0 /* ← 这条指令执行后 MMU 开启 */ /* 紧接着的指令必须在已映射的虚拟地址处! */ mov pc, r13 /* 跳到内核虚拟地址处的 __mmap_switched */MMU 开启的瞬间,CPU 开始用虚拟地址运行。得益于恒等映射的存在,MMU 开启后的紧接几条指令仍能正确取指,顺利跳转到内核虚拟地址。此后整个内核在虚拟地址空间中运行。
总结:关 MMU → 建页表 → 开 MMU,是从物理地址空间过渡到虚拟地址空间的唯一安全路径。
start_kernel():内核 C 语言入口
init/main.c 中的 start_kernel() 是内核的 C 语言入口,依次初始化各子系统:
asmlinkage __visible void __init start_kernel(void)
{
/* 基础初始化 */
setup_arch(&command_line); // 解析 DTB,初始化内存布局
mm_init(); // 内存管理子系统
sched_init(); // 调度器
irq_init(); // 中断控制器
/* 时钟 */
time_init();
tick_init();
/* 控制台 */
console_init(); // 这之后才能看到内核打印
/* 其余子系统 */
vfs_caches_init(); // VFS 文件系统缓存
signals_init();
/* 完成初始化,进入多进程阶段 */
rest_init();
}
rest_init():创建第一个用户进程
static noinline void __ref rest_init(void)
{
/* 创建 PID=1 的 kernel_init 线程 */
kernel_thread(kernel_init, NULL, CLONE_FS);
/* 创建 kthreadd 线程(负责创建其他内核线程) */
kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
/* 0 号进程变为 idle 进程,在没有任务时执行 */
cpu_startup_entry(CPUHP_ONLINE);
}
进程 ID 分配:
| PID | 名称 | 来源 |
|—–|——|——|
| 0 | idle/swapper | start_kernel 本身演变而来 |
| 1 | init(kernel_init) | rest_init 创建 |
| 2 | kthreadd | rest_init 创建 |
kernel_init:挂载 rootfs,启动用户空间
static int __ref kernel_init(void *unused)
{
/* 等待所有 CPU 初始化完成 */
kernel_init_freeable();
/* 释放 init 段内存(__init 标记的代码/数据) */
free_initmem();
/* 挂载根文件系统,执行 init 进程 */
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
}
/* 按顺序尝试这几个路径 */
if (execute_command)
run_init_process(execute_command); // 内核参数 init= 指定的
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No working init found.");
}
run_init_process 通过 execve 调用,将内核线程替换为用户空间进程,从此内核正式进入用户空间。
DTB 如何传给内核
U-Boot 跳转内核时,通过寄存器传递参数:
r0= 0r1= machine type(或~0表示使用 DTB)r2= DTB 在 DDR 中的物理地址
内核在 setup_arch() 中通过 r2 读取 DTB,解析内存大小、外设信息等。
面试要点
Q:内核的 0 号进程、1 号进程、2 号进程分别是什么?
A:0 号进程是 idle 进程(swapper),由 start_kernel 本身演变而来,CPU 空闲时执行。1 号进程是 init,由 rest_init 创建,负责挂载 rootfs 并启动用户空间的 /sbin/init。2 号进程是 kthreadd,负责创建所有内核线程。
Q:
__init宏有什么用?A:标记只在启动阶段执行一次的函数和数据。内核启动完成后,
free_initmem()会释放这部分内存,通常能释放几百 KB。
第四阶段:init 进程(用户空间)
BusyBox init(嵌入式常用)
嵌入式系统通常使用 BusyBox 的 init,读取 /etc/inittab:
# /etc/inittab
::sysinit:/etc/init.d/rcS # 系统初始化脚本
::respawn:-/bin/sh # 启动 Shell,退出后重启
::ctrlaltdel:/sbin/reboot # Ctrl+Alt+Del 处理
/etc/init.d/rcS 通常完成:
mount -a # 挂载 /proc /sys /dev 等
/sbin/mdev -s # 动态创建设备节点
ifconfig eth0 192.168.1.200 # 网络配置(如有)
systemd(桌面/服务器 Linux)
较新的发行版使用 systemd 替代 BusyBox init,通过 Unit 文件管理服务依赖和启动顺序,启动速度更快(并行启动)。
完整时间线(面试速记版)
上电
│
├─ [0ms] BootROM:检测启动模式,执行 DCD 配置 DDR(400MHz),加载 U-Boot
├─ [~200ms] U-Boot 汇编:SVC 模式,关 MMU/Cache,OCRAM 建栈
├─ [~300ms] board_init_f:串口输出 "U-Boot ...",DDR/时钟完整初始化
├─ [~400ms] 代码重定位到 DDR 顶端,board_init_r
├─ [~500ms] main_loop:显示倒计时
├─ [~3500ms] bootcmd 执行:TFTP/MMC 加载内核和 DTB 到 DDR
├─ [~3600ms] bootz:解压内核,关 MMU,跳转内核物理入口
├─ [~3700ms] head.S:建页表,开 MMU,进入内核虚拟地址空间
├─ [~3800ms] start_kernel:串口输出 "Booting Linux ..."
├─ [~5000ms] 各子系统初始化完成,挂载 rootfs
├─ [~5500ms] exec /sbin/init
└─ [~6000ms] Shell 登录提示符
高频面试题汇总
| 问题 | 关键答案 |
|---|---|
| BootROM 做了什么? | 检测启动模式,执行 DCD 配置 DDR,加载 U-Boot,跳转 |
| DCD 是什么? | Device Configuration Data,DDR 初始化寄存器序列,嵌在 U-Boot 镜像头中,由 BootROM 执行 |
| BootROM 阶段 DDR 跑多少频率? | DCD 按 BSP 配置初始化,iMX6ULL 典型 400MHz DDR3L,非”低频” |
| U-Boot 为什么切换到 SVC 模式? | SVC 是全权限模式,有独立栈指针,ARM 传统 OS/Bootloader 模式 |
| U-Boot 为什么关 MMU? | 早期无页表,开 MMU 会立即 Fault;关闭后 VA=PA,物理地址直接访问 |
| U-Boot 为什么关 DCache? | MMIO 寄存器不能被缓存,否则写不到硬件、读到旧值 |
| U-Boot 早期栈在哪里? | 片内 OCRAM(DDR 未初始化时),重定位后切换到 DDR 顶端 |
| U-Boot 为什么重定位? | 让出低地址空间给内核,避免覆盖;修正 .rel.dyn 中的绝对地址引用 |
| gd 结构体在哪? | 固定在 r9 寄存器,存放全局运行时数据 |
| U-Boot 怎么把参数传给内核? | r2 寄存器传 DTB 物理地址;bootargs 环境变量写入 DTB /chosen 节点 |
| 内核为什么关 MMU 后再建页表再开 MMU? | 没有页表不能开 MMU;开 MMU 后必须有映射才能继续执行;三步是从物理→虚拟地址的唯一安全路径 |
| start_kernel 做了什么? | 初始化所有子系统:内存/调度/中断/时钟/VFS/控制台,最后调 rest_init |
| 1 号进程是怎么来的? | rest_init() 中 kernel_thread(kernel_init) 创建,execve /sbin/init 后变为用户进程 |
__init 有什么用? | 标记一次性初始化代码,启动完成后 free_initmem() 释放,节省内存 |
| 内核如何找到 rootfs? | 通过 cmdline 中的 root= 参数,或 initramfs 内置 |
关联笔记
- U-Boot启动流程分析 — U-Boot 汇编到 board_init_r 完整代码分析
- U-Boot环境变量配置 — bootcmd / bootargs 配置参考
- Linux内核移植(iMX6ULL) — 内核配置与编译
- iMX6ULL启动与烧录方式汇总 — 各启动模式配置
延伸阅读
理解启动全流程后,下一步是内核移植和 Linux 设备驱动模型。




