故障分析

更新时间:2023-09-06 10:40:14下载pdf

故障分析 是 TuyaOS 提供的通用平台 Core Dump 分析工具,用于捕捉程序发生 段错误 时的栈信息,通过栈回溯定位到异常代码。

该工具仅适用于涂鸦导入的芯片平台,其他芯片平台未验证。建议在使用之前先模拟段错误验证能否工作,如不适用于您的芯片平台,请用该芯片平台的调试工具代替。

本文介绍了如何使用 TuyaOS 网关开发框架的故障分析功能。

背景信息

产品在使用的过程中,有时会遇到由于程序出现段错误而导致重新启动,如果是业务逻辑中出现段错误,则可能导致该业务功能无法使用。这种异常往往不是必现的,大部分情况是在产品使用过程中偶然遇到的。所以,在测试阶段可能没被发现,上线使用后却越来越多地暴露问题。

如果设备运行的是 Linux 操作系统,可以通过开启 Core Dump 功能。程序发生异常时,内核会产生 .core 文件,通过 gdb 工具可以追溯函数的调用栈,从而定位到异常代码。但是 .core 文件很耗存储空间,大部分工作中的 IoT 产品默认都关闭该功能,仅在开发调试阶段打开。

为了解决以上问题,TuyaOS 提供了轻量级的 Core Dump 功能,适用于工作中的 IoT 设备出现程序异常的场景。

实现原理

SDK 通过接管异常信号,当程序发生异常时,SDK 将栈的信息保存到文件中,文件只占用几 KB 的存储空间。通过 Core Dump 工具进行栈回溯分析,通过查看栈顶的函数以及栈的函数调用可以追溯到异常代码所在的函数。

设备上运行的程序可以是不带符号链接表的,但在编译程序的时候,要编译一个带符号链接表(编译选项加上 -g)的调试程序做为备份,Core Dump 工具进行栈回溯分析时需要用到该文件。

开发指导

在网关初始化之后,您需要调用 tuya_gw_app_debug_start 接口开启故障分析功能,主要是收集程序异常时的栈信息。

在实现 日志管理 中的设备本地日志功能时,需要把保存栈信息的文件也打包到日志包中,这样才能在程序异常时获取到设备异常的栈信息文件。

使用示例:

int main(int argc, char **argv)
{
    OPERATE_RET rt = OPRT_OK;

    // TuyaOS
    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;
}

当程序出现段错误异常时,把保存的栈信息文件和编译时加上调试信息的程序(文件的命名要跟设备运行的程序一致)拷贝到 Core Dump 工具的同级目录下,执行 Core Dump 工具进行栈回溯分析。

Core Dump 工具的用法:

python3 coredump.py -d <dump文件>

源代码:

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()

解析示例:

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 函数中,再结合日志的上下文定位可能发生段错误的代码。