ARTICLE DETAIL

资讯详情

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

嵌入式Linux启动卡在Starting kernel?从原理到实战的完整排查指南

嵌入式Linux启动卡在Starting kernel?从原理到实战的完整排查指南 搞嵌入式Linux移植的人十有八九都见过这行字U-Boot打印完Starting kernel ...之后屏幕就像死了一样串口一个字符都不再往外吐。你说它卡死了吧板上灯还在闪你说它活着吧又没有任何输出可以证明。我最早遇到这个问题是在给一块新板子适配内核的时候板子是自己画的DDR时序参考的是芯片手册默认值U-Boot能跑起来uImage也能加载偏偏就是卡在这一句后面一筹莫展。后来前前后后折腾了两三天把所有怀疑对象逐个排除才搞清楚问题出在内核镜像的加载地址和DDR实际布局对不上。这篇文章就把那次以及后来多次遇到的“卡在starting kernel”问题从原理到实操完整梳理一遍。我不打算只给结论而是把排查思路、关键命令、需要检查的内核配置项、以及那些最容易踩的坑全部摊开讲。如果你是刚开始做内核移植、正准备把主线内核跑到自己板子上的开发者这篇文章能帮你省掉大量瞎猜的时间如果你已经遇到了同样的问题顺着文中的排查路径走一遍大概率能定位到根因。1. 卡在starting kernel的本质你根本不知道内核有没有跑起来在开始排查之前先把“卡住”这件事本身拆开看。Starting kernel ...这行字是U-Boot在跳转到内核入口地址之前打印的最后一条信息打印完之后U-Boot就把CPU的控制权完全交给了内核。对于ARM平台来说接下来的动作是U-Boot设置寄存器如r0存放机器类型或设备树指针r1存放机器IDr2存放DTB物理地址关闭或保持MMU、cache的状态取决于U-Boot的实现跳转到内核镜像的入口地址通常是解压后真正的代码段起始位置。关键点在于从这行打印之后U-Boot就“撒手不管”了后续的一切都取决于内核是否真的被正确引导起来。如果你的串口什么输出都没有存在两种完全不同的情况要么内核压根没有执行起来要么内核已经在运行但它的早期串口输出没有送到你面前。我见过不少人在这个阶段陷入误区总觉得“一定是内核崩溃了”然后对着代码一通乱查。其实最节省时间的做法是先区分清楚下面这几种可能性再决定往哪个方向深挖。1.1 从U-Boot到内核跳转前后发生了什么我用一个容易理解的类比U-Boot就像机场的地勤它负责把乘客内核镜像送到正确的登机口内存地址然后广播一声“开始登机”打印Starting kernel ...。之后飞机能不能飞起来是飞行员内核自己的事地勤已经无法干预了。如果飞机没飞起来可能是乘客根本没上对飞机镜像不对可能是登机口给错了地址不对也可能是飞行员还没睁开眼睛早期初始化失败。对应到技术层面U-Boot在跳转之前做了什么直接决定了内核能不能起来# U-Boot中典型的启动命令 setenv bootargs consolettyS0,115200 root/dev/mmcblk0p2 rw load mmc 0:1 ${kernel_addr_r} /boot/zImage load mmc 0:1 ${fdt_addr_r} /boot/board.dtb bootz ${kernel_addr_r} - ${fdt_addr_r}bootz命令接收三个参数内核加载地址、ramdisk地址-表示没有、设备树地址。U-Boot会读取zImage头部的信息把内核重定位到合适的位置然后把DTB地址放到r2寄存器最后跳到内核入口。这一系列动作里任何一个环节出了问题内核都无法正常启动。而且问题往往不是“崩了”这么简单更多的是“压根没跳对地方”或者“跳过去了但环境不对”。1.2 现象分类无声无息、乱码、还是无限重启卡在starting kernel的表现其实有好几种我按实际见过的现象列一下现象特征大概率原因方向排查优先级打印完就彻底没动静串口干干净净内核未真正执行、内存地址错误、镜像损坏、DTB非法极高有输出但是乱码波特率不匹配、串口初始化失败、时钟频率不对高输出几个字符后停住早期初始化崩溃、cache/MMU配置问题高反复重启打印不断重复内核panic后看门狗复位、DDR不稳定、电源问题中有内核早期日志但到某个点停住某个驱动初始化失败、控制台未注册、死锁中每一种现象背后的排查路径差别很大。比如乱码多半不是内核代码的问题而是串口配置和实际硬件不匹配反复重启则要优先怀疑硬件稳定性而不是软件逻辑。我自己最头疼的就是“无声无息”这种信息量太少只能靠工具和配置逐步缩小范围。2. 开篇第一刀确认内核到底有没有被执行遇到问题时人的第一反应通常是看代码、查资料但在嵌入式环境下我建议先做最小化验证用最简单的方法确认“内核到底有没有跑起来”。这一步能砍掉一半的无效排查。2.1 用U-Boot的go命令做裸跳转测试U-Boot提供的go命令可以从指定地址直接执行代码绕过bootz/bootm这些复杂的加载流程。我们可以先用一个已知能运行的简单程序测试跳转链路是否正常。比如在U-Boot里写一段极简的汇编让它在串口输出一个字符.globl _start _start: ldr r0, 0x11000000 /* 假设这是UART数据寄存器地址 */ mov r1, #A str r1, [r0] b .编译成纯二进制后用load命令加载到内存再用go跳到那里执行。如果串口能收到A说明U-Boot到自定义代码的内存访问没有问题串口硬件也正常。这个测试不涉及内核却能快速排除“串口坏了”和“内存读不了”这两个基础问题。在真实的项目里我不会每次都写汇编但至少会在U-Boot环境里交叉验证一下内存内容。比如用md命令查看内核镜像所在内存区域的内容是否与bin文件一致# 查看内存内容确认加载到内存的镜像不是全0或乱码 md ${kernel_addr_r} 10 # 对比md5校验值 md5sum ${kernel_addr_r} 0x800000这一步能发现大量“加载阶段就出错”的情况TFTP传输断流、SD卡读取错误、DMA配置冲突等都会导致内存里的镜像和文件不一致。内核镜像本身就是坏的后面自然不可能跑得起来。2.2 确认加载地址是否落入DDR有效范围这是我在自研板子上栽过的跟头。DDR的起始地址和大小由控制器配置决定但U-Boot里的kernel_addr_r可能和实际DDR布局完全对不上。如果你的板子是512MB内存、起始地址0x40000000而U-Boot里设置的kernel_addr_r0x38000000那加载、校验、跳转看起来都“正常”但实际内核代码根本没有放入有效的内存地址执行必然失败。怎么确认在U-Boot里执行# 查看内存布局相关环境变量 printenv kernel_addr_r fdt_addr_r ramdisk_addr_r # 查看DDR范围不同架构/厂商命令略有不同 bdinfobdinfo会打印board info其中包含内存的起始地址和大小。把kernel_addr_r和它对比一下确保加载地址在DDR范围内并且留出足够空间别让内核解压时覆盖了DTB或者ramdisk。我个人的习惯是留足余量如果把内核镜像加载到0x40000000那DTB放在0x48000000ramdisk放在0x4C000000避免内核解压时相互踩踏。这一点在后续排查中非常重要尤其是zImage自解压时会把自己重新排列到内存的合适位置如果你的DTB刚好放在了解压目标区域附近启动过程就会变得诡异。2.3 检查内核镜像格式与U-Boot命令是否匹配这里的坑非常隐蔽。U-Boot的bootm需要uImage格式在zImage前面加了一个64字节的头bootz直接加载zImage而64位ARM常用的booti则用来启动Image格式未经压缩的内核映像。如果你用bootm去启动一个zImageU-Boot会尝试解析它的头信息如果解析失败会明确报错但有些情况下U-Boot的bootm会把zImage误认成uImage跳进去之后自然就死掉了。我建议在U-Boot里明确确认你实际用的命令和镜像格式# 查看镜像文件头确认格式 # uImage前64字节是U-Boot头以27051956Magic Number开头 md ${kernel_addr_r} 4 # 确认头部信息与镜像类型 iminfo ${kernel_addr_r}iminfo会解析头部信息并显示镜像类型、创建时间、校验和是否通过。这一步能发现镜像文件本身是否损坏、格式是否正确、CRC校验是否失败。我见过有人从网上下载了现成的uImage结果文件在下载过程中损坏了一部分U-Boot加载时也没有严格校验跳进去直接卡死在starting kernel。3. 把内核的嘴巴打开早期串口输出的正确姿势排除了“压根没跳转”的可能之后下一个要解决的就是“怎么让内核开口说话”。内核在启动早期串口驱动可能还没有初始化此时你想看到内核的运行状态必须显式打开早期打印功能。这也是解决此类问题最核心、最有效的手段。3.1 内核配置里必须打开的选项我这里以ARM平台为例打开早期串口输出需要以下几个内核配置项。不同架构和内核版本选项名略有差异但思路相通。# 内核早期调试相关配置 CONFIG_DEBUG_LLy # 底层调试输出支持 CONFIG_EARLY_PRINTKy # 早期打印支持ARM CONFIG_DEBUG_LL_INCLUDEdebug/8250.S # 根据你的串口芯片选择如果是ARM64平台情况稍有不同用的是earlycon机制配置项是CONFIG_SERIAL_EARLYCONy CONFIG_DEBUG_LLy CONFIG_EARLY_PRINTKy打开这些配置并重新编译内核后在bootargs里加入earlycon参数内核就会在极早期通过UART输出调试信息。对于ARM64平台常见的写法是setenv bootargs earlyconuart8250,mmio32,0x11000000 consolettyS0,115200 ...这里的0x11000000要替换成你板子上UART控制器的物理地址。earlycon的作用就是在内核正式初始化串口驱动之前直接操作这个物理地址进行输出。3.2 设置earlycon时常见的地址和时钟坑配置earlycon最典型的坑是地址写错。我见过不止一次有人把UART的虚拟地址当成了物理地址填进去结果内核启动时访问了一个根本不存在的内存映射直接异常。在U-Boot阶段MMU通常是关闭的你看到的地址是物理地址内核启动早期earlycon也直接操作物理地址。如果你拿到的数据手册里给的是总线地址或者经过某段地址映射后的地址需要仔细换算。另外earlycon里还有一个容易被忽略的参数——时钟频率。8250驱动需要知道UART的时钟源频率才能正确计算波特率分频系数。如果时钟频率填错串口输出的波特率就不对你在终端看到的就会是乱码。常见的写法是把时钟频率也放进参数里earlyconuart8250,mmio32,0x11000000,115200不同内核版本和驱动实现的支持程度不一样有的会从设备树里读有的必须显式指定。我的建议是不仅在bootargs里写还需要检查设备树里uart节点的clocks属性是否正确指向了实际的时钟源。3.3 用printascii做最原始的调试输出如果启用了CONFIG_DEBUG_LL内核里会多出一个非常原始的打印函数printascii它不需要完整的console框架只要UART寄存器映射成功就能用。在排查阶段我会在内核入口处加几行打印确认代码执行到的位置。内核从入口到start_kernel之间的代码在arch/arm/kernel/head.S和init/main.c里可以在可疑的位置临时加入// 在start_kernel开头加一行 void start_kernel(void) { printascii(Enter start_kernel\n); ... }但这种做法只适合本地调试改动内核源码后需要重新编译。而且printascii的输出不会自动加换行打印完必须手动加上换行符否则下一条输出会和上一条黏在一起看日志时很容易误判。我在实际项目中通常会在以下位置添加临时打印stext内核入口——确认CPU是否真正跳到了内核__mmap_switched——确认MMU切换是否完成start_kernel开头——确认C环境是否正常。如果代码到了start_kernel但串口没有输出而U-Boot阶段串口是正常的那问题十有八九出在串口驱动或时钟初始化上面。如果连stext都没走到则要回头查跳转地址和CPU状态。3.4 不要忽略loglevel参数还有一个经常被忽略的参数是loglevel。内核的printk输出有不同的级别0~7默认的console_loglevel可能只有4意味着级别高于4的调试信息不会输出到控制台。在排查阶段我建议直接加上setenv bootargs ... loglevel8 ignore_loglevelloglevel8把控制台级别调到最高ignore_loglevel则忽略所有日志级别的过滤。这样能确保凡是printk出来的内容都会显示在串口上不会因为级别过滤而漏掉关键信息。实际经验里很多“内核卡住”其实只是日志没打出来内核还在正常运行只是消息被过滤掉了。4. 设备树和bootargs内核启动的“导航系统”搞定早期打印之后正常情况下你能看到内核开始执行了。如果看到了一部分日志后仍然卡住或者连第一条日志都看不到就要把注意力转向内核启动早期会依赖的两个关键输入设备树DTB和bootargs。4.1 设备树不匹配引发的“启动中断”设备树是内核启动时的地图它告诉内核“硬件在哪、内存多大、串口用什么地址”。如果地图画错了内核自然找不到路。以下几种情况我都在实战中遇到过DTB物理地址超出了内核能访问的范围。32位ARM上如果DDR起始地址在0x40000000而U-Boot把DTB放到了0x08000000这个地址在DDR地址空间之外内核去读DTB时就会访问非法内存直接hang住。确认方法是在U-Boot里打印fdt_addr_r并和bdinfo的输出对比。DTB格式非法或版本过旧。有些U-Boot版本自带FDT库会对DTB做格式检查如果检查失败会直接报错。但有些情况没有任何提示内核拿到一个损坏的DTB解析到一半遇到非法数据就卡住。建议在U-Boot里执行fdt addr ${fdt_addr_r} fdt print如果U-Boot能正确解析并打印出设备树内容说明DTB本身是有效的。如果报错说明DTB文件生成有问题需要回查设备树源文件dts的编译过程。DTB和内核版本不匹配。主线内核和较老的板级补丁集之间设备树绑定的改动非常频繁。比如某个外设的bindings在新内核里改了属性名你把老DTB直接套在新内核上启动时解析到该节点就会失败甚至崩溃。最好的做法是让设备树和内核从同一个源码树编译出来避免版本错位。4.2 bootargs里的console参数决定你能否看到日志这是整个排查过程中最常见的根源之一。bootargs里console参数指定了内核控制台使用哪个串口以及波特率。如果这个参数和你的实际硬件不一致内核其实已经启动了但日志输出到别的地方去了你自然什么都看不到。有一个真实案例让我印象很深板子上的调试串口接到的是UART2地址是0x11010000但bootargs里写的是consolettyS0,115200。在Linux里ttyS0默认对应的是注册顺序排第一的串口而内核驱动对多个同类型串口的注册顺序很多时候和硬件编号不完全一致结果数据全部发到了UART1板上根本没有引出这个串口的引脚看起来就像是卡死了。排查方法很简单确认你的调试串口到底对应哪个ttySx编号。最快的方式是先让内核默认配置跑起来然后用cat /proc/tty/driver/serial查看各串口的状态。如果连系统都进不去就只能对照设备树里各uart节点的status、pinctrl、alias属性来判断。这里有一个实用技巧在设备树里给串口设置好serialNalias可以让内核按你的预期编号分配tty设备。例如aliases { serial0 uart2; /* 把UART2映射为ttyS0 */ serial1 uart1; };这样bootargs里的consolettyS0,115200就不会出现错位问题。这也是我后来给新板子做移植时的默认做法。4.3 从 “bootargs” 到内核启动参数里的内存信息除了consolebootargs里还有一类参数直接影响启动流程内存信息。部分平台如果不传mem参数内核会从设备树或U-Boot的ATAG里读内存布局但如果传了就以bootargs里的为准。我见过有板子的bootargs里残留了开发阶段的mem64M拿到256MB的板子上忘记改结果内核以为只有64MB内存启动到内存初始化阶段就出现异常表现也是卡在早期。这里要提醒的是有些U-Boot的发行版比较智能会动态生成bootargs把内存大小自动拼进去但如果你用的是手动设置的固定bootargs就要特别留意这种硬编码参数。排查时用printenv bootargs仔细检查里面的每一个参数尤其是mem、console、root、init这些关键字。5. 深入早期初始化从head.S到start_kernel逐段排除如果早期打印已经打开设备树和bootargs看起来都没问题但内核还是卡住那就需要深入内核早期初始化代码找出具体停在哪一行。这个过程比较细但能精确到指令级的问题定位。5.1 在内核汇编入口和C入口之间缝入打印ARM平台的内核入口在arch/arm/kernel/head.S它会先做CPU模式检查、构建页表、开启MMU然后跳转到start_kernel。这个过程如果出了问题任何C代码都跑不了只能用printascii或在汇编代码里添加串口输出。查看日志停在了哪一步。比如看到已经打印了Switching to non-boot CPU或者rcu_sched_grace_period之类的字眼说明内核已经完成了大部分早期初始化进入了调度器和中断初始化阶段。这时的问题通常和某些具体的驱动或中断控制器配置有关而不是早期启动链路本身的问题。以我自己的经验比较难排查的其实是“start_kernel前期全过、但在初始化某个子系统时卡住”的情况。有一次我碰到的问题表现是内核卡在Calibrating delay loop...当时第一反应是怀疑定时器驱动没写好导致loops_per_jiffy无法通过延时校准。后来用JTAG挂上去看发现是在注册中断控制器时死循环了因为设备树里GIC的interrupt-controller节点配错了地址内核去访问不存在的寄存器总线握手永远等不到回应卡死在那里。这种问题纯靠串口日志只能缩小到一个大致的区域真正定位还是需要JTAG或者认真的代码走读。5.2 CPU模式下开启MMU后出的幺蛾子还有一个很经典的场景MMU刚开启时会有一小段“过渡期”代码还在物理地址执行但已经通过新页表进行取指了。如果页表建立有误比如某个地址没做映射内核在开启MMU后的第一条指令就无法取指CPU直接挂掉。由于这个过程发生在任何打印之前你看到的自然只有Starting kernel ...然后就没有然后了。这种情况的排查非常棘手因为串口什么都给不了你。我当时的做法是缩小内核的KERNEL_RAM_VADDR与物理地址的映射关系确认页表代码里的偏移量是否正确。还有一次是U-Boot在跳转前关掉了cache但没关MMU内核head.S里对MMU状态的检测逻辑分支走到了异常路径也是卡死。在64位ARMARM64平台上情况稍有不同内核要求启动时MMU必须关闭如果你的U-Boot配置成了开启MMU进入内核同样会卡在入口处。这类问题没有捷径只能对照架构手册打印关键寄存器的值来确定CPU所处的状态。5.3 中断与异常向量表被忽略的“隐形杀手”内核早期初始化阶段会设置异常向量表如果向量表地址设置错误或者代码段在重定位后异常向量表没有同步更新一旦出现任何异常未定义指令、数据中止、IRQ等CPU就跳到一个无效地址同样表现为卡死。这类问题往往和设备树没有关系纯粹是启动代码背景下的细节。前阵子我处理过一个Armv7平台的案例内核启动到初始化中断控制器后一旦开启中断就立刻卡住。后来发现U-Boot传过来的机器类型ID和内核编译时的机器ID不一致Linux 3.x的内核在使用非设备树启动方式时机器ID必须匹配否则内核会在启动早期拒绝继续执行。虽然现在的内核基本都走设备树启动但如果你是用了比较老的平台或者自己写的bootloader还是要注意CONFIG_MACH_*相关配置是否包含了你板子的机器ID。6. 常见问题速查与独家避坑经验这一节把前面提到的各类情况浓缩成一份可以直接对着查的清单再加几个我不碰一次壁绝对学不会的经验。建议你遇到问题时先从表格里排除一遍再按前面章节的深入步骤走。6.1 卡在starting kernel常见原因速查表检查项快速验证方法解决方向内核是否真正被跳转U-Bootgo命令做裸跳测试检查入口地址、DDR范围镜像是否损坏md5sum、iminfo重新下载、校验TFTP/SD读取boot命令与镜像格式匹配确认bootz/booti/bootm选择对应U-Boot命令早期串口是否开启内核配置CONFIG_DEBUG_LL打开earlycon/earlyprintkconsole参数是否匹配检查bootargs与设备树alias修正ttyS编号、波特率设备树地址与格式fdt print重新编译DTB、调整加载地址内存布局是否错位bdinfo对比load地址修改环境变量bootargs里的mem参数printenv bootargs去掉硬编码mem或改写正确值MMU/cache状态确认U-Boot跳转前状态在跳转前关MMU/cache异常向量表JTAG看PC位置修正启动代码向量表这个表不是全量但覆盖了90%以上的可能原因。在实际排查过程中不要跳着看按照从上到下的顺序一项一项排除因为越靠前的检查成本越低覆盖的基础性问题也最多。6.2 我踩过的最深刻的几个坑第一个坑是U-Boot和内核的DTB传递方式不匹配。当时用的是比较老的U-Boot它把DTB地址通过r2寄存器传给内核但我的内核配置里开启了ATAG方式导致内核认为r2里存的是ATAG指针跑去解析一段无效内存直接hang住。解决办法是确保U-Boot和内核都走设备树启动流程或者在U-Boot里禁用ATAG传递。第二个坑是串口驱动注册顺序和ttyS编号对不上。前面也提到过设备树里没有显式配置alias的时候内核按照驱动探测顺序给串口编号而这个顺序经常和你想的不一样。结果console参数写的是ttyS0实际输出全跑到ttyS2上去了。这个坑最恶心的地方在于——如果你手里正好只有连接到ttyS2的串口线一切都是“正常”的但如果像我当时一样板子上只引出了UART0对应的物理串口而ttyS0对应的其实是UART1你就会看到屏幕一片黑严重怀疑人生。第三个坑是DDR频率调太高导致的数据不稳定。板卡在低温环境下能正常启动在室温下反而卡在starting kernel。这个现象本身就让人抓狂因为最开始根本没往硬件方面想。后来用连续多次启动做压力测试发现失败率会有波动才意识到可能是DDR训练参数在边缘状态偶尔一位跳变导致内核代码被取到错误字节。这个问题的解决和软件关系不大需要回去调DDR控制器参数或者降低频率这也是为什么我建议在排查早期就做好“多次重启”的实验习惯。第四个坑不那么常见但值得拿出来说U-Boot里的fastboot/secure boot功能。有些板子的U-Boot开启了安全启动校验跳转内核前会对内核镜像做签名验证验证失败但U-Boot没有明确报错因为配置被裁剪掉了错误打印表现也是卡在starting kernel。你查U-Boot代码才知道它压根没跳转只是卡在校验循环里。遇到这种情况要检查secure boot相关的配置和使能状态。6.3 一个完整的排查实录从“无声无息”到“顺利进入内核”最后分享一次完整的排查过程把前面所有方法串起来。有一块基于某国产应用处理器的板子现象是U-Boot正常能ping通、能加载内核但打印完Starting kernel ...后完全无输出。我按下面的顺序操作检查加载地址bdinfo显示DDR范围是0x40000000~0xBFFFFFFF2GB而U-Boot默认kernel_addr_r0x40800000在范围内没问题。验证镜像完整对SD卡里加载出来的zImage做md5sum和在PC上编译出来的文件一致说明加载无误。打开earlycon重新编译内核开启CONFIG_DEBUG_LL和CONFIG_EARLY_PRINTKbootargs里加上earlyconuart8250,mmio32,0x11000000,115200这里UART地址是从板子原理图拿到并对照芯片手册确认过的。重新启动看到了内核早期日志停在了Uncompressing Kernel...之后不久具体表现为输出到某个地址相关的错误信息指向PAGE TABLE创建阶段。走查设备树发现dts里CPU的reg 0x0 0x0而实际CPU的MPIDR值是0x80000000导致内核启动secondary CPU时无法找到对应的CPU节点在SMP初始化阶段卡住。修正设备树把CPU节点的reg值改成实际MPIDR重新编译DTB内核顺利启动。整个过程耗时半天真正的问题是设备树中CPU节点MPIDR跟硬件不一致。这个案例说明带上了早期打印之后问题从“完全摸不着头脑”变得有迹可循定位难度一下子降低了好几个档次。这类“卡在starting kernel”的问题绝大多数都不是什么玄学而是链路上某个环节没有对齐。只要把U-Boot到内核的交接过程拆解清楚一步步确认问题总能浮出水面。遇到这类现象时别慌先确保自己能看得见内核的运行状态再沿着启动链路逐段排查剩下的就是时间问题了。根据我的经验一旦earlycon能够正常输出90%的启动问题都能在半小时内定位到具体模块真正难的是那些连输出都没有的情况这时更要沉住气从最简单的硬件通路查起不要一上来就陷入内核源码的汪洋大海。
返回列表