蔚淦丞
发布于 2026-11-05 / 1 阅读
0
0

系统异常调试定位方法体系:从观测到根因的完整路径

系统异常调试定位方法体系:从观测到根因的完整路径

本文融合了"内存管理调试"、MP-learn 系列(内存泄露/越界/重复释放/调试方法二)、/proc/meminfo 解读、top 命令操作速查、GDB Debug Info 学习笔记共 7 篇文章的全部内容,按照"问题分类 → 系统级观测 → 进程级定位 → 代码级根因 → 防御性编程"的自顶向下逻辑重新组织。所有原始内容均完整保留,未做任何删减。


一、内存问题常见的类别

  1. 内存泄漏——内存malloc后未free(或者是有缓存未实际释放)
  2. 内存越界
  3. 内存重复释放
  4. 内存释放后还在使用

二、系统级观测:确定问题层面

2.1 Linux 系统层面:/proc/meminfo

可以通过/proc/meminfo来查看系统内存使用情况如下,详细说明见:/proc/meminfo解读

[root@iZ2vc9tzeqqfy8lzwtvn1aZ ~]# cat /proc/meminfo
MemTotal:        3649004 kB
MemFree:          720336 kB
MemAvailable:    1878912 kB
Buffers:          209796 kB
Cached:           907400 kB
SwapCached:        14988 kB
Active:           695344 kB
Inactive:        1745148 kB
Active(anon):      49308 kB
Inactive(anon):  1292364 kB
Active(file):     646036 kB
Inactive(file):   452784 kB
Unevictable:           4 kB
Mlocked:               4 kB
SwapTotal:       2097148 kB
SwapFree:        2008684 kB
Dirty:              1568 kB
Writeback:             0 kB
AnonPages:       1315248 kB
Mapped:          1130364 kB
Shmem:             18376 kB
KReclaimable:     347364 kB
Slab:             412504 kB
SReclaimable:     347364 kB
SUnreclaim:        65140 kB
KernelStack:        5616 kB
PageTables:        23320 kB
NFS_Unstable:          0 kB
Bounce:                0 kB
WritebackTmp:          0 kB
CommitLimit:     3921648 kB
Committed_AS:    2552812 kB
VmallocTotal:   34359738367 kB
VmallocUsed:       17076 kB
VmallocChunk:          0 kB
Percpu:             8576 kB
HardwareCorrupted:     0 kB
AnonHugePages:    655360 kB
ShmemHugePages:        0 kB
ShmemPmdMapped:        0 kB
FileHugePages:     63488 kB
FilePmdMapped:     55296 kB
CmaTotal:              0 kB
CmaFree:               0 kB
DupText:               0 kB
MemZeroed:             0 kB
Unaccepted:            0 kB
HugePages_Total:       0
HugePages_Free:        0
HugePages_Rsvd:        0
HugePages_Surp:        0
Hugepagesize:       2048 kB
Hugetlb:               0 kB
DirectMap4k:      384284 kB
DirectMap2M:     3643392 kB
DirectMap1G:           0 kB
[root@iZ2vc9tzeqqfy8lzwtvn1aZ ~]#

AnanPages、Mapped的和大致等于用户态消耗的内存:

AnonPages:       1315248 kB
Mapped:          1130364 kB

Slab是内核小内存的消耗:

Slab:             412504 kB

如果这两项都没有明显的增长,说明是内核buddy处泄露

决策流程图

MemAvailable 持续下降?
  ├─ 否 → 无泄漏
  └─ 是 → drop_caches 后是否恢复?
           ├─ 是 → 缓存行为,非泄漏
           └─ 否 → AnonPages+Mapped 是否同步增长?
                    ├─ 是 → ✅ 用户态泄漏(查进程 RSS)
                    └─ 否 → SUnreclaim 是否同步增长?
                             ├─ 是 → ✅ 内核 Slab 泄漏(查 /proc/slabinfo)
                             └─ 否 → 计算残差是否持续增长?
                                      ├─ 是 → ✅ 内核 Buddy 泄漏(查 alloc_pages 调用点)
                                      └─ 否 → 检查 HardwareCorrupted / CmaUsed 等罕见字段

