【分享】程序段错误分析
在日常的软件开发过程中,我们无可避免地会遇到程序崩溃的问题。在开发和调试阶段,我们可以利用如 GDB
或 GDB Server
这样的工具来重现问题并进行栈回溯,从而轻松定位并解决问题。
然而,对于在线产品中的偶发性崩溃问题,处理起来就显得较为困难。尽管我们可以通过 Linux 的 backtrace 功能,捕捉 SIGSEGV 信号,并在信号回调中调用 backtrace 和 backtrace_symbols 接口来进行栈回溯,但遗憾的是,许多工具链已经裁剪了这一功能,因此在大多数情况下,backtrace 功能无法使用。
本文将提供两种方法,以解决线上产品中偶尔出现的 crash 问题。
Core Dump
核心转储(Core Dump)是指在程序异常终止时,将程序在内存中的数据和状态以文件的形式保存下来,它包含了程序崩溃时的内存快照,可用于后续调试和分析。
开启 Core Dump
通常情况下,Linux 系统是把 coredump 关闭的,需要我们手动打开。
开启 coredump 功能,并且调整 coredump 文件的大小为无限制:
Code: Select all
$ ulimit -c
$ ulimit -c unlimited
注意,我们可以更加硬件资源调整 coredump 文件的大小限制。
默认情况下,coredump 文件的存放路径为当前目录,我们可以修改 coredump 的存放路径:
Code: Select all
echo "/data/core_%e_%p" > /proc/sys/kernel/core_pattern
其中 %e,%p 的解释如下:
Code: Select all
%p - insert pid into filename 添加pid
%u - insert current uid into filename 添加当前uid
%g - insert current gid into filename 添加当前gid
%s - insert signal that caused the coredump into the filename 添加导致产生core的信号
%t - insert UNIX time that the coredump occurred into filename 添加core文件生成时的unix时间
%h - insert hostname where the coredump happened into filename 添加主机名
%e - insert coredumping executable name into filename 添加命令名
分析 Core Dump
一旦 coredump 文件生成,我们就可以用 gdb
工具来分析。
Code: Select all
$ arm-linux-gnueabihf-gdb /path/to/executable /path/to/coredump
注意,程序编译时需要添加 -g
选项,保留符号表。
在 gdb
中,可以使用 bt
命令查看堆栈回溯信息,确定程序崩溃的位置。
Code: Select all
(gdb) bt
Crash Dump
Crash Dump 是 TuyaOS 网关开发框架提供的轻量级的 Core Dump 功能,可以作为 Core Dump 的代替方案。
实现原理
TuyaOS 网关开发框架通过捕捉 SIGSEGV 信号,在信号回调中把当前的栈内存对齐后保存到文件中,该文件仅占几 KB 的存储空间。
栈回溯就是遍历所有地址,先检查该地址是否在程序运行时的地址空间,在运行时的地址空间则通过 addr2line
工具把地址转成文件和函数名。
如何使用
在网关初始化之后,调用 tuya_gw_app_debug_start
接口开启 Crash Dump 功能。示例:
Code: Select all
int main(int argc, char **argv)
{
OPERATE_RET rt = OPRT_OK;
TUYA_CALL_ERR_RETURN(tuya_iot_init("./"));
TUYA_CALL_ERR_RETURN(tuya_iot_set_gw_prod_info(&prod_info));
TUYA_CALL_ERR_RETURN(tuya_iot_sdk_pre_init(TRUE));
TUYA_CALL_ERR_RETURN(tuya_iot_wr_wf_sdk_init(IOT_GW_NET_WIRED_WIFI, GWCM_OLD, WF_START_AP_ONLY, M_PID, M_SW_VERSION, NULL, 0));
TUYA_CALL_ERR_RETURN(tuya_iot_sdk_start());
tuya_gw_app_debug_start("./log_dir/");
while (1) {
tuya_hal_system_sleep(10*1000);
}
return OPRT_OK;
}
如何解析
当程序出现段错误时,把保存的栈信息文件和编译时加上 -g
选项的程序放到同一路径下,接着,新建一个名为 coredump.py
文件,把以下的脚本内容拷贝到文件中。最后,执行命令进行解析:
Code: Select all
python3 coredump.py -d <dump文件>
注意,上面的程序名称要与设备运行的程序保持一致。
coredump.py
脚本:
Code: Select all
import argparse
import os
parser = argparse.ArgumentParser(description='SDK Coredump Analyzer')
parser.add_argument(
'-d', '--dump_file', required=True, type=str, help='crash dump file')
args = parser.parse_args()
sys_so = ["libc.so", "libc-", "libpthread-", "libpthread.so", "ld-", "ld.so", "stdc++", "uClibc", "libgcc"]
'''
crash dump file format:
stack dump:
00000c00 00000001 7fd10000 00000001
stack dump End
dump text section
00400000-00897000 r-xp 00000000 00:08 237597 /var/tmp/tyZ3Gw
'''
def parse_dump_file(filename):
is_stack = False
is_text = False
stack = []
text = {}
if not os.path.isfile(filename):
return stack, text
with open(filename, 'r') as f:
for line in f:
if line.find("stack dump:") != -1:
is_stack = True
continue
if line.find("stack dump End") != -1:
is_stack = False
continue
if line.find("dump text section") != -1:
is_text = True
if is_stack:
stack.extend(line.split())
if is_text and line.find("r-xp") != -1:
text_content = line.split()
if len(text_content) != 6:
print("parse text section error")
continue
addr = text_content[0]
path = text_content[-1]
filename = os.path.basename(path)
# Filter system so
is_omit = False
for so_name in sys_so:
if filename.find(so_name) != -1:
is_omit = True
break
if is_omit:
continue
addr_range = addr.split('-')
if len(addr_range) != 2:
continue
text[filename] = addr_range
return stack, text
def dump_addr2line(stack, text):
for addr in stack:
addr = int(addr, 16)
for name in text:
addr_start = int(text[name][0], 16)
addr_end = int(text[name][1], 16)
if addr >= addr_start and addr <= addr_end:
# Shared object need to offset
if name.find(".so") != -1:
addr = addr - addr_start
addr = str(hex(addr))
if not os.path.exists(name):
print("{} is not found".format(name))
break
os.system('addr2line {} -e {} -f'.format(addr, name))
break
def main():
dump_file = args.dump_file
print("crash dump file: {}".format(dump_file))
stack, text = parse_dump_file(dump_file)
dump_addr2line(stack, text)
if __name__ == '__main__':
main()
示例
Code: Select all
kyson@LAPTOP-ORFJBPHU:~/workspace/tuya/tools/crash_dump$ python3 coredump.py -d 959_user_iot_1645100484
crash dump file: 959_user_iot_1645100484
__start
??:?
sig_proc
/root/workspace_temp/EmbedSDKs/ty_gw_zigbee_ext_sdk/ty_gw_zigbee_ext_sdk/sdk/svc_linux_crash_dump/src/crash_dump.c:287
??
??:0
emberAfSendDefaultResponseWithCallback
/root/workspace_temp/EmbedSDKs/ty_gw_zigbee_ext_sdk/ty_gw_zigbee_ext_sdk/sdk/zigbee_host/slabs/v2.2/protocol/zigbee/app/framework/util/util.c:764
__start
??:?
...
栈回溯分析会把程序异常时入栈的函数都打印出来,我们主要看栈顶的函数。从以上的输出可以看出,程序段错误发生在 emberAfSendDefaultResponseWithCallback
函数中。