coredump问题定位方法介绍
core dump又叫核心转储, 当程序运行过程中发生异常, 程序异常退出时, 由操作系统把程序当前的内存状况存储在一个core文件中, 叫core dump。一般对于带GC、不直接操作内存的语言不太会遇到coredump问题,往往出问题是因为C/C++代码业务逻辑异常导致触发到内存越界、非法指针等情况导致。
问题产生
触发coredump的方式各不相同,可能访问非法内存等;进程退出信号也不一样,常见的有SIGABRT、SIGSEGV等
常见的Exception Type异常类型的信息:
1、SIGIPIPE: 管道另一端没有进程接手数据 2、SIGSEGV: 代表无效内存地址, 比如空指针, 未初始化指针, 栈溢出等 3、SIGABRT: 收到Abort信号退出, 通常代码为了保护状态正常会做一些检测, 例如插入nil到数据中、double free、malloc过大内存等会遇到此类错误 4、SIGBUS: 总栈错误, 与SIGSEGV不同的是, SIGSEGV访问的是无效的地址, 而SIGBUS访问的是有效的地址, 但是总栈访问异常(如地址对齐问题) 5、SIGILL: 尝试执行非法的指令, 可能不被识别或者没有权限 6、SIGFPE: 数学计算相关问题, 比如除零操作
对coredump问题,在C/C++代码编写时,一定要注意内存分配与释放、全局变量访问互斥等问题。
coredump相关配置
coredump文件转储
Linux 默认没有打开core文件生成功能,也就是发生段错误时不会core dumped。可以通过以下命令打开core文件的生成:
# 查看各项limit配置
ulimit -a
# 不限制产生 core 的大小
ulimit -c unlimitedunlimited 意思是系统不限制core文件的大小,只要有足够的磁盘空间,会转存程序所占用的全部内存,如果需要限制系统产生 core 的大小,可以使用以下命令:
# core 最大限制大小为 409600 字节
ulimit -c 409600把核心转储功能关闭,只需要将限制大小设为0 即可:
ulimit -c 0注意,如果只是输入命令“ulimit -c unlimited”,这只会在当前终端有效,退出终端或者打开一个新的终端时是无效的。因此可以在将上述配置加入到 /etc/profile 中:
# 编辑 profile 文件
vi /etc/profile
# 将下行加到入profile 文件中
ulimit -c unlimited由于OPENUBMC业务使用systemd进行进程管理,需要在systemd的配置中将coredump配置添加上才行,在systemd的service配置中,增加LimitCORE、LimitSTACK配置即可,大小可以配置为infinity、100000000(100MB),例如安全子系统配置security.service等:
[Unit]
Description=security service
After=key_mgmt.service
Requires=dbus.service key_mgmt.service
[Service]
User=root
Restart=always
RestartSec=2
StartLimitInterval=0
EnvironmentFile=/dev/shm/dbus/.dbus
Environment="ROOT_DIR="
Environment="PROJECT_DIR="
WorkingDirectory=/opt/bmc/apps/hica
ExecStartPre=/bin/sleep 1.5
ExecStart=bash -c 'exec -a security /opt/bmc/skynet/skynet /opt/bmc/apps/hica/subsys/security/config.cfg'
# 修改coredump转储信息
ExecStartPost=/bin/bash -c 'echo 0x21 > /proc/$MAINPID/coredump_filter'
KillMode=process
MemoryHigh=400M
MemoryMax=512M
# coredump配置,转储最大100MB
LimitCORE=100000000
# coredump配置,转储最大100MB
LimitSTACK=100000000
[Install]
WantedBy=multi-user.target由于RTOS默认dump过滤只有栈数据,未包含堆数据、BSS等,导致OPENUBMC很多coredump文件大小仅为400k,如果需要增加数据,则需要改写内核参数 才行(proc/[pid]/coredump_filter)
生成Coredump相关配置
| 配置选项 | 配置效果 |
|---|---|
| /proc/sys/kernel/core_uses_pid | 如果设置为1,那么即使core_pattern中没有设置%p,最后生成的core dump文件名仍会加上进程ID。 |
| /proc/sys/kernel/core_pattern | 设置格式化的core文件保存位置以及文件名。 (1)原来文件内容是core-%e ,表示在可执行文件当前目录下生成“core-命令名”规则的coredump文件。 (2)使用"/corefile/core-%e-%p-%t"将会控制所产生的core文件会存放到/corefile目录下,产生的文件名为core-命令名-pid-时间戳。 具体参考下表描述。 |
| /proc/[pid]/coredump_filter_type | 0:使用open source方式生成coredump; 1:使用RTOS方式生成coredump |
| /proc/[pid]/coredump_filter | 参考coredump_type,open source方式和RTOS方式不同。 |
core_pattern规则:
1%% 单个%字符
2%p 所dump进程的进程ID
3%u 所dump进程的实际用户ID
4%g 所dump进程的实际组ID
5%s 导致本次core dump的信号
6%t core dump的时间 (由1970年1月1日计起的秒数)
7%h 主机名
8%e 程序文件名coredump_filter规则:
| coredump_filter_type = 0 | coredump_filter_type = 1 | |
|---|---|---|
| Bit位 | 采用系统coredump过滤 | 采用rtos idump过滤 |
| 0 | ANON_PRIVATE 匿名私有映射 | Stack 用户空间的进程栈,包括所有的线程栈 |
| 1 | ANON_SHARED 匿名共享映射 | Bin Data 二进制可执行文件数据段,包括bss |
| 2 | MAPPED_PRIVATE file-backed 私有内存 | Bin Text 二进制可执行文件代码段 |
| 3 | MAPPED_SHARED file-backed共享内存 | Lib Data 调用库的数据段,包括bss |
| 4 | ELF_HEADERS ELF文件映射 | Lib Text 调用库的代码段 |
| 5 | HUGETLB_PRIVATE 私有的大页 | Heap 用户空间的进程堆,包括匿名映射 |
| 6 | HUGETLB_SHARED 共享的大页 | Map Data File 普通文件的映射,不包括匿名映射 |
| 7 | - | 预留 |
| 8 | - | ELF_HEADERS ELF文件映射 |
| 9 | - | AnonMapping 匿名私有映射和共享映射 |
opensource采用0x33的bit位收集coredump,而ROTS使用0x1的bit位收集coredump。
coredump的格式可以用sysctl查看:
# 查看当前core_pattern配置
sysctl -a |grep core_pattern
# 设置core文件转储模板
sysctl -w kernel.core_pattern=/data/var/coredump/core-%e-`cat /etc/version.json|cut -d '"' -f 12|awk '{gsub(" ","_",$0);print($0);}'`编译配置
在生产环境中,为了执行效率,开启了很多优化编译选项,在复现coredump问题时,可以执行以下选项,来利用gdb获取更多信息:
增加调试信息:gcc编译时,需要增加-g参数,才会增加调试信息,编译阶段会保留程序的函数名、符号名等信息
不删除符号表:程序编译完成后,为了减小文件大小,默认会使用strip来删除文件的符号表信息。在出包定位问题时,不要删除符号表
关闭编译器优化:-O选项提供了不同级别的编译器优化,可能会抛弃无用分支,内联函数等,一般使用-O2。建议调试时使用-O0(表示不优化)
关闭代码段和数据段的随机化:移除-fPIE -pie编译选项,Linux 平台通过 PIE 机制来负责代码段和数据段的随机化工作
系统配置
地址空间布局随机化(Address Space Layout Randomization)
ASLR 技术在 2005 年的 kernel 2.6.12 中被引入到Linux系统,它将进程的某些内存空间地址进行随机化来增大入侵者预测目的地址的难度,从而降低进程被成功入侵的风险。
ASLR有几个配置:
0:没有随机化。即关闭 ASLR。 1:保留的随机化。共享库、栈、mmap() 以及 VDSO 将被随机化。 2:完全的随机化。在 1 的基础上,通过 brk() 分配的内存空间也将被随机化。
通过内核参数来控制:
echo 0 > /proc/sys/kernel/randomize_va_space定位思路
一般出现coredump问题后,业务进程会异常退出。在打开coredump转储后,在转储文件夹(OPENUBMC存在在/data/var/coredump)中发现对应的coredump文件
必现问题定位
如果对coredump问题有明确的触发场景和复现手段,这种问题往往都是比较好定位的,除了通过增加日志,可以直接使用gdb启动进程、gdb attach进程来定位。
gdb启动进程
目前各组件都是用systemd拉起的,我们如果需要用gdb启动组件,然后进行调试,则需要先将对应systemd的服务关闭,以安全子系统为例:
systemctl stop security
# 关闭后,查看security进程,会发现已经不再出现
ps aux|grep security此时,需要先准备好gdb程序、相关库文件,然后手动用gdb拉起对应进程,假设gdb相关文件都在/data/目录下
# 需要准备gdb二进制和libreadline.so.8文件
# 设置环境变量
export LD_LIBRARY_PATH=.:/lib:/data:$LD_LIBRARY_PATH
export PATH=$PATH:/data
# 为了和systemd配置一致,可以先进入对应工作目录
cd /opt/bmc/apps/hica
# 利用gdb启动skynet
/data/gdb_1711 /opt/bmc/skynet/skynet
# --- 下面是gdb的控制台 ---
# 首先设置配置文件路径,保证skynet能拉起安全子系统
(gdb) set args /opt/bmc/apps/hica/subsys/security/config.cfg
(gdb) rgdb attach到正在运行的进程
除了直接用gdb拉起进程,我们也可以将gdb attach到某个正在运行的进程上,一旦出现coredump信息,gdb也能及时中断程序并展示详细的调用栈信息
# 获取需要attach的进程号
ps aux|grep security
./gdb_1711 attach 42351
# --- 下面是gdb的控制台 ---
# 让进程继续执行
(gdb) cgdb基础操作
这里不详细介绍gdb的命令,只是简单列举下常用的一些命令(gdb只要首字母匹配到唯一命令即可):
info threads (简写:i th):查看当前进程所拥有的线程
backtrace/bt:打印当前线程的堆栈信息 加上full参数可以打印局部变量的值(简写bt f)
frame/f:显示或者切换栈帧
thread:切换线程
print:打印变量,可以打印局部变量、寄存器、常量、函数、表达式等
x:显示内存信息,格式:x /[Length] [Format] [Address expression]
其中,Length表示元素个数,Format表示数据格式,包括两部分:
第一部分是内存数据显示格式
- o - octal
- x - hexadecimal
- d - decimal
- u - unsigned decimal
- t - binary
- f - floating point
- a - address
- c - char
- s - string
- i - instruction
第二部分是数据按几位进行组合展示
- b - byte
- h - halfword (16-bit value)
- w - word (32-bit value)
- g - giant word (64-bit value)
x/40xb 0x0000ffffcd1a1af0:展示0x0000ffffcd1a1af0数据,每个byte按16进制单独显示,总计显示40个元素
x/40xg $sp:显示arm的sp寄存器
概率问题定位
针对一般的C/C++程序,coredump文件能比较清晰的给出相关的堆栈信息。但对于OPENUBMC的大部分业务来说,在skynet拉起服务后,大部分的执行代码都在lua中执行,出现coredump的调用链都是:
skynet主进程 -> lua业务服务脚本 -> c/c++库文件
出现coredump的进程例子:
经过lua虚拟机后,默认给出的堆栈信息往往都不太可靠,原因如下:
现代编译器经过优化代码之后, C 栈上已经没有 stack frame 的基地址了,所以现在不能简单的看堆栈的数据内容来推测 stack frame 。也就是经过优化的代码不一定适用 rbp 来保存 stack frame ,它也不一定入栈。对于 gcc ,这个优化策略是通过 -fomit-frame-pointer 开启的,只要用 -O 编译,就一定打开的。在 stack 本身出问题时,gdb 的猜测很可能不准确,人工来猜或手工补全或许更靠谱一些。方法就是先用 x/40xg $rsp (对arm来说是sp寄存器)打印出 C stack 的内容,然后观察确定 stack 上的哪些数据落在代码段上。
针对遇到的coredump问题,可以通过寻找复现场景、增加日志、使用asan分析这几种手段来进行。
寻找复现场景、增加日志复现
这一部分可以在出现coredump后,先观察coredump属于哪个进程,再结合操作日志、debug日志来观察进程崩溃的时间点,最后结合测试用例的执行情况、操作日志的操作,来分析出哪些场景容易出现coredump问题。
已9月份遇到的ManageMainServe线程崩溃问题来举例:
- 测试反馈遇到core文件,文件名为core-ManageMainServe。
经查看,core文件的信号为SIGABORT、进程为bmc_core(此时安全子系统和平台子系统为一个进程)
coredump文件的第二个参数为线程名,在代码中搜索,发现security_policy进程中,入侵检测管理进程的进程名为ManageMainServer,说明core在此线程中
- 结合日志分析(由于coredump不一定导致用例失败,可以查看core文件创建时间),发现在出现coredump问题是,执行的操作是添加、删除用户
分析业务场景,入侵检测会监控linux中的passwd、shadow文件改动,并打印日志,添加用户等操作正好触发此处的日志信息,怀疑在解析入侵检测消息时发生异常,需要在入侵检测模块增加日志,并复现问题
复现后,发现在特殊的报文中,json日志解析会异常,最后排查是因为json数据为NULL时,使用json-c中的数组操作直接获取元素时,会导致json数据断言失败并产生coredump
由于json-c的操作字符串,涉及的危险函数较多,针对相关函数,在使用前一定要做好判断,不能认为外部数据不会出问题而忽略掉异常分支。
ASAN(Address Sanitizer)
ASAN是一种地址错误检查器,此工具可以进程运行时检测出运行语句做了哪些操作,触发了内存、变量访问错误,并详细的告知错误的代码段,十分适合用来排查内存越界、泄漏等问题。
ASAN也有缺点:它能检测到被执行的代码,某些异常分支在运行时难以执行到,所以难以被ASAN检测,针对此问题,可以设计相关DT用例来触发异常场景,并开启检测功能来检测异常。
针对某些概率性问题,在出包复现时,也可以增加ASAN来检测内存异常,辅助定位。
出包指导
上述教程只是手动的开启ASAN并运行,也可以基于上述方法,在hpm包中,针对某些组件开启ASAN来检测问题(ASAN功能比较占内存,难以在所有组件开启并放到板上运行,希望qemu能解决此问题)
- (manifest仓)增加libasan相关的库文件到hpm包2. (hica仓)修改systemd相关环境变量,保证进程拉起时可以启用asan
出包后,针对此包复现即可
日志分析指导
出现内存问题后,asan会根据配置,将问题记录到日志中(一般是日志路径+进程号),查看日志描述即可分析内存异常问题
解析包含lua代码的coredump调用链
由于lua代码在运行时,使用的是lua虚拟机,内存中的堆栈信息其实是有将相关的逻辑dump出来的。使用gdb在attch进程时,能明显的看出整个调用链:
对应coredump文件,猜测其整个堆栈里也有相关的数据信息,可能是由于编译选项或者执行逻辑,导致gdb没有办法获取到真实的调用链信息
使用转储工具,收集coredump时内存地址
由于BMC系统开启了随机地址的相关配置,每次程序加载时的内存地址都不一样。为了在分析coredump文件时附带内存地址信息,需要在core时收集进程的相关信息。
linux系统提供了管道命令来自定义进程core时的执行动作,我们可用该管道配置进行收集数据
core文件基于管道来收集数据,只需要在收集数据前,将相关进行的/proc/{pid}/maps文件进行备份即可
编码规范
良好的编程习惯能防范大部分的编码问题,特别是C/C++这种需要操作内存的语言,希望大家共同学习,不出现coredump问题