2.2 top 命令:找到谁在消耗资源

top 命令常用操作速查

以下示例均来自aliyun服务器真实输出,可对照实操验证。


1. 基础用法

top            # 交互模式,每3秒刷新
top -b -n 1    # 批处理模式,只输出一次(适合脚本/笔记采集)
头部信息解读(本机实际输出)
top - 14:29:26 up 14 days,  7:16,  1 user,  load average: 1.74, 1.42, 1.34
Tasks: 138 total,   2 running, 136 sleeping,   0 stopped,   0 zombie
%Cpu(s): 71.0 us,  9.7 sy,  0.0 ni, 19.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
MiB Mem :   3563.5 total,    717.8 free,   2368.1 used,    723.5 buff/cache
MiB Swap:   2048.0 total,   1971.0 free,     77.0 used.   1195.4 avail Mem
字段 含义
load average 1/5/15分钟平均负载,超过CPU核数需关注
us/sy/id/wa 用户态/内核态/空闲/IO等待 CPU占比
buff/cache 可回收缓存,不算真正占用
avail Mem 应用程序实际可用内存(比free更准确)

2. 排序(最常用)

命令行指定排序字段
top -b -n 1 -o %CPU    # 按CPU使用率降序
top -b -n 1 -o %MEM    # 按内存使用率降序
top -b -n 1 -o RES     # 按物理内存占用降序
top -b -n 1 -o TIME+   # 按累计运行时间降序
交互模式下切换排序

在 top 运行中按以下键:

  • P — 按 %CPU 排序
  • M — 按 %MEM 排序
  • T — 按 TIME+ 排序
  • N — 按 PID 排序
本机按内存排序实际输出
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
2124184 root      20   0 3295344 848196  29580 S   0.0  23.2   4:30.94 java
 1240 admin     20   0   21.8g 658152  36228 S   0.0  18.0  12:36.73 openclaw-gatewa
  466 root      20   0  289724 177156 170096 S   0.0   4.9   6:31.16 systemd-journal
 3028 root      20   0  657752  97700      0 S   0.0   2.7   0:01.15 searxng worker-
  938 root      20   0 2416184  66400  27568 S   0.0   1.8  16:03.09 dockerd

💡 java 进程占 23.2% 内存(848MB),是当前最大内存消费者。


3. 按线程显示

top -H              # 交互模式,显示所有线程
top -b -n 1 -H      # 批处理模式
本机线程模式实际输出
Threads: 355 total,   8 running, 347 sleeping,   0 stopped,   0 zombie
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
2584260 root      20   0 1224028 229256  56244 R  68.8   6.3   0:01.66 node
2584262 root      20   0 1224028 229256  56244 S   6.2   6.3   0:00.15 node
2584263 root      20   0 1224028 229256  56244 R   6.2   6.3   0:00.14 node
2584264 root      20   0 1224028 229256  56244 S   6.2   6.3   0:00.15 node

💡 注意头部从 Tasks 变为 Threads,同一进程的多个线程共享 VIRT/RES,但 %CPU 各自独立。

交互模式中切换
  • H — 切换 进程/线程 视图

4. 只看指定进程

top -p <PID>             # 只看单个进程
top -p 1234,5678         # 看多个进程(逗号分隔)
top -b -n 1 -p 2124184   # 批处理模式
本机查看 java 进程实际输出
Tasks:   1 total,   0 running,   1 sleeping,   0 stopped,   0 zombie
    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
2124184 root      20   0 3295344 848196  29580 S   0.0  23.2   4:30.95 java

💡 Tasks 总数变为 1,只聚焦目标进程,排查问题时非常实用。

配合 pgrep 快速定位
top -p $(pgrep -d',' nginx)       # 查看所有nginx进程
top -H -p $(pgrep -x java)        # 查看java所有线程

5. 其他实用交互快捷键

按键 功能
k 向选中进程发送信号(默认SIGTERM)
r 调整进程优先级(renice)
f 选择显示的列
c 显示完整命令路径
1 展开每个CPU核心的使用情况
E 切换内存单位(KB/MB/GB)
q 退出

