ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

U-Boot启动卡在Starting kernel?排查思路与根因解析

U-Boot启动卡在Starting kernel?排查思路与根因解析 1. “Starting kernel”到底意味着什么这条提示之后发生了什么很多刚接触内核移植的朋友看到串口停在Starting kernel ...就会下意识认为“内核崩了”然后开始怀疑内核配置、交叉工具链甚至怀疑自己改的驱动代码。实际上这个结论下得有点早。这条信息根本不是你内核打印出来的它是 U-Boot 在完成引导准备后的最后一句提示。换句话说你看到这句话时整个系统还处于 Bootloader 的控制之下它还没把执行权真正交到内核手里。U-Boot 在执行bootm、booti或者bootz命令时会做一系列跳转前的准备校验镜像、准备设备树、重新定位内存中的镜像、处理启动参数然后关闭中断、清空 Cache、停掉 MMU最后才把 PC 指针指到内核入口地址。Starting kernel ...这句话就打印在跳转动作之前所以它本身不是什么错误日志而是一个“即将交接”的信号。这也是为什么排查这个问题时我总会先纠正团队里新人的一个误区不要盯着内核源码找原因先确认 U-Boot 是在跳转过程中挂掉的还是跳转之后内核第一时间就静默崩溃了。这两种情况用的排查手段完全不同前者要看 U-Boot 代码路径后者要看内核起始阶段的早期打印。那怎么区分方法其实不复杂。看Starting kernel ...之后是否还有任何额外输出哪怕是一个字符。如果后面干干净净说明内核还在做最早期初始化此时 CPU 可能正处在关 MMU、关 Cache 的状态连第一行早期打印都没来得及输出。如果后面有输出但卡在中间某一处说明内核确实跑起来了只是在某个外设初始化上卡住这属于另一类问题。另外还有一种情况U-Boot 打印完这行后马上重启或死循环这种通常不是内核的问题而是 U-Boot 跳转执行时访问了非法地址或者引导参数里的地址与实际加载地址不一致。总之第一件事是把问题框定在“U-Boot 侧”还是“内核侧”框错了方向后面全是弯路。2. 先别急着刷内核准备一套可观察的调试环境2.1 打开早期打印通道内核移植阶段系统还谈不上稳定的串口驱动这时候唯一可靠的观察窗口就是早期串口输出。ARM64 平台在.config里要确认打开CONFIG_SERIAL_EARLYCON同时在 U-Boot 的bootargs里加上earlycon参数。举个例子如果你的调试串口用的是8250兼容串口地址在 0xfe215000那么启动参数里应该写earlyconuart8250,mmio32,0xfe215000这个参数的作用是在内核正式初始化串口驱动之前就用一个极简的早期控制台打印信息。没有它内核在前几百行代码里出的任何问题你都看不到。ARM32 平台则要确认CONFIG_DEBUG_LL和CONFIG_EARLY_PRINTK是否打开。偶尔会遇到打开CONFIG_DEBUG_LL之后编译报错的情况通常是因为调试串口的物理地址没写对需要在arch/arm/Kconfig.debug里选对对应的 SoC 串口模型。我做调试时还会顺手改一下内核源码里printk的优先级把console_loglevel调到最高console_loglevel15或者在内核 cmdline 里加loglevel8保证pr_debug级别的消息也能实时看到。这一步在排查启动早期卡死时几乎是必备的因为默认日志等级会过滤掉很多关键信息。2.2 U-Boot 侧的观察手段串口日志当然也离不了 U-Boot 侧的辅助输出。U-Boot 里有一套完整的调试开关CONFIG_DEBUG_UART打开后 U-Boot 使用极简串口驱动输出调试信息不依赖完整串口驱动初始化CONFIG_DEBUG_UART_BOARD_INIT板级初始化调试串口CONFIG_DEBUG_UART_SHIFT串口寄存器地址对齐方式8250类串口通常为 2同时建议打开 U-Boot 的CONFIG_CMD_BDI这样在 U-Boot 命令行下可以用bdinfo命令查看内存布局、启动参数等关键信息。下面这个命令组合是我每次排查启动问题时必用的setenv bootargs consolettyS0,115200n8 earlyconuart8250,mmio32,0xfe215000 root/dev/mmcblk0p2 rw booti 0x0027f800 - 0x08300000注意这时候要手动指定内核镜像地址和设备树地址不要直接run bootcmd。手动指定能让你明确知道每次启动时三要素是什么内核镜像地址、ramdisk 地址、dtb 地址。三要素里任何一个填错表现都可能恰恰是卡在Starting kernel ...。2.3 备好反汇编和符号表工具当你用上面手段仍看不到内核输出时就需要确认 CPU 是否真的进入了内核入口点。做法是反汇编内核镜像找到入口地址然后对比 PC 指针。这个过程离不开两样东西交叉编译工具链里的 objdump和带符号表的 vmlinux不是压缩过的 uImage/Image。比如 ARM64 的典型入口aarch64-linux-gnu-objdump -D vmlinux | head -50你会看到入口处通常先是几条异常向量表和初始化代码。确保你加载到内存的地址和链接地址一致否则 CPU 跳过去取到的就是乱码表现同样是“死在你眼前却没有任何输出”。这个检查在早期是最容易被忽略的。3. 从根因到现象把“卡住”拆成四种常见模式这几年的工程实践下来我发现卡在Starting kernel的原因翻来覆去就那么几类绝大多数逃不出以下四个维度。3.1 设备树问题最多发的一类根因设备树DTB在启动链路中的作用是“告诉内核板子上有什么、地址在哪、时钟怎么配”。如果 DTB 内容不匹配实际硬件内核早期初始化阶段就会出问题具体表现可轻可重轻则某个外设初始化失败但控制台还能输出日志重则内存节点配置错误导致内核在 mmu_init 阶段直接挂掉连一个字符都来不及打印最隐蔽的是 DTB 中chosen节点指定了错误的stdout-path内核启动参数里没给console早期日志去向不明所以在排查时我强烈建议先用工具确认 DTB 内容和预期一致。用fdtdump查看fdtdump my_board.dtb重点看这几个节点/memory、/chosen、/soc下的 uart 节点、/cpus的cpu0节点。内存节点尤其要仔细它描述的是物理内存范围内核在整个初始化阶段都在跟它打交道。一旦这里写得比实际内存大或小轻则浪费内存重则访问到不存在的地址直接 Data Abort。还有一种常见情况是 DTS 里model和compatible字符串写错导致内核无法匹配到对应的machine descriptor。例如你的板子明明是rk3568-evb.dts改出来的却保留了原来的model Rockchip RK3568 EVB而内核已内置了CONFIG_ROCKCHIP_RK3568两者本身兼容但如果板级定制的部分依赖machine匹配就会踩坑现象是配对的 pinctrl、时钟驱动没有被加载启动流程走到一半就完全静默了。3.2 内存初始化与地址映射不正确“Starting kernel”卡住有一大类原因是 U-Boot 阶段 DDR 初始化完成但设备树中描述的内存范围跟 U-Boot 实际配置不一致或者 U-Boot 搬运内核镜像时覆盖了同一段内存。我在 NXP i.MX8MP 上就碰到过一个极具迷惑性的案例DDR 容量 2GBDTS 里/memory节点写了reg 0x0 0x40000000 0x0 0x80000000U-Boot 里也确认了bdinfo显示memstart 0x40000000但内核镜像被load到了 0x42000000而 DTB 被加载到了 0x48000000两者都在内存范围内看似没有问题。实际跑起来却发现 U-Boot 在解析booti命令时会默认使用一个内部的image_addr相关区域做拷贝当它跟设备树地址重叠时内核启动早期访问设备树就拿到了被覆盖的数据结果同样卡死在Starting kernel之后毫无输出。这个情况的排查要点是在 U-Boot 命令行下用md命令读取 DTB 头部确认内容是否完整md 0x48000000 8正常你会看到d00dfeed开头的 magic number小端模式是edfe0dd0。如果看到的内容不对那基本说明 DTB 在搬运或加载过程中被破坏了。3.3 内核配置裁剪过度或启动参数错误很多团队为了压缩镜像体积在内核 menuconfig 里裁剪了一堆“看起来用不到”的模块。嵌入式平台不是不可能这么干但裁剪要有底线。卡在启动早期的几个常见“裁剪事故”把CONFIG_SERIAL_CORE和CONFIG_SERIAL_8250编成了模块而不是编进内核。早期启动阶段根文件系统还没挂载模块根本没有加载机会串口自然没输出——可你看到的现象却是“Starting kernel 后无任何日志”第一反应常常是错误地怀疑内核镜像坏了删掉了CONFIG_PRINTK或CONFIG_EARLY_PRINTK。这个属于极端情况但确实有人为了压镜像尺寸干过删掉后整个排查难度陡增启动参数里console指定的设备编号与实际硬件不符。比如 SoC 的调试串口是ttymxc1i.MX 平台惯例或者ttyS08250 类写错了内核其实已经启动成功只是所有日志都送错了地方启动参数这块我建议不要在“裸奔”状态下做过多的root传递。排查启动早期问题可以让内核在没有根文件系统的情况下尽量输出日志至少能看到内核起没起来。一个常用的最小化启动参数consolettyS0,115200 earlyconuart8250,mmio32,0xfe215000不加root内核会停在VFS: Cannot open root device的报错上。这是一个很好的信号因为它说明内核已经正常完成初始化问题根本不在内核移植而在根文件系统。3.4 U-Boot 配置与源码分支问题最后这一类往往最玄学但也最常见于“从主分支拿的 U-Boot 直接编译”的情况。U-Boot 针对不同 SoC 家族有不同的板级配置移植时如果选错了defconfig或者手工修改配置时把关键配置项搞错了启动流程在跳转内核前就会埋雷。比如CONFIG_SYS_LOAD_ADDR、CONFIG_SYS_TEXT_BASE、CONFIG_SYS_SDRAM_BASE这三项如果有任何一个与硬件实际不一致轻则启动参数错误重则 U-Boot 自己都跑不稳。我见过一个比较典型的案例某团队在一颗全志 V3s SoC 上做产品改完 U-Boot 后启动串口正常输出 U-Boot 启动 logo但执行bootcmd后也停在Starting kernel ...。最后发现他们用的 U-Boot 版本默认 CONFIG_SYS_TEXT_BASE是 0x4A000000而板子上的 DDR 地址范围是 0x40000000~0x40000000 64MBCONFIG_SYS_SDRAM_BASE 却被他们改成了 0x40000000。U-Boot 本身能跑因为前面一段代码运行在片内 SRAM 或引导ROM里但加载内核时把镜像放到 0x4A000000 就完全越界了。做量产项目时每次拿到原厂或社区的 U-Boot我会先通读include/configs/board.h把里面跟内存基地址、镜像加载地址相关的宏全部标注出来再对照板子原理图确认。这一步虽然繁琐但能省掉后面好几个通宵。4. 从根因到修复逐类击破的实操记录4.1 设备树加载地址不一致问题的模拟排查假设你的板子用了 RK3568 这颗 SoCU-Boot 打印完Starting kernel ...后完全无输出。第一步并不是去查内核源码而是回到 U-Boot 命令行手动引导一次并把中间关键地址打印出来setenv bootargs consolettyS2,1500000n8 earlyconuart8250,mmio32,0xfe660000 load mmc 0:1 0x0027f800 Image load mmc 0:1 0x08300000 board.dtb booti 0x0027f800 - 0x08300000如果此时有Switch to non-boot CPU这类的信息出来说明内核第一个 CPU 已经跑起来了问题大概率在 CPU 后续初始化。如果依旧毫无输出就执行md 0x08300000 4确认 DTB 头部magic是edfe0dd0小端。如果这里已经是deadbeef或者ffffffff说明 DTB 没正确加载到内存问题在文件系统加载阶段而不是内核。DTB 地址和设备树里reg描述的地址空间是两个概念很多人混淆。前者是指给内核传参数的物理地址后者是描述内存和外设的位置。把它们混为一谈排查时就会走弯路。4.2 控制台设备未正确匹配的复现与修复再来看另一类非常隐蔽的问题控制台初始化永远比预期晚导致你看不到任何早期输出。之前我在某 ARM64 平台上做过一次移植U-Boot 打印正常内核镜像大小和加载地址都核对无误但是跳转后没有任何输出。后来我在 cmdline 里增加earlycon参数并确认串口地址无误的基础上把CONFIG_SERIAL_8250_CONSOLE打开、CONFIG_SERIAL_8250_DW打开后重新编译内核输出立刻就出来了。查根因后发现这颗 SoC 的串口控制器用的是 DesignWare 8250 IP但默认配置只编译了标准的8250驱动没打开DW的 variant。内核启动后期执行console_init时8250 core 探测不到对应的设备树节点因为驱动模型没有匹配控制台一直起不来所以你看到的系统就是“死了”实际上内核算是活着甚至可能顺利挂载了根文件系统。修复路径很简单在menuconfig里选中Device Drivers Character devices Serial Support for DesignWare 8250编进内核而不是模块。这类问题告诉我们“Starting kernel 后没有打印”和“内核没起来”不能直接画等号。不要在什么都没有验证的情况下就给内核判处死刑先确认控制台这条路通不通。4.3 内核入口处立即崩溃的定位方法当你确定 U-Boot 跳转正确、控制台配置也正确但内核就是没输出时就该动用反汇编和 JTAG 了。ARM64 平台的入口代码在arch/arm64/kernel/head.S入口会设置页表、开启 MMU然后跳转到start_kernel。整个过程很短暂任何一个异常都会跳转到异常向量表而此时控制台还没初始化所以表现为“死了但没任何日志”。用 JTAG比如 OpenOCD GDB连接目标板在跳转前设好硬件断点target remote :3333 hbreak *0x0027f800 continue断点命中后单步执行观察pc寄存器是否能够正常往前跑。如果单步时报Cannot access memory at address ...说明 PC 访问的地址根本不在有效内存范围内回头看三要素镜像链接地址、U-Boot 加载地址、SoC 实际内存地址。这个方法还能帮你确认内核是否在 MMU 开启后访问了非法地址。因为 MMU 一开启所有地址访问都要经过页表翻译如果页表建立的映射与实际物理内存不匹配典型情况是 DTB 内存节点出错就会立即触发异常。4.4 内存节点错误导致的启动中断最后再分享一个很容易忽略的知识点DTS 里 memory 节点的最小粒度。如果你的 SoC 支持 36 位物理地址而 DTS 里写成 32 位地址内核在early_mem解析时会把高地址截断映射出来的内存范围完全错误。更隐蔽的是如果 memory 节点没有写device_type memory内核在设备树解析早期就会把这个节点忽略导致系统认为没有任何物理内存。这种情况下部分架构会直接 panic部分是直接死循环都表现为启动早期无任何日志。这类问题排查时用fdtdump检查 DTB 里的/memory节点是一个可靠动作fdtdump board.dtb | grep -A 4 memory一个合法的内存节点至少包含memory40000000 { device_type memory; reg 0x0 0x40000000 0x0 0x80000000; };注意device_type memory是内核强依赖的字段缺少它时内核甚至不会打印任何告警直接认为“无内存”。这是很多人踩过的坑。5. 实战案例复盘一个看似无解的内核启动卡死前面讲了理论这里我复盘一个非常典型的实战案。整块板子基于一颗 ARM64 多核 SoC主频 1.5GHz双核 Cortex-A53外挂 1GB DDR3。现象如下U-Boot 正常启动能通过tftp下载内核和设备树手动引导booti 0x200000 - 0x1000000之后停在Starting kernel ...串口无任何后续输出earlycon正常添加U-Boot 里测试串口发数据没问题排查步骤第一步核对 DTB 内容和加载地址md 0x1000000 4显示edfe0dd0没问题第二步内核Image头部信息通过readelf -h Image确认是 AArch64 格式加载地址 0x200000 也符合链接地址第三步尝试booti 0x200000 - 0x1000000和bootm 0x200000 - 0x1000000两种命令现象一致第四步用 JTAG 连接在入口地址设断点发现断点根本未命中PC 直接跑飞到这里基本锁定问题在 U-Boot 跳转前的准备工作而不是内核。于是我把 U-Boot 的dcache状态作为排查重点。在do_bootm_linux的实现代码里cleanup_before_linux会执行一次完整的 Cache 清理和 MMU 关闭。如果这里存在问题跳转后内核拿到的是脏 Cache 数据指令可能还没执行就已经出错。最终定位到问题是 U-Boot 的板级配置里CONFIG_SYS_DCACHE_OFF没有被正确编译进去导致跳转前没有关闭 Data Cache。这段经验其实点出了一个常见误区很多人以为 U-Boot 一定会帮你把 Cache 关掉再跳内核实际上不同架构、不同 U-Boot 版本的处理逻辑并不统一有些平台依赖设备树里dma-coherent的属性来做 cache 维护一旦 DTS 里缺少该属性Cache 里的脏数据就成为隐患。解决方案在板级头文件include/configs/board.h中加入#define CONFIG_SYS_DCACHE_OFF或者确保 U-Boot 跳转前调用cleanup_before_linux并关闭 cache。修复后booti顺利进入内核启动日志完整打印。这个案例的参考价值在于整个排查过程没有一次“猜测试试”每一步都在缩小范围。最终定位到 U-Boot cache 维护的问题靠的不是玄学而是对启动过程的拆解和对关键寄存器的观察。6. 用这套方法论把内核移植卡死变成“可见问题”6.1 把排查步骤固化成脚本内核移植是个长期迭代过程每次修改板级代码后都可能重新踩一次相同的坑。我习惯把整套排查流程固化成一个 shell 脚本自动比对 DTB 关键节点、自动检查串口参数、自动校验加载地址然后把结果打印成日志。脚本的输入是U-Boot 的 bdinfo 输出和fdtdump 的 DTB 转储输出是一行行“OK”或“FAIL”。编写这种工具没什么技术门槛重点是把“人肉检查”变为“机器检查”避免每次移植不同板子时因为粗心漏掉某个步骤。我在公司内部分享时一直强调内核移植的难点往往不是改代码而是建立一个完善的验证闭环。6.2 移植后的启动冒烟测试清单移植结束前我会跑一遍自己的启动冒烟测试内核版本和编译时间uname -a内存容量是否被内核正确识别free -m串口驱动是否真正接管了控制台拔掉串口线再接上看启动日志能否继续输出CPU 核数是否匹配cat /proc/cpuinfo设备树中的外设节点是否都 probe 成功ls /dev/和dmesg | grep -i error这套清单执行下来一条龙排除掉 90% 以上“移植成功了但功能不对”的隐藏问题。如果你的板子连这些基础能力都不正常那就回到本章节前面的方法继续排查。6.3 我的体会做嵌入式 Linux 移植这些年最深的体会是“卡在 starting kernel”绝大多数情况下不是内核本身的 bug而是三要素镜像、内存、设备树中的某一样在特定环节出了问题。调试过程看起来千头万绪但只要掌握了 U-Boot 的跳转机制、设备树的解析过程、串口早期打印的开启方法配合 JTAG 做底层观测问题基本都能在半天之内定位到具体环节。最后再分享一个小技巧在做任何改动之前先在 U-Boot 里把当前每次启动用的环境变量完整保存下来然后利用bootargs的分号特性结合saveenv做一个“可回滚启动配置”。万一排查过程中把环境变量改乱了至少还有一份确定的基线可以恢复。这个习惯陪着我做完了十几个平台的移植工作从来没有因为环境变量问题多熬过一个通宵。
返回列表