🎯 本文主线(先吃颗定心丸):别被 GDB、OpenOCD、HardFault、栈帧这些词吓到——本篇专讲「板子跑飞了怎么定位根因」,你真正要懂的不是「怎么写调试器」,而是「芯片死机时已经把现场拍好了,照着读就能定位」。本篇只做三件事:用脚本造三种事故现场、用调试器把现场捞出来、看懂内核怎么接住死机。本篇主线就一句:死机定位就是「读现场」——内核早靠
los_exc.S接管了 HardFault,硬件自动把现场压进栈、内核分派成调用链,你用 OpenOCD+GDB(M4 端口 3334)连上,先手抄 PC/LR/SP 再拿 PC 对照.map锁死行号,真凶就现行了(比喻见下方🏠导读)。下面四站全是它的展开。
🗺️ 详细拆解路线(四站,跟着走不迷路):
- 🚉第一站·双剑合璧:
tools/里demo_hang.sh(造三种事故现场)+hang_probe.gdb(把现场挖出来),后面整篇围着它们转,先认识这两件排查神器。- 🚉第二站·三破坏现场:同一份能跑的工程,故意「少接一根线」,看 A/B/C 三种死法各长什么样——尤其 A 和 C 肉眼现象一样、根因天差地别。
- 🚉第三站·把现场捞出来:开三个终端,OpenOCD+GDB 连上 M4 协处理器(端口 3334),三板斧把 hang 点拽出来;并划两条红线(禁用
monitor reset halt、端口认准 3334)。- 🚉第四站·内核接住死机:为什么你能
bt?los_exc.S早已接管 HardFault,硬件自动压栈、内核分派成调用链,你才能顺着线索抓真凶。读完全文,记得回来看末尾的「全文总结」,它会把上面四站串成一句话。
🏠零基础导读(接第 13 篇 3.5 节那场 HardFault 现场):调试死机,本质就是「车祸现场勘察」——芯片自己撞了车,但它贴心地把"行车记录仪"留了下来(这正是第 13 篇 3.5 节讲过的 HardFault 现场,本篇手把手带你把它读出来)。先记这 6 个对应关系:
现实比喻 真名(芯片里叫啥) 死机 / 程序跑飞 车撞了:CPU 不知道下一步干啥,直接熄火 HardFault 最严重的车祸——车彻底撞烂,CPU 兜底喊停 寄存器现场(R0-R15、xPSR…) 行车记录仪:出事那一刻各项读数全记下了 los_exc打印的死机信息交警出具的事故报告:寄存器 + 调用栈 + 出错任务 三种破坏现场 三种典型事故原因(A/B/C 三场调度类事故) 定位根因 看记录仪 + 读报告,还原"出事前谁在调谁",揪出真凶
一、第一站·双剑合璧:造现场与挖现场的两件神器
动手前先认识工程里两件排查神器——
demo_hang.sh负责「造三种事故现场」,hang_probe.gdb负责「把现场挖出来」。后面整篇都围着它们转,先认全貌。
一句话理解:这俩脚本一个负责「造事故现场」,一个负责「把现场挖出来」——后面整篇都围着它们转。
工程里tools/目录下有两件排查神器:
| 脚本 | 干啥的 |
|---|---|
tools/demo_hang.sh | 一键切换「三种卡死演示现场」a / b / c,或restore还原基线 |
tools/hang_probe.gdb | 连上 M4 协处理器,halt 在真实 hang 点,直接bt看调用链 |
- GDB:GNU 调试器,连上芯片后能读寄存器、看函数调用链,是「把现场挖出来」的主力。
- OpenOCD:开源调试服务器,一头接调试器硬件、一头当 GDB 的「服务端」,让 GDB 能远程戳到芯片内部。
它们最妙的设计:demo_hang.sh首次运行会把app/main.c、app/app_task.c、los_dispatch.S快照到.demo_hang_bak/,之后每次切换都「先还原快照、再施加改动」。你可以反复 a→b→c→restore 来回切,不会把工程玩坏。
🚏第一站小结:
demo_hang.sh造三种现场、hang_probe.gdb挖现场;两者配.demo_hang_bak/快照,能反复 a→b→c→restore 来回切而不玩坏工程。
二、第二站·三破坏现场:同一份工程,三种死法
认完工具,来看「事故」本身。同一份能跑的工程,故意「少接一根线」,看三种死法各长什么样——这是理解「为什么灯不亮」的活教材。
一句话理解:同一份能跑的工程,故意「少接一根线」,看三种死法各长什么样——这是理解「为什么灯不亮」的活教材。
demo_hang.sh准备了三组「少接一根线」的教学对照实验。每组只改一处,现象却各不相同:
| 方案 | 动了哪里 | 肉眼现象 | 真实根因(技术版) | 一句话人话(到底哪根线没接) |
|---|---|---|---|---|
| A | 绕开LOS_Start(),在main()里直接RedLedTask() | 红灯常亮、绿灯不亮 | PendSV 优先级没配、CONTROL.SPSEL 仍为 0(还用 MSP)、PSP 从未初始化——调度链四缺五,卡在第一个LOS_TaskDelay | 调度器根本没真正接管:任务还用着启动时的主栈(MSP),没切到它自己专用的线程栈(PSP),一进第一次延时就卡死 |
| B | 在LOS_Start()之前插HalTickLock()掐掉 SysTick | 两灯亮后冻住 | 心跳停摆,任务跑到LOS_TaskDelay后醒不过来 | 系统心跳被人为掐了:任务睡着后没人把它叫醒,两灯亮一下就冻住 |
| C | los_dispatch.S里cpsie i改成cpsid i(关中断) | 红灯常亮、绿灯不亮 | SysTick 中断进不来,GDB 读0xE000ED04的bit28(PENDSVSET)恒为 1 | 全局中断被关死,连切任务的"换班铃"PendSV 都进不来,红灯亮完绿灯没机会上场 |
表里几个吓人的词,先各给一句直白说明:
- PendSV:内核用来「切换任务」的专用中断(“换班的铃”)。
- MSP / PSP:两个栈指针。MSP 是「启动/中断时用的主栈」,PSP 是「任务运行时用的线程栈」。任务要跑起来,得切到 PSP。
- CONTROL.SPSEL:控制位,决定当前用 MSP 还是 PSP。它还是 0,说明还停在 MSP,任务栈没生效。
- SysTick:内核心跳定时器,任务延时/调度全靠它。心跳被掐,任务永远睡过去。
关键认知:A 和 C 肉眼现象完全一样(都是红灯常亮),但根因天差地别。光看现象分不出,必须靠 GDB 读寄存器。
💡可复用经验:复现 / 隔离 bug 时用单变量对照:每次只改一处,看现象怎么变。A/B/C 三种死法「肉眼相同、根因不同」,靠眼睛分不出,必须靠 GDB 读寄存器。你自己的板子出问题,也先「只改一处」复现,别乱动多处。
📌图注:课堂三破坏现场 A/B/C 对照——同样"板子死了",根因可能是栈溢出、数组越界、野指针三种不同的"凶手"。后面教你怎么从现场照片(栈帧)认出是哪个。
🚏第二站小结:A/B/C 三种死法同份工程各改一处——A 调度器没接管(卡在首次延时)、B 心跳被掐(醒不过来)、C 关中断(PendSV 进不来);尤其 A 与 C 肉眼同相、根因不同,必须靠 GDB 不靠眼睛。
三、第三站·把现场捞出来:OpenOCD + GDB + 3334
事故现场摆好了,怎么把它「解剖」开?开三个终端,用调试器把「卡住那一刻」的寄存器原样读出来——死机不是终点,是起点。
一句话理解:死机不是终点,是起点——开三个终端,用调试器把「卡住那一刻」的寄存器原样读出来。
三终端协作,把 hang 态「解剖」开:
# 终端1:用「正确方式」把 M4 跑进 hang 态(不是 reset,是常驻)bashtools/demo_hang.sh a--flash# 终端2:起 OpenOCD,当 gdb server,常驻监听 M4 的 3334 端口openocd-fE:/m4/m4_liteos/openocd/board_stm32mp157_m4.cfg# 终端3:连上去,hang_probe.gdb 已写死连 :3334,连上即 halt 在真实 PCarm-none-eabi-gdb-xE:/m4/m4_liteos/tools/hang_probe.gdb E:/m4/m4_liteos/build/liteos_m.elf连上之后,三板斧定现场:
(gdb) info reg pc # 当前卡在哪条指令 (gdb) info symbol $pc # 这条指令属于哪个函数 (gdb) bt # 打印调用链,看是谁调到了这里💡协处理器是啥?STM32MP157 是个双核芯片:A7 是「主驾」(跑 Linux),M4 是「副驾」(跑我们的 RTOS)。M4 不是独立上电的,得 A7 把它「释放」出来才跑。所以你不能像对待单核芯片那样随手一个
reset把 M4 复位。
红线禁区:不要用monitor reset halt
hang_probe.gdb开头用大段注释警告过:STM32MP157 的 M4 是协处理器。如果你敲monitor reset halt,会把 M4 复位到0x00000008(复位向量)的假象——因为 A7 主核不会重新释放协处理器,你看到的 PC 不是真实 hang 点,是复位假象。正确做法:M4 早已在 hang 态跑着,gdb 一连上就 halt 在真实 PC,直接bt即可。
端口口诀:M4=3334,A7=3333
双核芯片有两个 gdb 端口,连错核就什么都看不到。hang_probe.gdb已写死:3334(M4 专属)。记牢这个口子,别去敲 3333(那是 A7 的)。
💡可复用经验:双核芯片里 M4 是「协处理器」,调试时禁用
monitor reset halt——它会把 M4 复位成 A7 没重新释放的假状态,PC 看到的是假象。正确姿势:连对端口(M4=3334、A7=3333)、gdb 一连上就 halt 在真实 PC,直接bt。
🚏第三站小结:三终端协作——
demo_hang.sh a --flash跑进 hang 态、OpenOCD 监听 3334、GDB 一连即 halt;三板斧info reg pc/info symbol $pc/bt拽出 hang 点。两条红线:禁用monitor reset halt、端口认准 3334(A7 是 3333)。
四、第四站·内核接住死机:为什么你能bt
📌图注:板子跑飞先别慌,也别急着复位。内核在死机瞬间已经自动拍了张"黑匣子"照片——把出事时的寄存器、调用现场全存进栈。你后面要做的,就是读出这张照片,顺着线索抓真凶。
你以为死机是「程序飞了啥都没了」?其实内核早把「致命错误」接好了,所以
bt才能打印出调用链。这一站讲清这条「黑匣子」是怎么拍的。
一句话理解:你以为死机是「程序飞了啥都没了」,其实内核早把「致命错误」接好了,所以bt才能打印出调用链。
不是。内核早就把「致命错误」接好了——靠的就是第 14 篇引出、但没展开的那个文件:kernel_liteos_m/.../gcc/los_exc.S(异常 / 死机处理)。
📌HardFault 是什么?(先说人话)它是芯片级别最严重的一类「错误中断」——当 CPU 执行时遇到了自己兜不住的险情(取到非法指令、访问禁止地址、除零等),它不硬着头皮乱跑,而是「举手投降」跳进一个叫 HardFault 的特殊处理函数。一句话:CPU 慌了,喊「这活我干不了」,于是整车熄火进急救。它是所有故障的「最终兜底」——底下 MemFault / BusFault / UsageFault 兜不住的,全归它管。
📌空指针 / 野指针为什么致命?指针本质就是"一个地址"。空指针指向地址 0(通常是保留/无存储区),野指针指向已释放或从未分配的随机地址。CPU 顺着这个地址去"取指令/读数据",要么取到垃圾、要么踏进禁区——硬件立刻报错,程序当场飞掉。
📌栈溢出为什么会死机?栈"从顶向低地址长"(满递减栈),栈的下面紧挨着
.bss、内核堆甚至代码区。一旦函数嵌套太深/局部数组太大,栈一路往下"长"出边界,就踩到下面那些内存——数据全乱了,自然死机。这也正是死机常常"莫名其妙"的原因:真凶在栈溢出那一刻,症状却表现在老远之外。
4.1 异常分派链(为什么你能看到调用链)
📌图注:这张剖视图把"黑匣子"拆开给你看:死机时,CPU 自动把一堆寄存器现场压进栈,摞成一叠"证据"。哪一格数值不对劲(比如返回地址被改成乱码),就能反推是哪类破坏。配合第 21 篇的 backtrace 一起看。
一句话理解:故障一触发,硬件先自动存好现场,再顺着内核写好的「分派链」一路收口成可调用的信息。
当 HardFault / MemFault / BusFault / UsageFault 触发时,硬件自动把R0-R3 / R12 / LR / PC / xPSR压进栈,然后跳进los_exc.S:
硬件触发 HardFault → 自动压栈 R0-R3/R12/LR/PC/xPSR → HalExcHardFault(los_exc.S 的强符号) → 读 HFSR(0xE000ED2C) / FSR(0xE000ED28) 判是 Bus / Mem / Usage 哪类 → osExcDispatch → HalExcHandleEntry(C 函数,把现场收成 EXCCAUSE + 调用链) → 你就能 bt 了- HFSR / FSR是两个「故障状态寄存器」:硬件把「是哪类故障、错在哪」记在里面,内核读它们判断该走哪条处理分支。
los_exc.S和los_dispatch.S(PendSV/SysTick)一起,不靠同名强符号覆盖弱别名,而是靠HalHwiInit把SCB->VTOR重定向到内核的g_hwiForm数组,再把HalExcHardFault/HalPendSV/SysTick_Handler等直接填进g_hwiForm对应槽位。所以你一行启动文件没改、内核 submodule 禁改,两边靠约定对接——死机时内核的 Handler 自然顶上。
💡死机第一反应:板子一旦死机,先别急着改代码——第一步是把
los_exc打印的那串寄存器(尤其 PC / LR / SP)原样抄下来,再拿 PC 去对照.map文件(或info symbol $pc)定位它对应哪一行代码。PC 一锁定,根因就缩小到一行了,比瞎猜快十倍。📌PC / LR / SP 各是啥?PC(程序计数器)= 出事那行的地址;LR(链接寄存器)= 谁叫你来干的(上一层调用者);SP(栈指针)= 当时的栈顶(还原调用栈的地基)。PC 告诉你"死在哪一刻",LR 告诉你"谁把你叫来的",SP 告诉你"现场家底堆在哪"——这仨配合,才能把事故现场读出来。
🚏第四站小结:
los_exc.S早已接管 HardFault——硬件自动压栈 R0-R3/R12/LR/PC/xPSR,再经HalExcHardFault → 读 HFSR/FSR → osExcDispatch → HalExcHandleEntry分派成调用链,你才bt得出来。死机第一反应:先手抄 PC/LR/SP,再拿 PC 对照.map锁行号。
五、全文总结(回到主线)
本文一句话总结(建议收藏):死机不是世界末日——内核早靠
los_exc.S接管了 HardFault,硬件自动把现场压进栈、内核分派成调用链,你只要用 OpenOCD+GDB(M4 端口 3334)连上,先手抄 PC/LR/SP,再拿 PC 对照.map锁死行号,真凶就现行了。(三种破坏现场 A/B/C 同一份工程各改一处,单变量对照、靠 GDB 不靠眼睛。)
用四站回顾一遍:
- 🚉第一站·双剑合璧:
demo_hang.sh造三种现场、hang_probe.gdb挖现场,配.demo_hang_bak/快照能反复 a→b→c→restore 不玩坏工程。 - 🚉第二站·三破坏现场:A/B/C 同份工程各改一处——A 调度器没接管、B 心跳被掐、C 关中断;A 与 C 肉眼同相、根因不同,靠 GDB 不靠眼睛。
- 🚉第三站·把现场捞出来:三终端 + OpenOCD + GDB(M4=3334),三板斧
info reg pc/info symbol $pc/bt;禁用monitor reset halt、端口认准 3334。 - 🚉第四站·内核接住死机:
los_exc.S接管 HardFault,硬件压栈 +HalExcHardFault → HFSR/FSR → osExcDispatch → HalExcHandleEntry分派成调用链,你才bt得出来;先抄 PC/LR/SP 再对.map。
🧰本篇可复用经验速记(照着做,别只读):
- □ 造现场:
demo_hang.sh切 A/B/C 三态 +.demo_hang_bak/快照,单变量对照,不靠眼睛靠 GDB。- □ 连 M4:OpenOCD 端口认准 3334(A7=3333),禁用
monitor reset halt,连上即 halt 直接bt。- □ 捞现场三板斧:
info reg pc/info symbol $pc/bt,先手抄 PC/LR/SP。- □ 锁行号:拿 PC 对照
.map文件定位死在源码哪一行。- □ 收尾:排查完
demo_hang.sh restore回干净基线,别留破坏态。
术语表(本文出现)
| 缩写/符号 | 含义 |
|---|---|
| GDB / OpenOCD | GNU 调试器 / 开源调试服务器(连芯片读寄存器/设断点) |
| demo_hang.sh | 演示脚本:切换三种卡死现场 a/b/c 或 restore 还原 |
| hang_probe.gdb | GDB 脚本:连 :3334、halt 在真实 PC、bt看现场 |
.demo_hang_bak/ | 基线快照目录(demo_hang.sh 还原用) |
| PendSV | 内核切换任务的专用中断(“换班铃”) |
| MSP / PSP | 主栈指针 / 线程栈指针(任务运行时切到 PSP) |
| CONTROL.SPSEL | 控制位:决定当前用 MSP 还是 PSP(=0 用 MSP,任务栈未生效) |
| SysTick | 内核心跳定时器 |
| HalTickLock | 内核 API:掐掉 SysTick 心跳(方案 B 用它) |
0xE000ED04(ICSR) | 中断控制状态寄存器(方案 C 读它 bit28 = PENDSVSET 恒为 1) |
| PENDSVSET | ICSR 的 bit28:悬起 PendSV 标志 |
| cpsie i / cpsid i | 汇编指令:开/关中断(方案 C 把cpsie i改成cpsid i关中断) |
| HardFault | 最严重兜底故障(CPU 兜不住的险情都落这里) |
| MemFault / BusFault / UsageFault | 存储器管理错误 / 总线错误 / 用法错误(可配置故障;未使能或升级时全归 HardFault 管,HardFault 才是最终兜底) |
los_exc.S | 内核异常/死机处理汇编(强符号接管 HardFault 等) |
| Default_Handler | 启动文件默认异常实现(死循环);本工程靠HalHwiInit把SCB->VTOR重定向到g_hwiForm绕开它(不是同名强符号覆盖——内核用的是HalExcHardFault等不同名符号) |
| HFSR / FSR | 硬故障/故障状态寄存器(地址 0xE000ED2C / 0xE000ED28) |
| HalExcHardFault / osExcDispatch / HalExcHandleEntry | los_exc.S / C 里的异常分派函数 |
| PC / LR / SP | 程序计数器 / 链接寄存器 / 栈指针(死机现场三关键读数) |
backtrace /bt | 调用栈回溯:把函数调用链从栈里扒出来 |
| A7 / M4 | Cortex-A7 主核(跑 Linux)/ Cortex-M4 副核(跑 RTOS,协处理器) |
| 协处理器(coprocessor) | 双核芯片里 M4 这颗"副核",需 A7 释放后才独立跑 |
| 空指针 / 野指针 | 指向地址 0 / 已释放随机地址的指针,解引用致死机 |
| 栈溢出(stack overflow) | 栈越过边界踩到下方内存,导致数据错乱死机 |
系列导航
上一篇:第 19 篇 · LED 任务实战与错误码 |本篇:第 20 篇 · 死机定位:los_exc 与三破坏现场| 下一篇:第 21 篇 · 死机定位:FPU 栈帧与 backtrace板级适配与应用 07–25:07 工程全景 | 08 BSP | 09 SystemInit | 10 lds上 | 11 lds下·解剖 | 12 lds下·照妖镜 | 13 启动文件 | 14 接管中断 | 15 target_config上 | 16 FPU栈档 | 17 改错速查 | 18 LED详解 | 19 LED实战 |20 los_exc| 21 FPU栈帧 | 22 八环节 | 23 全文件对账 | 24 六类判据 | 25 经验增删改留
基础 01–06:01 环境准备 | 02 源码下载裁剪 | 03 os_config | 04 Makefile | 05 链接脚本 | 06 烧录
切换汇编 01–06:汇编1 | 汇编2 | 汇编3 | 汇编4 | 汇编5 | 汇编6
代码仓库:stm32mp157-liteos-m @ Gitee · 基线 v0.1-baseline · 全部版本标签
mp157-liteos-m @ Gitee](https://gitee.com/tstcoder/stm32mp157-liteos-m) · 基线 v0.1-baseline · 全部版本标签