6. 常见排查场景速查

场景 命令
找出CPU最高的进程 top -b -n 1 -o %CPU | head -10
找出内存泄漏嫌疑进程 top -b -n 1 -o RES | head -10
某进程CPU飙高看线程 top -H -p <PID>
IO等待高确认瓶颈 看 %wa + iostat -x 1
僵尸进程检查 看头部 zombie 计数

三、进程级定位:MP 操作系统专用诊断工具

3.1 内存泄漏

分析和解决思路

采用分层定位法,明确泄露层级:

  • 系统层面初步判断泄露发生在:内核态 or 用户态 —— 大概率是用户态
  • 确定是用户态之后,泄露进程 --> 泄露函数 --> 问题代码段

MP 定位到进程和函数

当内存短缺出现时,会自动记录内存使用情况到/syslog/meminfoxxx

哪个进程在泄露

show diagnose memory top
一般来说mpos是占大头,如果aaa、ipv6之类的进程占大头,很明显就是这个异常的进程在泄露。
如果在mpos泄露,就需要进一步定位到是哪个模块的问题,因为mpos里面是有多个模块的。

哪个函数在泄露

操作系统里通过pub.so提供了内存申请和释放的接口,会替代glibc里的malloc和free,此外也提供了一些统计信息的工具。因此可以通过如下的命令很快的定位到内存泄漏的函数。

申请了1M以上的函数地址

show slab alloc exclude mega-bytes-se 1

申请了10000次以上的函数地址

show slab alloc exclude blocks-se 10000

根据函数地址查函数名

display symbol addr xxxxxxxx
补充:根据函数名查地址使用 display symbol sym xxxxxxxx

增量观测方式

记录当前状态

slab alloc statistics save

查看增量

show slab alloc increment summay

C库缓存确认方式

show diagnose memory anon-mem-info

其中 system bytes 是申请的内存,in use bytes 是使用中的内存,差值即为缓存

编程注意事项

  1. 谁分配,谁释放——从源头控制住
  2. 推动分配接口可追溯——尽量避免新增公共内存分配接口,如确需新增,需支持传入调用者信息的接口。因为不可能传入整个调用栈信息(太庞大了)
  3. 统一内存接口封装,提升可维护性与可观测性

3.2 内存越界

分析与解决思路

  1. 通过在内存块前后放置特定数据(红区)等管理信息来判断。
  2. 先区分内存越界者是主动free被检测到还是越界写了别人的内存(有时候发生问题的模块不一定是出问题的模块,可能是别人往他的内存写数据了)
  3. 关键是先找到问题内存块地址,才能通过gdb关注此内存块前后范围红区情况,查看前面或后面有红区的内存块函数信息,通过这个函数找到可能的问题模块
  4. 如果是free被内存管理检测到,则通过关键信息区分通用池和专用池,不同类别池子内存管理信息布局有差异

实际可用工具

detect

先进入configure terminal模式,然后配置dfm mem detect-sshd on 。该工具可以动态监测某个进程的内存的管理区是否正常,可以检测到越界但还没释放的内存。

efence

同样先进入configure terminal模式,然后配置dfm mem efence-sshd on 。此工具开销太大,但很精准,可以抛出越界的第一现场。其底层用的是mprotect接口。

小结

如何定位内存越界问题
  1. 主动free被内存管理检测到越界,根据打印信息判断,如果是前管理区不对,大概率是前一块内存写越界了,向前找。后管理区不对,大概率是当前输出信息的caller有问题
  2. 其他模块挂机或者功能不对找到被修改的内存块地址,向前或者向后多打印一些内存信息,更好定位问题模块。
    专用池

专用池问题内存块前后块挨着,但是通用池前后块可能隔很远

  1. 关键点一是定位到出问题的内存块地址,二是异常内存的值是否有特征,有特征可以通过gdb的find指令查询这个内存地址
