ARTICLE DETAIL

资讯详情

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

嵌入式Linux启动卡死排查:从U-Boot到内核的调试完整指南

嵌入式Linux启动卡死排查:从U-Boot到内核的调试完整指南 嵌入式工程师应该都遇到过这种场景uboot正常启动打印完“Starting kernel ...”之后串口就像断气一样一行输出都没有控制台彻底沉默。内核移植过程中卡在starting kernel这一步可以说是调试路上最常见也最磨人的问题之一。漫长等待后复位、重新编译、反复烧写结果还是老样子这种时候很容易让人怀疑人生。这篇文章我就把这类问题的排查思路完整梳理一遍从“为什么这里会卡”到“每一步怎么定位”再到我实际项目中踩过的坑和验证过的修复方案全部写清楚。不管你是刚接触内核移植的新手还是已经被这个问题折磨了几天的老手按着这套方法走一遍大概率能把问题范围缩小到具体某一个环节。1. 项目背景与问题现象卡住的本质是交接失败1.1 起点uboot如何把控制权交给内核要搞懂“Starting kernel”为什么会是最后一行输出先得理解这一行背后发生了什么。uboot在引导内核时经过一串复杂的准备动作初始化DDR、加载内核镜像到内存、解压如果需要、设置启动参数ATAG或device tree、关闭中断和cache、最后跳转到内核入口地址。打印“Starting kernel ...”这个信息时uboot其实已经完成了绝大多数工作正在执行最后一条指令把PC指针跳进内核代码段。从这一刻开始控制权就交接给了内核。内核的第一个动作通常是运行汇编入口如stext随后进入start_kernel的C代码路径。如果内核在这两步中的任何一步挂掉后果都一样没有输出因为此时串口和其他硬件都还没被内核初始化。这也是为什么这个问题如此让人头疼——uboot阶段一切正常看起来“该做的都做了”但内核就是没反应。其实卡住的位置五花八门可能是参数没传对可能是内核本身崩在早期初始化也可能是串口配置不同步导致有输出你看不见。这些情况表面上都表现为“Starting kernel之后无输出”但排查路径完全不同。1.2 这类问题的典型表现与影响范围我见过太多人一看到“卡在starting kernel”就急着改内核代码、重编uboot结果折腾半天毫无进展。问题在于这个现象覆盖的原因面太广了没有先定位就乱改基本等于开盲盒。典型表现有这么几种uboot串口输出正常bootargs和bootcmd看起来都对执行bootm或bootz后打印Starting kernel然后就没有然后了。还有一种情况是串口最后输出到一行奇怪的地址、异常向量或者打印了一部分函数名后卡住。少数情况下串口干脆连Starting kernel都没打全比如只打了“Sta”就停了。第三种最迷惑人你甚至看不到“Starting kernel”而是整个终端假死连uboot的提示符都不出来。从影响范围来看这个问题在新板卡第一次跑系统、替换内核大版本、切换交叉编译工具链、修改dts后都容易爆发。可以说只要动过内核相关的东西都有可能在某个时刻撞上它。所以这不是一个“会不会遇到”的问题而是“什么时候遇到”的问题。提前把排查思路理清楚能节省大量无谓的时间。2. 排查前的准备工作把手上的“仪表盘”调出来2.1 串口是唯一的窗口先确认串口配置很多人卡住之后第一步就是翻代码这是错误的方向。调试内核启动问题第一件事应该是确保你手上有一个可靠的观察窗口——通常就是串口。我对串口的要求很简单用示波器或逻辑分析仪确认TX/RX波形正常波特率、数据位、停止位、校验位全部匹配硬件流控关闭。这里有个细节很容易忽略uboot阶段的串口波特率和内核阶段可能不一致。比如uboot里配置的是115200但内核dtb或bootargs中用的console参数却是另一个波特率就会出现“uboot正常输出内核其实已经跑起来但你看不到”的情况。这不是内核卡死而是通信双方频率没对上。另外串口线虚接也会制造假象。特别是用USB转串口模块时接触不良会导致输出半截就断看起来就像内核卡住了。我建议排查时先用loopback测试串口线完整性或者在uboot里反复执行打印命令确认输出稳定再开始往下查。硬件层面的问题不排除后面所有软件排查都会建立在不可靠的基础上。2.2 让内核“说话”earlycon与log_buf内核启动早期串口驱动可能还没初始化此时如果发生异常你什么信息都看不到。为了尽早拿到内核日志我们需要在内核启动初期就把串口“点着”这就是earlycon机制的价值。earlycon本质上是一个早期控制台实现在真正的串口驱动注册前直接操作寄存器输出字符。通过bootargs传入earlycon参数内核在非常早的阶段就能向串口打印信息。有了earlycon内核启动日志可以提前到汇编之后的最早C代码段这对定位“卡在starting kernel”至关重要。具体到不同平台earlycon的写法有差异。ARM平台通常写成earlyconuart8250,mmio32,0x01c28000或更通用的earlycon配合dtb里chosen节点的stdout-path。要注意的是earlycon的地址必须和实际串口寄存器地址一致否则输出照样起不来。这块我会在后面的实操部分详细展开。log_buf是另一个关键点。内核日志缓冲区会在启动早期初始化如果系统在后续某个环节崩溃重启后通过crash工具或pstore还可以看到log_buf残留的部分日志。平时调试时建议打开CONFIG_LOG_BUF_SHIFT并设置较大值并开启CONFIG_PSTORE这样即使串口输出中断依然有机会从内存镜像中挖出有用信息。2.3 建立可靠复现环境在动手改代码之前先把调试环境搭好。我经历过太多次“烧一次uImage等五分钟”的低效循环所以现在的做法是尽量用网络加载内核镜像uboot下通过TFTP下载内核文件系统用NFS挂载。这样改完内核重新编译后直接从服务器拉取新镜像就行省去反复插拔SD卡或USB烧录的麻烦。如果是全新板卡网络可能还没调通那至少准备双份启动介质。比如SD卡和eMMC各放一套镜像万一某一个介质启动有问题可以互相验证是软件还是存储本身的问题。另外bootdelay设置不要设成0留出2-3秒的按键时间方便随时中断进入uboot交互模式调整bootargs和启动命令。我还会在启动命令里加上bootmenu或预设多个boot选项一个是正常启动一个是带earlycon和debug参数的启动还有一个是不加载dtb直接booti的裸启动选项。这样每次复位后敲个数字就能切换启动方式调试效率提升不是一点半点。3. 核心原因拆解到底哪些环节会“卡死”交接3.1 bootargs错误频率最高的翻车原因在系统没有输出到串口的情况下最先怀疑的不是内核代码而是启动参数。bootargs是uboot传给内核的“遗嘱”里面写着console用哪个串口、root设备在哪、内存多大——这些参数中任何一个错误都可能导致内核无法正常启动。最常见的是console参数配置错误。比如内核编译时默认console是ttyS0而你的串口实际对应ttyS2或者波特率不是预设值这时内核可能已经正常启动但所有log都发往了错误的端口自然什么都看不到。解决方法是显式指定console参数consolettyS2,115200n8也可以同时保留早期控制台earlycon consolettyS2,115200n8有了earlycon后即使后续console没匹配上至少有前期的输出可以定位。内存参数同样致命。内核启动时会根据dtb或ATAGS中的内存信息建立页表如果uboot传的内存大小和实际DDR大小不匹配内核访问超出范围的内存就会触发data abort直接死在早期初始化阶段。这部分问题排查时一定要确认uboot中bdinfo显示的内存值、内核dtb中memory节点的值、以及板卡实际内存大小三者一致。3.2 DTB与内核版本不匹配设备树DTB是内核与板卡硬件之间的“桥”桥接失败的问题也相当普遍。一个内核可以支持多种硬件平台具体跑在哪个平台上全靠DTB中的model和compatible属性来匹配。如果DTB里的compatible和内核machine_desc中定义的compatible对不上内核启动时就会报“unable to find a machine”或直接走默认路径行为不可预期。还有一个容易被忽略的点DTB由DTS编译而来而不同版本的内核要求不同版本的DTCdevice tree compiler。老DTC编译出的DTB塞给新内核可能因为某些属性格式不兼容而解析失败新DTC编出的DTB放到老内核里也可能出现意外字段被忽略或误解。所以我建议始终使用与内核源码配套的DTC来编译设备树不要图省事用系统自带的旧版本。另外现在很多bootloader支持在启动时对DTB打补丁比如修改mac地址、追加bootargs如果补丁程序处理不当破坏了DTB结构内核解析时就会挂掉。这类问题的特点是直接启动没问题通过bootloader加载就卡住。排查方法也简单——用uboot的fdt print命令确认DTB是否完整以及启动时传给内核的DTB地址是否正确。3.3 内核早期初始化阶段挂死DDR、时钟、PLL如果bootargs和DTB都没有问题接着关注内核早期初始化代码。内核启动早期要做的事情非常多建立页表、初始化CPU、设置栈、初始化时钟系统、初始化DDR控制器这些步骤全在C语言的main函数之前或刚开始的阶段。时钟或PLL配置错误在内核早期初始化时很容易导致死循环或总线挂死。比如内核尝试切换CPU频率时因为PLL配置不对导致锁相环无法锁定程序就会停在等待标志位翻转的循环里。这类问题没有打印输出因为此时串口还没初始化看起来就是“卡死”。DDR控制器和内核的关系也值得注意。uboot已经初始化过DDR内核启动时通常不会再重复初始化而是直接读取配置。但有些内核版本为了支持cpuidle或深度睡眠会在启动早期重新配置DDR控制器的某些寄存器。如果这个配置过程和板卡的实际DDR芯片不匹配系统就会立即崩溃。这类底层问题的排查比较依赖硬件调试手段。我曾经在一块ARM板卡上遇到类似情况最后是用逻辑分析仪抓取DDR的CS信号发现内核启动过程中CS信号异常跳变才定位到是DDR时序配置的问题。如果没有硬件仪器退而求其次的方法是修改内核代码加入串口输出比如在start_kernel前面调用一个自写的早期print函数逐步缩小崩溃范围。3.4 外设驱动的“隐形杀手”如果系统已经能打印一部分启动日志但在某个外设驱动的初始化阶段停止看起来也像是卡住但性质不一样。比如initcall中某个驱动去访问不存在的硬件寄存器或者等待某个中断永远不来都会导致系统“停摆”。这类“隐藏炸弹”通常在换了内核版本或改了dts后暴露出来。比如dts里adc节点配置了某个GPIO作为通道输入但那个GPIO被其他外设复用驱动初始化时就会冲突。我之前遇到过一次系统卡在内核中段打印机不断刷“failed to request GPIO”最后定位为两个外设的GPIO配置重叠了。解决这类问题关键是让内核把更多的启动过程日志打印出来。把console_loglevel调高加上loglevel8和ignore_loglevel参数开发阶段可以让所有级别的日志都输出到串口。这样即使驱动hang住至少能看见它挂在哪个驱动的哪一行代码附近。配合内核的DEVMEM和JTAG调试基本能定位到具体函数。4. 一步一步排查从现象到结论的完整实操4.1 第一步区分“内核压根没跑”还是“跑起来没输出”拿到卡住的现象不要急着改代码先做一次快速判断内核到底有没有在执行。我用过最直接的办法是看硬件信号。不用特别高级的设备一个几块钱的LED接到某个GPIO上在内核代码最早期比如stext开头加几句gpio操作让它闪烁。如果LED亮了说明内核已经跳转到入口并在执行如果LED完全没反应说明跳转本身就没成功问题出在uboot到内核的交接环节。led点灯法虽然土但非常有效。特别是新板卡刚上电时第一版内核连串口都没有完全调通灯光就是最原始的状态指示器。后来我习惯了保留这段“调试灯”代码直到系统稳定再移除。补充一句如果内核指令集或入口地址有误跳转后会触发undef instruction异常或直接跑飞现象表现为LED闪一下就灭或者串口出现奇怪的地址打印。这类异常可以通过设置内核的CONFIG_DEBUG_LL和earlyprintk来提前捕获。我的建议是底层问题先用硬件手段确认再用软件手段细化。4.2 第二步用earlycon拉出内核的第一声如果确认内核确实在跑但还是没日志输出那就要想办法把它“喊醒”。earlycon是最优先的选择。以我常用的ARM64平台为例调试时可以这样设置setenv bootargs consolettyS0,115200 earlyconuart8250,mmio32,0x10000000 root/dev/nfs rw nfsroot192.168.1.10:/srv/nfs ipdhcp这里0x10000000是串口的寄存器基地址。不同SoC的串口地址不同务必查阅芯片手册确认。如果写错earlycon依然不会输出。有个小技巧值得分享可以先不加载内核在uboot用md命令读取串口寄存器的值确认地址是否正确。比如查看UART的线控寄存器LCR和波特率分频寄存器如果读出来的值和预期一致说明地址没有写错。另外部分平台对earlycon的支持比较特殊。比如某些厂商的BSP要求在内核config中额外选中CONFIG_SERIAL_EARLYCON否则即使bootargs写了earlycon也不会生效。编译内核前记得检查这个配置项。earlycon一旦正常工作你会看到比原来多得多的启动日志问题范围也会大幅缩小。4.3 第三步确认DTS与内核对板卡的定义拿到earlycon日志后第一件事是确认内核是否正确匹配了你的板卡。日志的前几行通常会显示类似Machine model: MyBoard Rev.2如果没有这一行或者显示的是其他板子的名字说明DTB没配对或者compatible匹配逻辑出了问题。查看dtb中的模型信息可以在uboot环境里直接操作fdt addr 0x41000000 fdt print / model如果输出为空或乱码说明DTB加载有问题。还要重点检查chosen节点中的stdout-path这个属性告诉内核默认控制台是哪个设备如果写错了console参数可能直接被忽略。还有一种情况是内核编译时没有包含对应板卡的machine_desc。比如新加了板卡定义但没加到内核config中对应的ARCH列表里导致compatible匹配永远失败。这个问题在相对小众的SoC上更常见检查时一定要确认板卡相关文件确实被编译进内核了。4.4 第四步缩小到具体挂死点当日志输出到某个位置后就停住说明问题就藏在那附近。比如日志停在[ 0.901234] Serial: 8250/16550 driver, 4 ports, IRQ sharing enabled下一步就是打印更多信息。我常用的手段是打开内核的initcall_debug选项setenv bootargs ... initcall_debug开启后内核会逐个打印每个initcall的调用与返回哪个环节挂掉一目了然。如果initcall_debug依然看不出端倪那就上二分注释法。把dts里怀疑的外设节点先禁用或者把initcall中可疑的驱动编译为模块而不是内建一次只改一个变量逐步缩小范围。这个过程看似笨拙但往往是最稳的。还有一个实用的技巧把内核的CONFIG_DEBUG_LL和CONFIG_EARLY_PRINTK同时打开并在start_kernel的各级函数调用之间插入早期打印代码配合printk的延迟循环可以精确到某个函数内的某条语句。虽然粗暴但在没有JTAG的情况下这种“土法打点”往往是解决问题的唯一手段。4.5 第五步修复与验证定位到根因之后修复方案通常就比较直接了。如果是bootargs配置错误改uboot的环境变量如果是earlycon地址拼错改成正确的寄存器地址如果是dts兼容性不匹配补全板卡的compatible定义如果是驱动冲突调整外设节点配置或修改GPIO复用。修复后一定要做回归验证。我的习惯是把同样的问题场景摆出来比如反复复位几十次、切换启动介质确认同样的卡死现象不再出现。还要换一套交叉编译工具链试试排除不同编译器版本对代码生成的影响。我遇到过一种极其隐蔽的情况同一个源码用旧版GCC编译能正常启动新版GCC编译就卡死在starting kernel最后发现是编译器把某个volatile变量优化掉了导致设备驱动误判硬件状态。5. 常见问题速查与避坑实录5.1 排查速查表我把自己遇到过的和同行交流时听到的典型问题整理成了一张速查表排查时对着看可以节省大量时间现象可能原因验证手段解决方案串口完全无输出console参数与串口号不匹配查看uboot环境变量显式指定console参数有早期输出但后续中断earlycon地址配置错误检查寄存器基地址核对芯片手册修正地址内核日志到某一行停止initcall中驱动挂起开启initcall_debug禁用可疑设备节点能启动但不稳定内存参数不匹配核对uboot bdinfo与dtb修正memory节点DTB加载后启动失败DTC版本不兼容fdt print检查内容换用内核配套DTC修改dts后卡死外设硬件冲突逐个禁用外设节点调整GPIO复用配置新版GCC编出来的内核卡死编译器优化引入问题换旧版GCC对比检查volatile关键字使用复位一次卡死一次DDR控制器配置问题逻辑分析仪抓取信号修正DDR时序配置这张表不一定是全的但覆盖了绝大部分情况。每个问题背后都对应着一次“血泪教训”后面我会挑几个重点展开。5.2 我踩过的几个坑第一次遇到这个问题我以为是内核代码有bug花了两天时间去调内核内存管理结果最后发现是uboot的bootargs里tmp字符串大小不够console参数被截断成了consolettyS系统当然没有任何输出。从那以后我在调试启动参数前都会先看uboot的printenv输出确认参数完整。还有一个坑比较隐蔽串口模块的电平标准不对。TTL电平的串口模块接到了RS232电平的接口上uboot阶段偶尔能输出几个乱码内核阶段则完全沉默。当时我排查了所有软件可能性最后用示波器一看根本就是电平信号不匹配。所以硬件层面的基础检查一定要做在前面。DTS的reg属性写错也是高频问题。我曾经把串口的reg地址从0x10000000误写成0x10010000跳一位地址结果内核无法访问串口寄存器所有输出都丢掉了。这种数字差一个就全盘皆输的坑只有靠细心核对才能避免。我现在写DTS时都会对照芯片手册把每个外设的地址画个表格防止手滑。另一个很实际的教训是在没怀疑到uboot参数传递之前不要动内核源码。内核源码大、编译慢、修改风险高而启动流程中很多问题恰恰出在外围环境。我个人的经验是先用最短路径排查外围再深入内核时间利用率会高很多。6. 复盘与个人心得一次次排查“starting kernel”卡死问题我最大的体会是这类问题没有银弹唯一的出路是系统化缩小范围。先把串口调可靠再依次检查bootargs、DTB、内核早期日志、驱动初始化每一步都有明确的验证方法不用盲目猜测。这种“先外围后内核、先参数后代码、先现象后假设”的思路其实适用于几乎所有嵌入式启动问题。我也越来越重视保留现场。每次遇到卡死我先把串口完整日志保存下来再把uboot的printenv输出、fdt print输出、内核config文件一起归档。很多时候问题不是当场就能解决的过几天再回头翻日志结合当时的硬件配置往往能发现之前忽略的细节。把这些资料整理成问题单跟同事讨论时也有据可依而不是嘴上说“就是卡住了不知道为啥”。最后分享一个小技巧在uboot里准备一条“全调试”启动命令把earlycon、initcall_debug、loglevel8这些参数全打开并且设置一个异常长的bootdelay比如10秒。平时不用遇到疑难杂症时一键启动拿到最完整的日志再做分析。这个习惯帮我解决过不止一个莫名其妙的问题也推荐给你备而不用总比到用的时候再临时抓瞎强。
返回列表