☰
死机定位_los_exc与三破坏现场
2026/10/27 10:16:22 网站建设 项目流程

🎯 本文主线(先吃颗定心丸):别被 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后醒不过来系统心跳被人为掐了:任务睡着后没人把它叫醒,两灯亮一下就冻住
Clos_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 / OpenOCDGNU 调试器 / 开源调试服务器(连芯片读寄存器/设断点)
demo_hang.sh演示脚本:切换三种卡死现场 a/b/c 或 restore 还原
hang_probe.gdbGDB 脚本:连 :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)
PENDSVSETICSR 的 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 / HalExcHandleEntrylos_exc.S / C 里的异常分派函数
PC / LR / SP程序计数器 / 链接寄存器 / 栈指针(死机现场三关键读数)
backtrace /bt调用栈回溯:把函数调用链从栈里扒出来
A7 / M4Cortex-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 · 全部版本标签

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询