编程注意事项
  1. 严格遵循内存编码规范

  2. 使用malloc等接口分配的内存写的时候不能超过分配的大小

  3. 注意数组的索引值的有效性检查,避免向后或向前越界

  4. 分配的内存在读写时,如果读写的size是变量,一定要检测有效范围

  5. 建议使用宏封装统一管理内存分配和释放接口,新写代码交付前建议使用头文件sdt_efence_pub.h中提供的接口进行检测,这里的接口在越界时会直接出错病输出越界的代码位置。(正式提交时要改回原来的接口)

3.3 重复释放与释放后使用

重复释放问题

通用池重复释放会直接报出double free 的caller,大概率就是这个caller。两种情况:

  1. 自己的指针释放两次
  2. 释放了别人的指针
    如果重复释放时,该内存已经被还给内核,则不会打印出明确的信息,那么需要结合代码逻辑以及其他信息来确定,比如:/proc//maps可以查看该进程是否还有报错的内存地址,如果没有则有可能是重复释放问题

检测机制:红区设置为比如abcdefgh,释放时先把红区取反,缓存时间过后,将内存毒化为6b6b6b6b
通过dfm mem free-use-processname 可以将通用池内存块释放后仍然缓存指定时间,便于增大概率检测出此类问题

释放仍在使用问题

通过dfm mem free-use-processname 可以将通用池内存块释放后仍然缓存指定时间,便于增大概率检测出此类问题,如果看到6b6b6b6b也要想是不是内存释放单仍在使用的问题。

3.4 信号量死锁、异常挂机、CPU 问题、线程栈问题

信号量死锁

信号量死锁是多个进程(或线程)在使用信号量进行同步时,因彼此等待对方释放信号量,导致所有相关进程都无法继续执行的状态

解决核心思路

  1. 找出线程1拥有的信号量和等待的信号量1
  2. 找出信号量1的拥有者线程2
  3. 找出线程2拥有的信号量和等待的信号量2
  4. 重复2、3,直到查清死锁人物间的等待关系

调试命令

# 1. semLib.h : 查看信号量使用情况
show semaphore ?
# 查看互斥锁使用情况,pended代表有挂起的线程,找到这个锁的owner,看看这个owner为什么不释放锁
show semaphore mutex pended summary
# trace上一步得到的owner
trace <owner>

# 2. gnl_sem_pub.h 查看互斥信号量使用情况
show diagnose gnl semaphore mutex pend
# 查看指定类型信号量使用情况
show diagnose gnl semaphore

编程注意事项

  1. 避免信号量保护范围过大

  2. 获取和释放要成对,不要遗漏释放

  3. 避免死锁参考:

  4. 主动切断外部模块持有信号量再获取自己信号量的可能

  5. 避免持有自己信号量再同步调用外部模块接口,从而持有外部信号量

  6. 参考百科

  7. 全新代码使用gnl_sem_pub.h提供的接口。

异常挂机

解决方式

应用程序不管启动多少次,函数地址都是固定的,但是动态库的函数地址是不固定的。如何找到动态库的调用函数?

  1. 根据打印出来动态库的地址(可能会变),和具体挂机点的调用函数的地址算出一个偏移
  2. 通过这个偏移,可以在有addr2line的环境下,用addr2line工具找到具体的调用点(行号)

addr2line工具

addr2line工具在x86操作系统可以直接用,如果是非x86系统,可以在/config/product_config里找到工具对应的路径

addr2line -e out/image/libso/cli/libmem-cli.so <偏移>

可字节定位到so行号
后续学习:gdb debug符号如何导入导出?

小结

  1. 注意挂机信号,不同的挂机信号有不同含义,如11信号是常见的异常地址访问。
  2. 确认挂机线程号后想办法进入linux shell,通过gdb调试问题线程
  3. 如果能够导入调试符号且设备内存够,则可导入gdb符号表
  4. 如果不能导入符号,结合编译环境分析挂机位置代码信息
  5. 动态库函数挂机如何定位代码行号?先获取偏移然后addr2line

CPU(死循环,调度问题)

关键思路

