芯片上电到 Linux 启动全流程 文章封面

芯片上电到 Linux 启动全流程

面试高频题:请描述嵌入式 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]模式
00Boot from Fuses(根据 eFuse 配置)
01Serial Download(USB OTG,用于烧录)
10Internal 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)下运行。

准确的流程是:

  1. BootROM 先做自身运行所需的最小时钟初始化(不包含 DDR),使用片内 OCRAM 作为工作内存
  2. 从启动设备读出 DCD 数据
  3. 逐条执行 DCD 中的寄存器写序列,完整配置 DDR 控制器(MMDC):包括 PLL 倍频、时序参数(tRCD/tRP/tCL)、阻抗校准等
  4. 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)和部分独立寄存器:

模式缩写用途
UserUSR普通应用程序,权限最低
SupervisorSVCOS/Bootloader 默认模式,全权限
IRQIRQ处理普通中断
FIQFIQ处理快速中断
AbortABT内存访问错误处理
UndefinedUND未定义指令处理
HypHYP虚拟化扩展(ARMv7)
MonitorMONTrustZone 安全监控

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

执行流程:

  1. 初始化 FEC(Fast Ethernet Controller)网卡驱动
  2. 通过 DHCP 获取 IP(或使用 ipaddr、serverip 环境变量)
  3. 向 TFTP 服务器发送 GET 请求
  4. 接收 UDP 数据包,将每个数据块直接写入 DDR 目标地址
  5. 传输完成后报告文件大小

方式二:MMC/eMMC 加载

load mmc 0:2 0x80800000 /boot/zImage

执行流程:

  1. 初始化 USDHC 控制器
  2. 读取 FAT/ext4 文件系统目录项,找到文件
  3. 按文件大小读取数据,写入 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 = 0
  • r1 = 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 设备驱动模型。

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部