总结:系统基数知识为四类:分别为死机类型、Map表重要信息、Lst表重要信息、ARM基础知识
Ⅰ、死机类型:
- ASSERT死机、
- 空指针死机
- 野指针死机
- 看门狗死机
- 无coredump死机
Ⅱ、Map表重要信息:
- RAM、Flash的合法地址
- .bss\.data\text等各区段的地址分布
- thread stack地址与大小
- ram free情况
Ⅲ、Lst表重要信息:
- 根据LR、BackTrace确定最后的函数入口
- 函数执行过程、掌握一些汇编指令(汇编指令可以问ChatGPT)
Ⅳ、ARM基础知识:
- RO~R15寄存器的作用
- MSP与PSP
- 中断关系
- thread优先级关系
- 入栈与出栈
- coredump基础知识
分析:第一类:死机类型
1. ASSERT死机(断言失败死机)
-
本质 :这是一种主动的、受控的程序崩溃,是开发者在代码中故意设置的"安全检查点"。
-
触发条件 :当程序运行到
ASSERT(condition)语句时,会检查condition这个条件是否为真。如果为假(即程序状态违反了开发者预想的不变量、前置或后置条件),断言失败,系统主动触发死机/崩溃。 -
代码中可以设置断言,当断言的条件为false时,则触发死机。
ASSERT死机包括但不限于,malloc死机、看门狗死机、BTCcrash死机、mailbox死机 -
典型原因:
-
函数参数无效(如传递了NULL给不允许为NULL的参数)。
-
数据结构损坏(如链表断裂、计数溢出)。
-
状态机处于非法状态。
-
在中断上下文中调用了可能睡眠的函数。
-
-
特点与价值:
-
开发阶段的宝贵工具:ASSERT在调试版本中通常启用,在发布版本中关闭。它的目的是在开发阶段尽早、清晰地暴露程序逻辑中的隐藏错误。
-
信息明确:死机时通常会打印出文件名、行号、失败的条件表达式,指向问题根源非常直接。
-
它不是"意外"的硬件错误,而是程序逻辑错误的明确告警。
-
2. 空指针死机(空指针解引用)
-
本质 :这是一种被动的、非法的内存访问。
-
触发条件 :当程序试图通过一个值为
NULL(或0)的指针去访问或修改内存时,CPU会触发一个内存访问异常(如Data Abort, Segment Fault)。 -
根据PC、或寄存器的值为0,则判断为空指针死机
比如PC=0x0000000000,或者R0=0x0000000000并以R0的地址寻值,会触发空指针,
常见的空指针死机,多为:静态变量未赋值就引用、指针free后并置为NULL后被引用 -
典型原因:
-
指针未初始化就使用。
-
函数返回NULL,调用方未检查直接使用。
-
资源申请失败(如
malloc,kmalloc返回NULL)后未做错误处理。 -
对象已被释放,但指针未置NULL(与野指针相关)。
-
-
特点:
-
崩溃地址通常是一个低地址(如0x00000000,0x0000000X)。
-
在有MMU的系统中,该地址通常是未映射的,会立即触发异常。
-
问题相对容易定位,因为崩溃点就是解引用的那一行代码。
-
3. 野指针死机(野指针解引用)
-
本质 :这是最危险、最随机、最难调试的一类内存访问错误。
-
触发条件 :指针指向一个已经被释放、无效或未知的内存区域。当通过该指针访问内存时,行为不可预测。
-
根据PC、或寄存器的值不为0且不是合法地址,则判断为野指针死机。(合法范围见下面第二类Map表的RAM、Flash合法地址)
比如PC=0x000000084,或者R0=0x00000084并以R0的地址寻值,会触发野指针
常见的野指针死机,多为:局部变量未初始化就引用、栈溢出、指针free后不置为NULL被引用、踩内存
-
典型原因:
-
释放后使用 :内存/对象被
free或delete后,指针未置NULL,后续再次使用。 -
作用域问题:使用了局部变量的地址并传递出去,函数返回后该地址失效。
-
指针计算错误:数组越界访问后,误修改了相邻内存中的指针变量。
-
多线程环境下,一个线程释放了内存,另一个线程仍在使用。
-
-
特点:
-
崩溃地址是随机的、不可预测的。
-
崩溃可能发生在访问操作的很久之后,因为内存可能已被重新分配用作他途。
-
可能导致数据静默损坏(写入到其他合法数据结构中),在真正崩溃前系统行为已经异常。
-
调试极其困难,需要借助内存调试工具(如Valgrind, AddressSanitizer, 硬件Watchpoint)来捕捉。
-
4. 看门狗死机(看门狗超时复位)
-
本质 :这是一种系统自我恢复机制,是硬件或系统软件触发的复位,而非软件直接崩溃。
-
触发条件 :系统有一个硬件或软件看门狗定时器。主程序需要在定时器超时前周期性地"喂狗"(重置定时器)。如果未按时喂狗,则认为系统已"死锁"或"跑飞",看门狗电路会自动触发整个系统的复位。
-
看门狗死机,是指MCU固定一段时间会喂狗一次,当喂狗不及时,就就会触发看门狗死机。
常见的看门狗死机:线程卡死,中断不退出 -
典型原因:
-
主线程阻塞:陷入死循环、不可中断的睡眠、或长时间关中断。
-
调度问题:高优先级任务饿死低优先级的喂狗任务。
-
中断风暴:CPU被中断长期占用,无法执行主任务。
-
喂狗线程意外退出。
-
-
特点:
-
崩溃现象是系统重启,而非某个程序的异常退出。
-
问题不在崩溃点,而在导致无法喂狗的根本原因上。
-
调试方法:检查喂狗任务的状态,分析系统在复位前的最后日志,使用调试器回溯死锁位置。
-
5. 无coredump死机(无转储死机)
-
本质 :这不是一种独立的死机原因,而是一种现象描述------系统发生了严重错误,但未能生成用于事后分析的核心转储文件。
-
触发条件:在发生致命错误(如空指针、野指针、断言失败)时,生成coredump的过程本身失败或被破坏。
-
目前这个没有特别的规律。以下为遇见次数从高到底排列:
1、bt_drv驱动不对,导致死机
2、电压不稳,用io口外部供电负载电流大、串口上拉5V电平不匹配、Vcore设置异常导致的死机。
3、cp打印没有开启,cp死机
btc死机(个别patch会死机无log)
5、主频错误、PLL分频异常。
6、flash读写异常,某个地方就是读写不了,芯片直接卡住。(没遇到过,但是听说别人有遇到 -
典型原因:
-
磁盘空间不足:无法写入巨大的coredump文件。
-
权限或路径问题:coredump保存目录不可写。
-
系统配置关闭 :coredump功能被禁用(
ulimit -c 0)。 -
内存严重损坏:崩溃破坏了coredump生成机制所需的关键数据结构(如内存管理器的元数据)。
-
内核崩溃:用户态的coredump机制依赖于内核,如果内核本身崩溃(如驱动里的野指针),则无法生成用户态coredump。此时需依赖内核转储(如Kdump)。
-
硬件致命错误:严重的ECC内存错误、总线错误等,可能导致CPU立即停止或复位。
-
-
特点与挑战:
-
"死无对证":缺乏最关键的现场快照,使得问题复现和定位极其困难。
-
调试此类问题需要依赖:
-
系统最后的日志输出。
-
串口控制台的崩溃信息(如Oops信息)。
-
硬件调试器(JTAG/SWD)直接抓取崩溃瞬间的CPU寄存器和内存状态。
-
增加更早、更详尽的日志记录。
-
-
总结与对比
| 死机类型 | 本质 | 关键特征 | 调试线索 |
|---|---|---|---|
| ASSERT死机 | 主动暴露逻辑错误 | 信息明确(文件、行号、条件) | 断言表达式、程序逻辑 |
| 空指针死机 | 非法访问已知无效地址 | 崩溃地址为低地址(如0x0) | 调用栈、指针来源 |
| 野指针死机 | 非法访问未知无效地址 | 崩溃地址随机、行为不可预测 | 内存调试工具、崩溃现场内存内容 |
| 看门狗死机 | 系统自恢复机制 | 整体复位、喂狗中断 | 喂狗任务状态、系统阻塞点 |
| 无coredump死机 | 转储失败现象 | 缺乏核心现场文件 | 最后日志、内核信息、硬件调试器 |
在实际工作中,遇到死机问题,通常会按照以下思路排查:
-
首先查看是否有明确的崩溃信息(如ASSERT信息、Oops信息)。
-
如果有coredump,优先分析它。
-
如果无coredump,分析最后日志,判断死机前系统在做什么。
-
检查是否是看门狗复位,如果是,重点排查系统阻塞或任务调度问题。
-
对于随机性死机,高度怀疑野指针 或内存溢出,需要使用专项工具进行长时间压力测试捕捉。
-
考虑硬件问题(如内存条接触不良、电源不稳)的可能性,尤其是在排除了所有软件可能之后。
分析:第二类:Map表重要信息
一、RAM、Flash 的合法地址
这是什么
合法地址是指 芯片硬件实际存在的存储器地址范围,程序必须在这个范围内分配代码和数据,否则就是"非法地址",运行必崩。
Map 文件里怎么看
Map 文件本身不主动告诉你"芯片合法地址是多少",但它显示的所有段地址都必须落在合法范围内。你需要做的是:
-
拿到芯片手册(或链接脚本)确认合法地址区间。
-
Cortex-M 主流 MCU(STM32、GD32、NXP):
-
Flash:
0x08000000 ~ 0x080xxxxx(大小由型号决定) -
RAM:
0x20000000 ~ 0x200xxxxx
-
-
CC26xx/CC13xx(TI 无线芯片):
-
Flash:
0x00000000 ~ 0x000xxxxx(片内 Flash) -
RAM:
0x20000000 ~ 0x200xxxxx
-
-
RTL87x2G(瑞昱蓝牙芯片):
-
Flash:
0x08000000 ~ 0x080xxxxx -
RAM:
0x00100000 ~ 0x0010xxxx(紧耦合内存 TCM)+0x20000000 ~ 0x200xxxxx(系统 RAM)
-
-
-
打开 Map 文件,找到所有段的起始地址和结束地址,逐一比对。
-
必须检查 :
.text、.rodata、.data(加载地址和运行地址)、.bss、堆、栈。 -
任何段地址落在区间外 → 链接脚本错误,立即修正,否则芯片根本不会执行。
-
典型错误例子
-
误把 Flash 起始设为
0x00000000,但芯片实际 Flash 在0x08000000→ 启动后 PC 指向空白地址,跑飞。 -
.bss结束地址 =0x2000B000,芯片 RAM 最高是0x2000AFFF→ 严重超限,运行后 .bss 清零会破坏外设寄存器或直接 HardFault。
一句话记住
合法地址是硬件划定的红线,Map 里所有地址都必须在线内。
BES平台:
Map表中,存在一个总表,可以搜关键字"FRAMX"找到
Memory Configuration

所有可以访问的地址,都必须在上述范围内。0x2c000000~0x22c010000 是boot使用,APPbin正常不可访问。
注意,如果一个地址是变量,但是对这个地址执行跳转指令也是错误的。
Flash区表示实际的物理地址
Flash_NC区主要区分只读的属性。比如常见的const字符串,或者宏常量就常使用这些地址。
FLASHX区主要是区分XR的属性。表示可执行
二、.bss/.data/.text 等各区段的地址分布
这是什么
这是 Map 文件的 绝对核心,直接告诉你:
-
代码占了多少 Flash(.text + .rodata + .data 的加载区)
-
全局变量占了多少 RAM(.data + .bss)
-
每个段的起始地址、结束地址、大小
Map 里长什么样(以 GCC/ARMCC 输出为例)
==============================================================================
Section Cross References
==============================================================================
.text 0x08001000 0x3A4C // Flash 地址,长度 14924 字节
.rodata 0x08004A4C 0x1230 // Flash 地址,只读常量
.data 0x20000000 0x04E8 // RAM 地址,长度 1256 字节
load address 0x08005C7C // **这行极关键**:初始值存在 Flash!
.bss 0x200004E8 0x09A0 // RAM 地址,长度 2464 字节,不占 Flash
.heap 0x20000E88 0x0800 // 堆起始地址、大小(可选)
.stack 0x20001688 0x1000 // 栈起始地址、大小(可选)
==============================================================================
每段必须理解到骨子里的细节
| 段名 | 运行时位置 | 加载位置(初始值) | 占用 Flash? | 占用 RAM? | 存储内容 |
|---|---|---|---|---|---|
| .text | Flash | Flash(原地执行) | ✅ | ❌ | 函数指令 |
| .rodata | Flash | Flash | ✅ | ❌ | const、字符串字面量 |
| .data | RAM | Flash | ✅ | ✅ | 已初始化的全局变量(非 0) |
| .bss | RAM | 无(启动时清零) | ❌ | ✅ | 未初始化/初始化为 0 的全局变量 |
| .heap | RAM | 无 | ❌ | ✅ | 动态内存池(malloc) |
| .stack | RAM | 无 | ❌ | ✅ | 函数调用栈、局部变量 |
两个最容易算错的内存公式(必须背)
-
程序占用 Flash 总量 =
.text+.rodata+.data(只计load address那部分)
→ 有的工具链把.data加载区合并显示为.text,注意区分。 -
程序静态占用 RAM 总量 =
.data+.bss
→ 堆和栈是动态分配的,不在此列。
Map 里找这些信息的具体位置
-
MDK/ARMCC :打开
.map文件,搜索Memory Map of the image,会列出所有执行段。 -
IAR :搜索
Memory map summary。 -
GCC :搜索
Memory Configuration或Linker script and memory map。
实战检查清单
-
✅
.text结束地址 < Flash 最高地址 -
✅
.rodata结束地址 < Flash 最高地址 -
✅
.data加载地址(LMA)+ 大小 < Flash 最高地址 -
✅
.data运行地址(VMA)+ 大小 < RAM 最高地址 -
✅
.bss结束地址 < RAM 最高地址
BES平台:
以.bss为例,确定.bss的地址分布。
搜索map中"bss"、确定.bss起止地址
搜索map中"_data"、确定.data起止地址
确定text的分布,注意.text包含很多段,比如boot_struct\,boot_text_flash\,boot_text_sram等等
搜索Flash起始地址0x2c010000,就可以得到.text段的起始地址。
boot_struct 0x2c010000 0x10 _flash_start=. /表示整个boot_struct占用0x10字节, 0x2c010000+00x10就是下一个段起始地址
boot_text_flash 0x0c010010 0x1560 load address 0x2c010010 flashx_start_addr=./表示.bpoot_text_flash占用0x1560字节, 0x2c010010+0x1560是下一个段起始
依次跟踪,可以得到最后的.text地址
三、thread stack 地址与大小
⚠️ 最重要且最容易混淆的一类
你必须先确认自己处在 裸机单栈 还是 RTOS 多任务 环境,Map 文件能提供的信息完全不同。
BES平台:

3.1 裸机 / 单线程系统(只有主栈)
-
Map 里一定有明确的栈段,名字通常是:
-
STACK(ARMCC) -
CSTACK(IAR) -
.stack(GCC)
-
-
如何看地址和大小
在 Section 列表里找到该段,例如:
.stack 0x20001688 0x1000 // 起始 0x20001688,大小 4KB 【0x1000(十六进制)= 4096(十进制)4096 字节 = 4 × 1024 字节 = 4KB】栈顶(初始堆栈指针 SP)通常在该段的末尾地址:
__initial_sp = 0x20002688。这个符号在 Map 的 Symbol Table 里可以找到。
3.2 RTOS 多线程系统(用户线程栈)
Map 文件里没有名为 thread_stack 的段!
线程栈是在运行时 通过 xTaskCreate 或 osThreadNew 动态创建的,不是链接时分配的 ,因此Map 不直接包含它们的信息。
那怎么看线程栈的地址和大小?
情况 A:静态分配的任务栈(最常见于小型 RTOS)
-
你在代码里定义了一个全局数组作为栈,例如:
static StackType_t task1Stack[512]; -
去 Map 的 Global Symbols 表里搜索数组名,会看到它的地址和长度。
task1Stack 0x20001A88 0x0800 // 任务栈在 .bss 段,大小 2KB -
此时你可以直接从 Map 得知这个任务栈在 RAM 的具体位置。
情况 B:动态分配的任务栈(使用 malloc / pvPortMalloc)
-
栈在运行时从堆里分配,Map 完全看不到。
-
必须通过 RTOS 提供的 API 在运行时查询:
-
FreeRTOS:
uxTaskGetStackHighWaterMark() -
RT-Thread:
rt_thread_control()或rt_thread_measure_stack_water_mark()
-
关键结论
-
裸机主栈:Map 里明明白白,直接读。
-
RTOS 任务栈 :静态分配 → 搜数组名 ;动态分配 → 运行时 API。
-
任何声称"Map 文件能直接显示所有任务栈"的资料都是错误的。
3.3 栈使用量(水线)------ Map 帮不上忙
Map 只给总大小 ,用了多少 、还剩多少必须运行时看:
-
裸机:填充栈区域为固定模式(如
0xCC),运行后扫描。 -
RTOS:使用上述 API。
-
调试器:查看 SP 指针,结合栈底地址,估算当前使用量。
四、ram free 情况
这是 Map 信息里最容易被高估的一项 ------Map 只能回答"编译时静态还剩多少",回答不了"运行时还剩多少"。
BES平台:

4.1 静态剩余 RAM(从 Map 直接算)
定义 :链接器把 .data、.bss、堆、栈都安排好后,RAM 里还有多少字节是完全没有被分配给任何用途的。
计算公式
静态剩余 RAM = 芯片 RAM 最高地址 - max( 已分配区域最高地址 )
已分配区域最高地址 是以下三者的最大值:
-
.bss结束地址 -
堆结束地址(
__heap_end) -
栈起始地址(注意栈是向下生长的,起始地址就是栈底,结束地址是栈顶?要谨慎)
在 Map 里找这些地址
| 你需要找 | 常用符号名(GCC) | ARMCC / MDK | IAR |
|---|---|---|---|
| RAM 最高地址 | 在链接脚本定义,如 _estack |
Image$$RAM$$ZI$$Limit |
CSTACK$$Limit 等 |
| .bss 结束 | __bss_end__ |
Image$$RW_IRAM1$$ZI$$Limit |
BSS$$Base + 大小 |
| 堆结束 | __heap_end |
Heap$$Limit |
HEAP$$Base + 大小 |
| 栈起始 | __stack_start__ |
STACK$$Base |
CSTACK$$Base |
实战步骤
-
打开 Map,搜索 RAM 末尾符号(如
_estack = 0x20010000),确认芯片 RAM 终点。 -
找出所有已分配段的最大结束地址(通常是堆结束或栈起始,取决于链接脚本)。
-
差值 = 剩余静态 RAM。
用途
-
这个剩余 RAM 最终会被 堆 或 栈 动态使用(或者闲置)。
-
如果静态剩余 RAM 已经是负数 → 链接阶段就溢出,程序无法启动,必须立即修改链接脚本或裁剪代码。
4.2 动态剩余 RAM(Map 不能提供)
堆剩余
-
必须调用运行时函数:
-
malloc_stats() -
xPortGetFreeHeapSize()(FreeRTOS) -
rt_memory_info()(RT-Thread)
-
栈剩余
-
必须用"水线"监测:
-
裸机:预先填充
0xCC,运行时从栈底向上找第一个非0xCC的字节。 -
RTOS:
uxTaskGetStackHighWaterMark()。
-
碎片率
-
堆剩余空间大不等于能分配到大块内存。
-
需要调用堆调试函数(如
malloc_dump())查看最大连续空闲块。
一句话总结
Map 只给你看"地皮规划图",不告诉你"现在每栋楼住了多少人"。
-
看静态剩余 → 打开 Map 算一算。
-
看动态剩余 → 插桩、API、调试器。
附:Map 文件速查索引表(实战专用)
| 你想知道什么 | 在 Map 里搜什么 | 在哪个区块找 |
|---|---|---|
| 代码总共占多少 Flash | .text + .rodata + .data 的 load address |
Section Cross Table |
| 全局变量占多少 RAM | .data + .bss |
Section Cross Table |
| 这个全局变量在哪个地址 | 变量名(全字匹配) | Global Symbols |
| 裸机栈总大小 | STACK、CSTACK、.stack |
Section Table |
| 裸机栈顶(SP 初始值) | __initial_sp、CSTACK$$Limit |
Symbols |
| 堆起始地址 | __heap_base、Heap$$Base |
Symbols |
| 堆结束地址 | __heap_end、Heap$$Limit |
Symbols |
| RAM 还剩多少静态空间 | RAM 终点符号(如 _estack) - 已分配区域最大地址 |
自行计算 |
| 中断向量表位置 | __Vectors |
Symbols |
| 程序入口 | __main、_start、Reset_Handler |
Symbols |
| RTOS 静态任务栈地址 | 你定义的栈数组名 | Global Symbols |
Lst表重要信息、ARM基础知识。有空再更