CPU占用高,可能死循环,系统会自动检测输出相关日志(也可以通过工具命令来查看具体线程占用CPU情况),遇到此类问题尽量多输出几次线程堆栈信息,因为堆栈输出过程漫长有滞后性,多输出才好定位
线程得不到CPU调度?确认是否线程在等待资源(信号量等);如果确认不是等待资源,查看指定线程的调度统计信息确认;
总线程被调度的少?梳理下业务模块框架,是否锁过多,或者锁的使用不合理,导致等锁时间多?因为拿锁和放锁都涉及到CPU的重新调度。

调试命令

MPOS命令
spy cpu
show cpu
linux命令
top

top命令常见操作见下方链接:

https://ypp123.top/archives/5G7WHrnp
也可以查看/proc目录下的进程、线程状态,详见下方链接:

https://ypp123.top/archives/PNzpc9t0

小结

  1. 线程CPU占用高问题,系统基础会自动检测并输出日志,此外也可在mpos上使用以下命令来追踪:

  2. spy cpu后执行show cpu —— 查看当前系统各线程CPU占用情况

  3. show cpu monitor —— 查看系统CPU整体随着时间占用情况

  4. router#trace pthreadname —— 显示问题线程堆栈信息

  5. 线程没有被调度,可以通过内核调度统计信息确认,详见前文提到的《proc文件系统》

  6. 线程调度慢或不正常?硬中断是否异常、软中断是否异常、实时线程是否异常、其他线程CPU占用是否异常等逐一确认。

  7. sysrq TASK-CPU-INFO(a)可以显示CPU占用异常线程情况,在MP shell和linux shell都不能使用时可以尝试

线程栈(溢出,栈破坏)

关键思路

线程栈异常一般会触发异常堆栈信息

  1. 先确认是否栈溢出问题,有明确的提示信息:Detect stack overflow;
  2. 其他栈问题收集sp指针指向的栈信息,结合代码、AI等进一步分析
  3. gdb中 info registers 找到栈顶指针
  4. 然后x/1280x 栈顶指针,尽量多收集附近的信息

小结

  1. 先看是否有Detect stack overflow,
    有时候即使栈溢出,栈使用占比也不会显示占用100%,最根本还是要看 /proc/xxx/maps 看异常地址在哪个区域,是否在正常栈范围。
  2. 其他栈异常充分收集栈相关寄存器sp指向的信息,结合代码、AI等分析。栈异常大概率栈信息显示??,无法解析函数符号,无法显示完整堆栈信息

其他工具和手段

工作线程job信息

主要命令:show diagnose util workthread xxx

文件fd占用信息

主要命令:show diagnose util kernelfd xxx

日志

主要命令:collect diagnose sysinfo xxx


四、代码级根因:从地址回到源码

4.1 GDB Debug Info:符号与源码的桥梁

Linux C GDB Debug Info 学习笔记

1. Debug 信息的设置与有无对比

编译选项
选项 说明
-g / -g2 生成标准 DWARF debug 信息(推荐)
-g1 仅生成最小调试信息(函数名、行号)
-g3 包含宏定义等额外信息
-O0 关闭优化,保证源码与指令一一对应
-O2/-O3 开启优化,可能导致调试时变量被优化掉
验证命令
gcc -g -O0 demo.c -o demo_debug    # 带 debug 信息
gcc demo.c -o demo_release          # 不带 debug 信息
readelf -S demo_debug | grep debug  # 查看 .debug_* 段

2. 为什么要分离 Debug 信息

  • 减小发布体积:debug 信息通常占二进制 50%~80%
  • 保护源码:避免源码路径、变量名泄露
  • 按需加载:GDB 仅在需要时加载,减少内存占用
  • 独立更新:代码小改无需重新分发完整 debug 包

3. 分离 Debug 信息的方法

使用 objcopy
# 1. 提取 debug 信息到独立文件
objcopy --only-keep-debug demo demo.debug

# 2. 从原文件中剥离 debug 段
objcopy --strip-debug demo demo_stripped

# 3. 添加关联链接(让 GDB 自动找到 debug 文件)
objcopy --add-gnu-debuglink=demo.debug demo_stripped
验证关联
readelf -x .gnu_debuglink demo_stripped
# 输出包含 debug 文件名 + CRC32 校验值

4. GDB 如何使用分离的 Debug 信息

