1. 从一条命令到内核跑起来中间到底发生了什么很多人第一次接触嵌入式Linux启动流程时都会有一个疑问我在U-Boot命令行里敲下bootm 0x80008000然后内核就起来了这中间到底发生了什么看起来只是一条命令实际上背后是一条完整的启动链路——从镜像格式识别、头部校验、内存搬运、设备树传递到最终跳转到内核入口。这条链路上任何一环出问题你看到的就是黑屏、卡死或者一串看不懂的报错。bootm、booti、bootelf这三个命令是U-Boot里最常用的启动命令分别对应不同的镜像格式。搞嵌入式的朋友基本天天跟它们打交道但真正把它们的启动链路吃透的人并不多。大部分人的做法是板子能启动就行命令能跑通就不深究。但一旦遇到启动失败、镜像加载地址冲突、设备树传参不对、内核解压后跑飞这些问题就只能靠反复试参数来碰运气。这篇文章面向的是有一定嵌入式基础的开发者不管你是刚接触U-Boot的新手还是已经用过一段时间但没系统梳理过启动链路的老手都能从中找到有用的东西。我会把bootm、booti、bootelf三条命令的完整启动链路拆开讲清楚包括它们各自适用什么场景、内部做了哪些事、关键参数怎么传、踩过哪些坑。内容基于U-Boot通用实现来写不同SoC厂商的U-Boot分支可能有细微差异但核心逻辑是一致的。2. 三条启动命令的适用边界别拿bootm去启动ARM64内核2.1 镜像格式决定了你用哪条命令U-Boot的启动命令不是随便选的它跟镜像格式强绑定。选错了命令轻则报Bad Magic Number重则跳转后直接跑飞。bootm是历史最悠久的启动命令它处理的是U-Boot自己的镜像格式uImage。uImage是在原始内核镜像前面加了一个64字节的头部里面包含魔数、加载地址、入口地址、CRC校验等信息。这个格式在ARM32时代非常流行因为U-Boot可以自己解析头部知道该把镜像搬到哪里、从哪里开始执行。booti是后来为ARM64AArch64引入的命令它处理的是raw Image格式也就是内核编译出来的Image文件没有U-Boot头部。为什么ARM64不继续用uImage因为ARM64的启动协议变了内核要求设备树地址通过寄存器传递而且镜像本身不需要U-Boot再做搬运和校验直接用booti指定内核地址、ramdisk地址、设备树地址就行。bootelf处理的是ELF格式的可执行文件。这个命令用得相对少但在一些裸机程序加载、RTOS启动、或者某些特殊固件的场景下会用到。ELF格式的好处是段信息完整U-Boot可以按照ELF头里的program header把各个段加载到正确的地址。下面这张表可以帮你快速判断该用哪条命令命令适用镜像格式典型架构是否需要U-Boot头部设备树传递方式bootmuImage (legacy)ARM32, MIPS, PowerPC是通过bootargs或fdt_addrbootiraw ImageARM64否通过第三个参数显式指定bootelfELF通用否取决于程序本身2.2 一个常见的误判ARM64上用bootm会怎样我见过不少人在ARM64平台上习惯性地敲bootm结果报错。原因很简单ARM64的内核Image没有uImage头部bootm去读头部魔数的时候读到的是一堆无效数据直接判定格式错误。更隐蔽的情况是有人用mkimage工具把ARM64的Image重新打包成uImage然后用bootm启动。这样做理论上可行但实际很容易出问题——因为ARM64内核对启动参数的要求跟ARM32不同uImage头部里的入口地址和设备树传递方式可能跟内核预期不匹配。所以ARM64平台就老老实实用booti别绕弯子。2.3 bootelf的使用场景比你想的要窄bootelf在标准Linux启动流程里基本用不到它的主战场是加载裸机测试程序比如内存测试、外设初始化验证启动某些RTOS或实时固件在U-Boot阶段加载一个ELF格式的二级引导程序用bootelf的时候要注意ELF文件的加载地址是编译时确定的U-Boot会按照program header里的p_paddr来搬运各个段。如果编译时指定的地址跟实际内存布局冲突加载就会失败。所以用之前一定要用readelf -l看一下段的物理地址。3. bootm的完整启动链路从头部校验到跳转内核3.1 第一步镜像头部解析与CRC校验当你敲下bootm 0x80008000U-Boot做的第一件事是读取这个地址开始的64字节头部。头部结构大致是这样的typedef struct image_header { uint32_t ih_magic; // 魔数 0x27051956 uint32_t ih_hcrc; // 头部CRC uint32_t ih_time; // 时间戳 uint32_t ih_size; // 数据大小 uint32_t ih_load; // 加载地址 uint32_t ih_ep; // 入口地址 uint32_t ih_dcrc; // 数据CRC uint8_t ih_os; // 操作系统类型 uint8_t ih_arch; // 架构类型 uint8_t ih_type; // 镜像类型 uint8_t ih_comp; // 压缩类型 uint8_t ih_name[32]; // 镜像名称 } image_header_t;U-Boot首先检查ih_magic是不是0x27051956不是就直接报Bad Magic Number。然后校验头部CRC再校验数据CRC。这两步校验很关键——如果镜像在传输或烧录过程中损坏这里就会拦住不会让一个坏镜像跑到跳转阶段才崩溃。提示如果你确认镜像没问题但一直报CRC错误先检查加载地址对不对。有时候你把镜像下载到了错误的地址读到的头部自然是一堆乱码。3.2 第二步镜像搬运与解压校验通过后U-Boot看ih_load和当前镜像所在地址是否一致。如果不一致就把镜像数据搬到ih_load指定的地址。这一步很多人会忽略但它恰恰是很多启动失败的根源。举个例子你用TFTP把内核下载到0x81000000但uImage头部里记录的ih_load是0x80008000。U-Boot会自动把数据从0x81000000搬到0x80008000。如果0x80008000这块区域被其他东西占用了比如正在运行的U-Boot自身代码搬运就会覆盖掉关键数据导致各种奇怪的问题。搬运完成后如果ih_comp显示镜像是压缩的比如gzip、lzmaU-Boot会调用对应的解压函数把镜像解压到ih_load地址。解压后的数据大小可以通过ih_size推算但实际解压后的大小要看压缩格式。3.3 第三步设备树处理与bootargs传递对于ARM32平台bootm处理设备树有几种方式第一种是bootm kernel_addr - fdt_addr用短横线占位ramdisk第三个参数指定设备树地址。U-Boot会把设备树地址写到合适的位置并在跳转前通过寄存器传递给内核。第二种是通过环境变量fdt_addr或fdtcontroladdr来指定。如果命令行没给设备树地址U-Boot会尝试用环境变量里的值。第三种是设备树已经跟内核一起打包在uImage里multi-image格式U-Boot会自动拆分。bootargs环境变量是内核命令行参数U-Boot在跳转前会把它放到设备树的/chosen/bootargs节点里。这里有个细节如果你在命令行里用bootm ... bootargsxxx这种方式传参它会覆盖环境变量里的bootargs。但更推荐的做法是提前用setenv bootargs设置好避免命令行太长出错。3.4 第四步跳转到内核入口所有准备工作完成后U-Boot做最后几件事关闭中断和缓存或者按内核要求保留某些缓存把机器IDARM32或设备树地址ARM64放到指定寄存器跳转到ih_ep指定的入口地址对于ARM32寄存器约定是r00r1机器IDr2设备树地址。对于ARM64x0设备树地址。这些约定是内核启动协议规定的U-Boot必须严格遵守否则内核起来后找不到设备树直接卡死。注意跳转前U-Boot会执行cleanup_before_linux之类的清理函数不同架构实现不同。如果你在移植U-Boot时改动了这部分一定要确认没有破坏内核要求的寄存器状态。4. booti的启动链路ARM64下的精简与规范4.1 booti的参数格式与地址规划booti的命令格式是booti kernel_addr [ramdisk_addr [fdt_addr]]三个参数分别是内核Image地址、ramdisk地址用-表示没有、设备树地址。跟bootm不同booti不需要解析头部它直接认为你给的地址就是raw Image的起始位置。地址规划是ARM64启动的关键。典型的内存布局是这样的内核Image加载地址0x80080000或类似取决于具体平台设备树加载地址通常在内核之后比如0x83000000ramdisk加载地址再往后比如0x84000000这些地址不能重叠也不能跟U-Boot自身占用的内存冲突。我一般会在U-Boot里用bdinfo看一下内存布局确认可用区域后再定地址。4.2 设备树在booti里的传递细节ARM64内核对设备树的要求比ARM32严格。booti在跳转前会做这几件事首先检查设备树头部魔数是不是0xd00dfeed。不是的话直接报错。然后U-Boot会把bootargs写入设备树的/chosen节点。如果设备树里没有/chosen节点U-Boot会创建一个。接着U-Boot会处理设备树里的memory节点确保内存信息跟实际硬件匹配。有些平台还需要U-Boot修正设备树里的CPU频率、时钟等参数。最后跳转时把设备树地址放到x0寄存器。ARM64内核启动协议规定x0必须指向设备树x1到x3保留为0。如果x0传错了内核在early_init阶段就会崩。4.3 booti启动失败的几个典型原因实际调试中booti启动失败最常见的原因有这么几个内核地址不对。有些人把Image下载到了0x80008000但实际应该用0x80080000。ARM64内核的加载地址通常有对齐要求不对齐的话解压或跳转会出问题。设备树地址跟内核重叠。如果设备树加载地址离内核太近内核启动过程中可能会覆盖设备树数据。一般建议设备树跟内核之间留至少16MB间隔。bootargs里的console参数不对。这个不会导致启动失败但会导致你看不到任何输出误以为启动卡死了。确认consolettyS0,115200之类的参数跟实际串口匹配。内存节点信息不对。设备树里的memory节点如果写的地址范围跟实际内存不符内核起来后会访问非法地址。这个在移植新板子时特别容易遇到。5. bootelf的加载逻辑按段搬运与入口跳转5.1 ELF头部解析与program header遍历bootelf的工作方式跟bootm、booti完全不同。它不关心什么uImage头部或raw Image而是按照标准ELF格式来解析。U-Boot首先读取ELF头部确认魔数0x7f454c46即\x7fELF。然后根据e_phoff找到program header table遍历每一个program header。对于类型为PT_LOAD的段U-Boot会按照p_paddr指定的物理地址把p_filesz大小的数据从文件偏移p_offset处搬过去。如果p_memsz大于p_filesz剩余部分清零BSS段。这个过程跟操作系统加载ELF程序很像但U-Boot是在裸机环境下做的没有虚拟内存映射所以直接按物理地址搬运。5.2 bootelf的入口地址与参数传递所有段加载完成后U-Boot跳转到ELF头部里的e_entry指定的入口地址。跟Linux内核不同ELF程序的入口不需要遵循什么启动协议寄存器状态取决于程序本身的约定。如果你用bootelf加载的是Linux内核虽然不常见那就要自己确保寄存器状态符合内核要求。但更常见的用法是加载裸机程序或RTOS这时候入口地址和寄存器约定都是你自己定的灵活度很高。提示用bootelf之前务必用readelf -h和readelf -l确认入口地址和段地址。我遇到过有人编译时没指定链接脚本段地址默认从0开始结果U-Boot往地址0搬运数据直接把异常向量表覆盖了系统当场挂掉。5.3 bootelf与bootm/booti的本质区别从链路角度看bootelf比bootm和booti都要底层。bootm和booti是为Linux内核量身定做的内部做了很多针对内核的优化和约定处理。而bootelf是一个通用的ELF加载器它不关心你加载的是什么只负责把段搬到正确位置然后跳转。这也意味着bootelf的容错性更低。bootm会帮你校验CRC、处理压缩、传递设备树bootelf这些都不管。所以除非你确实需要加载ELF格式的程序否则启动Linux还是用bootm或booti更省心。6. 启动链路中的踩坑实录与排查思路6.1 镜像加载地址冲突一个反复出现的坑这个坑我踩过不止一次。现象是U-Boot里bootm命令执行后没有任何输出串口直接卡死也不报错。排查过程是这样的首先确认镜像下载地址和uImage头部里的ih_load是否一致。如果不一致U-Boot会做搬运。搬运的目标地址如果落在U-Boot自身代码或数据区域就会把正在运行的U-Boot破坏掉导致跳转后行为异常。怎么确认在U-Boot里用bdinfo查看内存布局找到U-Boot的代码段和数据段范围。然后确认ih_load不在这个范围内。如果冲突要么改镜像的加载地址重新打包要么把镜像下载到正确地址避免搬运。6.2 设备树地址传错导致的静默卡死ARM64平台上booti的第三个参数是设备树地址。如果这个地址传错了比如指向了一块未初始化内存内核在early_init阶段解析设备树时会读到无效数据然后卡死。串口可能连一行输出都没有。排查方法在U-Boot里用fdt addr addr和fdt print确认设备树内容是否正确。如果fdt print能正常输出说明设备树本身没问题那就要检查booti命令里的地址参数是否跟fdt addr用的一致。另一个容易忽略的点是设备树地址必须是8字节对齐的。ARM64内核要求设备树地址对齐到8字节不对齐的话可能解析失败。这个在手动指定地址时容易忘。6.3 bootargs参数错误引发的假死有时候内核其实已经启动了但因为bootargs里的console参数不对你看不到任何输出以为卡死了。这种情况最容易被误判。我的做法是先在U-Boot里用printenv bootargs确认参数内容重点检查console后面的设备名和波特率。然后可以用setenv bootargs ${bootargs} earlycon加上earlycon参数让内核在更早的阶段输出调试信息。如果earlycon能输出但正常console不行那就是console驱动或参数的问题。6.4 压缩镜像解压后跑飞用bootm启动压缩内核时U-Boot会先解压再跳转。如果解压后的数据大小超过了预留内存区域就会覆盖后面的数据。这个问题的隐蔽性在于解压过程本身不报错跳转后才崩溃。确认方法在U-Boot里用iminfo addr查看镜像信息它会显示镜像大小、压缩类型、加载地址等。然后根据压缩类型估算解压后的大小gzip一般能压到30%左右lzma压缩率更高但解压后更大。确保加载地址往后预留足够空间。7. 几条实战中总结的配置建议7.1 地址规划要留足余量不管是bootm、booti还是bootelf地址规划都是第一位的。我的习惯是内核加载地址根据SoC手册推荐值通常在内存的低地址区域设备树地址内核地址 内核最大可能大小 16MB余量ramdisk地址设备树地址 1MB余量这样规划虽然浪费一些内存但能避免绝大多数地址冲突问题。嵌入式设备的内存通常够用没必要为了省几MB去冒险。7.2 善用iminfo和fdt命令做预检在正式启动前用iminfo检查uImage头部信息用fdt print检查设备树内容用md查看内存数据。这几步花不了几秒钟但能提前发现大部分问题。特别是iminfo它会输出镜像的加载地址、入口地址、数据大小、校验结果。如果加载地址跟你预期的不一样这里就能发现。7.3 保留一份可用的启动参数调试过程中很容易把环境变量改乱。我的做法是在一切正常的时候用printenv把关键变量bootargs、bootcmd、fdt_addr等记录下来存到本地文件。一旦改乱了直接对照恢复。另外U-Boot支持env export和env import命令可以把环境变量导出到内存或从内存导入。调试时用这个功能做备份很方便。7.4 不同命令的调试输出要会看bootm启动失败时U-Boot通常会打印具体原因比如Bad Magic Number、Bad Header Checksum、Bad Data CRC。这些信息直接指向问题所在别忽略。booti的报错相对少一些因为它不做头部校验。如果booti失败大概率是地址问题或设备树问题需要自己用其他命令排查。bootelf失败时U-Boot会打印段加载信息。如果某个段加载失败会显示具体地址和大小。根据这个信息去检查ELF文件的链接脚本。7.5 跳转前的最后检查清单在敲下启动命令之前我一般会快速过一遍这几个点镜像地址是否正确是否跟U-Boot自身区域冲突设备树地址是否正确是否8字节对齐bootargs里的console参数是否匹配实际串口内存节点信息是否跟实际硬件一致如果是压缩镜像解压后空间是否足够这几项确认完启动成功率会高很多。嵌入式调试没有捷径把链路搞清楚把细节做到位问题自然就少了。
阅读完成 · 觉得有帮助?