Page 1 of 1

【分享】程序段错误分析

Posted: 2023年 Dec 1日 18:46
by Kyson

在日常的软件开发过程中,我们无可避免地会遇到程序崩溃的问题。在开发和调试阶段,我们可以利用如 GDBGDB 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 函数中。