自动查找顺序
  1. 同目录下 demo_stripped.debug
  2. .debug/ 子目录
  3. /usr/lib/debug/ 系统目录
  4. build-id 匹配(.build-id/xx/yyyy.debug)
手动指定
# 方式1:启动后加载
(gdb) symbol-file demo.debug

# 方式2:设置搜索目录
(gdb) set debug-file-directory /path/to/debug/files

# 方式3:启动时指定
gdb -ex "set debug-file-directory ./debug" demo_stripped
验证加载成功
(gdb) info sources        # 应显示源码文件列表
(gdb) list main           # 能正常显示源码
(gdb) print variable      # 能查看变量值

5. 最佳实践总结

场景 建议
开发阶段 gcc -g3 -O0 全量调试
测试/预发 gcc -g2 -O1 平衡性能与调试
生产发布 分离 debug + strip,保留 .gnu_debuglink
崩溃分析 确保 debug 文件版本与二进制严格匹配
CI/CD 自动归档 debug 文件到符号服务器

⚠️ 关键提醒:debug 文件必须与对应二进制同一次编译产物,否则 CRC 校验失败或符号错位

4.2 动态库崩溃定位:addr2line

应用程序不管启动多少次,函数地址都是固定的,但是动态库的函数地址是不固定的。如何找到动态库的调用函数?

  • 根据打印出来动态库的地址(可能会变),和具体挂机点的调用函数的地址算出一个偏移
  • 通过这个偏移,可以在有addr2line的环境下,用addr2line工具找到具体的调用点(行号)
addr2line -e out/image/libso/cli/libmem-cli.so <偏移>

可直接定位到so行号。

addr2line工具在x86操作系统可以直接用,如果是非x86系统,可以在/config/product_config里找到工具对应的路径。

4.3 无 core dump 时的寄存器现场分析

当只有 oops 日志中的寄存器和栈信息时,按以下优先级分析:

第一步:ERR + CR2 定性错误类型

ERR 值 CR2 特征 结论
0x04 ≈ 0x0 空指针读
0x05 ≈ 0x0 空指针写
0x07 合法地址 写只读/COW 失败
0x15 数据段地址 NX 违规(执行了非代码内存)
0x06 合法地址 读权限违规(ASan redzone / mprotect)

第二步:RIP 定位源码

addr2line -e /path/to/binary -f -C <RIP地址>

第三步:GPR 还原参数(x86_64 ABI)

寄存器 角色 崩溃时检查要点
RDI arg1 是否为 NULL / 野指针
RSI arg2 同上;若为长度值,是否异常大
RDX arg3 计数器/偏移量,是否溢出
RAX 返回值 前一调用返回 NULL/错误码后未检查
RBP 帧指针 若为 0 或非栈地址 → 栈已损坏

💡 核心原则:先定性(ERR+CR2)再定位(RIP+addr2line),用固定 ABI 约定弥补缺失的动态内存视图。


五、防御性编程:让 bug 不再发生

5.1 内存安全

  1. 谁分配,谁释放——从源头控制
  2. 推动分配接口可追溯——避免新增公共内存分配接口,如确需新增,需支持传入调用者信息
  3. 统一内存接口封装——提升可维护性与可观测性
  4. 严格边界检查——malloc 写入不超过分配大小;数组索引有效性校验;size 为变量时必须检测范围
  5. 交付前用 efence 检测——使用 sdt_efence_pub.h 提供的接口,正式提交时改回原接口

5.2 并发安全

  1. 避免信号量保护范围过大
  2. 获取和释放成对,不要遗漏
  3. 避免死锁:切断外部模块持锁再获取自己锁的可能;避免持自己锁再同步调用外部模块接口
  4. 全新代码使用 gnl_sem_pub.h 接口

5.3 可调试性设计

  1. 生产构建保留符号关联:strip + .gnu_debuglink,确保崩溃时可回溯
  2. CI/CD 自动归档 debug 文件到符号服务器
  3. 开启 ASan 做 CI 门禁:-fsanitize=address 在测试阶段拦截内存错误
  4. 日志分级与结构化:关键操作留痕,便于事后关联分析

评论