当前位置: 首页 > news >正文

『网易实习』周记(五)

『网易实习』周记(五)

文章目录

  • 『网易实习』周记(五)
    • Crash监控
      • Crash的简单定义
      • Java Crash
      • Native Crash
        • Native Crash简介
        • so组成
        • Native Crash的发生
        • Native Crash捕获与解析

本周知识清单:

1.调研了解native crash的收集方式
2.整理appdump中的native crash及定位

Crash监控

Crash的简单定义

Crash(应用崩溃)是由于代码异常而导致 App 非正常退出,导致应用程序无法继续使用,所有工作都 停止的现象。发生 Crash 后需要重新启动应用(有些情况会自动重启),而且不管应用在开发阶段做得 多么优秀,也无法避免 Crash 发生,在 Android 应用中发生的 Crash 有两种类型,Java 层的 Crash 和 Native 层 Crash。这两种Crash 的监控和获取堆栈信息有所不同。

Java Crash

Java的Crash监控非常简单,Java中的Thread定义了一个接口: UncaughtExceptionHandler ;用于 处理未捕获的异常导致线程的终止(注意:被catch的异常是捕获不到的),当我们的应用crash的时候,就会走 UncaughtExceptionHandler.uncaughtException ,在该方法中可以获取到异常的信息,我们通 过 Thread.setDefaultUncaughtExceptionHandler 该方法来设置线程的默认异常处理器,我们可以 将异常信息保存到本地然后上传到服务器,方便我们快速的定位问题。

public class CrashHandler implements Thread.UncaughtExceptionHandler {
    private static final String FILE_NAME_SUFFIX = ".trace";
    private static Thread.UncaughtExceptionHandler defaultUncaughtExceptionHandler;
    private static Context context;
    
    
    public static void init(Context applicationContext) {
        context = applicationContext;
        defaultUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
        Thread.setDefaultUncaughtExceptionHandler(new CrashHandler());
    }
    
    @Override
    public void uncaughtException(@NonNull Thread t, @NonNull Throwable e) {
        try {
            File file = dealException(t, e);
            
        } catch (Exception exception) {
            
        } finally {
            if (defaultUncaughtExceptionHandler != null) {
                defaultUncaughtExceptionHandler.uncaughtException(t, e);
            }
        }
    }
    
    private File dealException(Thread thread, Throwable throwable) throws JSONException, IOException, PackageManager.NameNotFoundException {
        String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
        
        //私有目录,无需权限
        File f = new File(context.getExternalCacheDir().getAbsoluteFile(), "crash_info");
        if (!f.exists()) {
            f.mkdirs();
        }
        File crashFile = new File(f, time + FILE_NAME_SUFFIX);
        PrintWriter pw = new PrintWriter(new BufferedWriter(new FileWriter(crashFile)));
        pw.println(time);
        pw.println("Thread: " + thread.getName());
        pw.println(getPhoneInfo());
        throwable.printStackTrace(pw); //写入crash堆栈
        pw.flush();
        pw.close();
        return crashFile;
    }
    
    private String getPhoneInfo() throws PackageManager.NameNotFoundException {
        PackageManager pm = context.getPackageManager();
        PackageInfo pi = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_ACTIVITIES);
        StringBuilder sb = new StringBuilder();
        //App版本
        sb.append("App Version: ");
        sb.append(pi.versionName);
        sb.append("_");
        sb.append(pi.versionCode + "\n");
        
        //Android版本号
        sb.append("OS Version: ");
        sb.append(Build.VERSION.RELEASE);
        sb.append("_");
        sb.append(Build.VERSION.SDK_INT + "\n");
        
        //手机制造商
        sb.append("Vendor: ");
        sb.append(Build.MANUFACTURER + "\n");
        
        //手机型号
        sb.append("Model: ");
        sb.append(Build.MODEL + "\n");
        
        //CPU架构
        sb.append("CPU: ");
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            sb.append(Arrays.toString(Build.SUPPORTED_ABIS));
        } else {
            sb.append(Build.CPU_ABI);
        }
        return sb.toString();
    }
}

Native Crash

Native Crash简介

就是C或者C++运行过程中产生的错误,从Android系统全局来说Crash通常分为App Crash ,Native Crash,以及Kernel Crash
在这里插入图片描述

  • App Crash就是Java 层面的Crash,那么往往是通过抛出未捕获的异常所导致
  • Native Crash,就是C/C++层面的Crash,是在介于framework层和Linux层之间的一层,Native Crash发生后系统会在路径/data/tombstones 下产生一些导致Crash 的文件 tombstone_xx ,并且Google 的NDK包里面提供了一系列的调试工具,例如:addr2line,objdump,ndk-stack
  • Kernel Crash,是由于内核崩溃往往是驱动或者硬件故障出现故障

so组成

JNI是Java Native Interface的缩写,它的主要作用是提供了若干API来实现Java和其他语言的通信(主要是C和C++)。 NDK是一系列工具的集合,它可以帮助开发者快速开发C(或者C++)的动态库(也称So库),并So库和Java应用一起打包。开发Android应用时,有时候Java层的编码不能满足实现需求,就需要到C/C++实现后生成SO文件,再用System.loadLibrary()加载进行调用,这里成为JNI层的实现。常见的场景如:加解密算法,音视频编解码等。Android 开发中通常是将 Native 层代码打包为.so格式的动态库文件,然后供 Java 层调用,.so库文件通常有以下三种来源:

  • Android系统自带的核心组件和服务,如多媒体,OpenGL ES图形库
  • 引入的第三方库
  • 开发者自行编译生成的动态库

推荐阅读:简单认识Android SO 文件
在这里插入图片描述
一个完整的so库文件包含C/C++代码和Debug信息,这些 debug 信息会记录 .so中所有方法的对照表,就是方法名和其偏移地址的对应表,这就是**符号表,我们可以通过addr2line+so库+偏移地址查询得到报错的具体信息。**这种so库比较大。通常 release 的.so都是需要经过 strip 操作,strip 之后的.so中的 debug 信息会被剥离,整个 so 的体积也会缩小许多。这些 debug 信息尤为重要,是我们分析 Native Crash 问题的关键信息,那么我们在编译 .so 时 候务必保留一份未被 strip 的.so或者剥离后的符号表信息,以供后面问题分析。
Mac下可以使用**file**命令查看

file libbreakpad-core-s.so
libbreakpad-core-s.so: *******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, stripped
// 这个就是被裁减了
file libbreakpad-core.so
libbreakpad-core.so: ******, BuildID[sha1]=54ad86d708f4dc0926ad220b098d2a9e71da235a, with debug_info, not stripped
// 这个是没有被裁减

获取 strip 和未被 strip 的 so
目前 Android Studio 无论是使用 mk 或者 Cmake 编译的方式都会同时输出 strip 和未 strip 的 so,如下图是 Cmake 编译 so 产生的两个对应的 so。
在这里插入图片描述
在这里插入图片描述
strip 之前的 so 路径:{project}/app/build/intermediates/merged_native_libs
strip 之后的 so 路径:{project}/app/build/intermediates/stripped_native_libs

Native Crash的发生

与Java平台不同,C++没有通用的异常处理接口,在C层,CPU通过异常中断的方式,触发异常处理流程,不同的处理器,有不同的异常中断类型和中断处理方式,linux把这些中断处理,统称为信号量机制,每一种异常都有一个对应的信号,可以注册回调函数处理需要关注的的信号量,所有信号量都是定义在<signal.h> 文件中。如下:

#define SIGHUP 1  // 终端连接结束时发出(不管正常或非正常)
#define SIGINT 2  // 程序终止(例如Ctrl-C)
#define SIGQUIT 3 // 程序退出(Ctrl-\)
#define SIGILL 4 // 执行了非法指令,或者试图执行数据段,堆栈溢出
#define SIGTRAP 5 // 断点时产生,由debugger使用
#define SIGABRT 6 // 调用abort函数生成的信号,表示程序异常
#define SIGIOT 6 // 同上,更全,IO异常也会发出
#define SIGBUS 7 // 非法地址,包括内存地址对齐出错,比如访问一个4字节的整数, 但其地址不是4的倍数
#define SIGFPE 8 // 计算错误,比如除0、溢出
#define SIGKILL 9 // 强制结束程序,具有最高优先级,本信号不能被阻塞、处理和忽略
#define SIGUSR1 10 // 未使用,保留
#define SIGSEGV 11 // 非法内存操作,与SIGBUS不同,他是对合法地址的非法访问,比如访问没有读权限的内存,向没有写权限的地址写数据
#define SIGUSR2 12 // 未使用,保留
#define SIGPIPE 13 // 管道破裂,通常在进程间通信产生
#define SIGALRM 14 // 定时信号,
#define SIGTERM 15 // 结束程序,类似温和的SIGKILL,可被阻塞和处理。通常程序如果终止不了,才会尝试SIGKILL
#define SIGSTKFLT 16  // 协处理器堆栈错误
#define SIGCHLD 17 // 子进程结束时, 父进程会收到这个信号。
#define SIGCONT 18 // 让一个停止的进程继续执行
#define SIGSTOP 19 // 停止进程,本信号不能被阻塞,处理或忽略
#define SIGTSTP 20 // 停止进程,但该信号可以被处理和忽略
#define SIGTTIN 21 // 当后台作业要从用户终端读数据时, 该作业中的所有进程会收到SIGTTIN信号
#define SIGTTOU 22 // 类似于SIGTTIN, 但在写终端时收到
#define SIGURG 23 // 有紧急数据或out-of-band数据到达socket时产生
#define SIGXCPU 24 // 超过CPU时间资源限制时发出
#define SIGXFSZ 25 // 当进程企图扩大文件以至于超过文件大小资源限制
#define SIGVTALRM 26 // 虚拟时钟信号. 类似于SIGALRM, 但是计算的是该进程占用的CPU时间.
#define SIGPROF 27 // 类似于SIGALRM/SIGVTALRM, 但包括该进程用的CPU时间以及系统调用的时间
#define SIGWINCH 28 // 窗口大小改变时发出
#define SIGIO 29 // 文件描述符准备就绪, 可以开始进行输入/输出操作
#define SIGPOLL SIGIO // 同上,别称
#define SIGPWR 30 // 电源异常
#define SIGSYS 31 // 非法的系统调用

常见的信号:
在这里插入图片描述
1·处理信号:sigaction
sigaction() 系统调用用于更改进程在接收到特定信号时所采取的操作。通过 sigaction 系统调用设置信号处理函数。如果没有为一个信号设置对应的处理函数,就会使用默认的处理函数,否则信号就被进程截获并调用相应的处理函数。

extern int sigaction(int, const struct sigaction*, struct sigaction*);
----
1/第一个参数,int类型,代表要关注的信号量
2/第二个参数,sigaction结构体指针,代表当某个信号发生时,应该如何处理
3/第三个参数,也是sigaction结构体指针,它代表默认的处理方式,当我们定义了信号处理的时候,
用它之前的默认处理方式

所以要订阅信号,最简单的做法直接用一个循环遍历订阅所有要订阅的信号,对每一个信号调用sigaction()

void init() {
    struct sigaction handler;
    struct sigaction old_signal_handlers[SIGNALS_LEN];
    for (int i = 0; i < SIGNALS_LEN; ++i) {
        sigaction(signal_array[i], &handler, & old_signal_handlers[i]);
    }
}

2·捕捉Carsh的位置
sigaction 结构体有一个 sa_sigaction变量,他是个函数指针,原型为:void (*)(int siginfo_t *, void *)
因此,我们可以声明一个函数,直接将函数的地址赋值给sa_sigaction

void signal_handle(int code, siginfo_t *si, void *context) {
    发生 Crash 的时候就会回调我们传入的signal_handle()函数了。
    在signal_handle()函数中,我们得要想办法拿到当前执行的代码信息。
}

void init() {
	struct sigaction old_signal_handlers[SIGNALS_LEN];
	
	struct sigaction handler;
	handler.sa_sigaction = signal_handle;
	handler.sa_flags = SA_SIGINFO;
	
	for (int i = 0; i < SIGNALS_LEN; ++i) {
	    sigaction(signal_array[i], &handler, & old_signal_handlers[i]);
	}
}

3·设置紧急栈空间
这种情况主要考虑的是无限递归造成堆栈溢出,如果在一个已溢出的堆栈处理信号,那么肯定是失败的,可以使用sigaltstack() 在任意线程注册一个可选的栈,系统会在危险时机把栈指针指向这个地方,使得可以在一个新的栈上运行信号处理函数

void signal_handle(int sig) {
    write(2, "stack overflow\n", 15);
    _exit(1);
}
unsigned infinite_recursion(unsigned x) {
    return infinite_recursion(x)+1;
}
int main() {
    static char stack[SIGSTKSZ];
    stack_t ss = {
        .ss_size = SIGSTKSZ,
        .ss_sp = stack,
    };
    struct sigaction sa = {
        .sa_handler = signal_handle,
        .sa_flags = SA_ONSTACK
    };
    sigaltstack(&ss, 0);
    sigfillset(&sa.sa_mask);
    sigaction(SIGSEGV, &sa, 0);
    infinite_recursion(0);
}

4·获取Crash数据
异常处理函数的第3个参数 void* context 将会用与 crash 数据的收集。context 参数是指向 ucontext_t 类型的一个指针。ucontext_t结构体会包含出现异常的线程上下文信息:

  • 执行栈
  • 存储的寄存器
  • 阻塞的信号列表

具体的字段信息:

  • uc_link: 当前方法返回时应该返回到的地址(如果 uc_link 等于NULL,那么当这个方法返回时进程就会退出)
  • uc_sigmask: 阻塞的信号
  • uc_stack: 执行栈
  • uc_mcontext: 存储的寄存器(uc_mcontext 字段与机器的处理器架构相关)

由于寄存器等信息在不同处理器架构下都不相同。如下是在 arm 架构下的 ucontext_t 定义:

#if defined(__arm__)

#define NGREG 18 /* Like glibc. */

typedef int greg_t;
typedef greg_t gregset_t[NGREG];
typedef struct user_fpregs fpregset_t;

#include <asm/sigcontext.h>
typedef struct sigcontext mcontext_t;

typedef struct ucontext {
  unsigned long uc_flags;
  struct ucontext* uc_link;
  stack_t uc_stack;
  mcontext_t uc_mcontext;
  sigset_t uc_sigmask;
  /* Android has a wrong (smaller) sigset_t on ARM. */
  uint32_t __padding_rt_sigset;
  /* The kernel adds extra padding after uc_sigmask to match glibc sigset_t on ARM. */
  char __padding[120];
  unsigned long uc_regspace[128] __attribute__((__aligned__(8)));
} ucontext_t;

从上面可以看出,在 ARM 下这里会在 gregset_t 数组中储存 18 个寄存器,而且 mcontext_t 的类型是 arm/sigcontext.h 中的 sigcontext:

struct sigcontext {
  unsigned long trap_no;
  unsigned long error_code;
  unsigned long oldmask;
  unsigned long arm_r0;
  unsigned long arm_r1;
  unsigned long arm_r2;
  unsigned long arm_r3;
  unsigned long arm_r4;
  unsigned long arm_r5;
  unsigned long arm_r6;
  unsigned long arm_r7;
  unsigned long arm_r8;
  unsigned long arm_r9;
  unsigned long arm_r10;
  unsigned long arm_fp;
  unsigned long arm_ip;
  unsigned long arm_sp;
  unsigned long arm_lr;
  unsigned long arm_pc;
  unsigned long arm_cpsr;
  unsigned long fault_address;
};

sigcontext 中的 arm_pc 就代表了 ARM 处理器的 PC 寄存器。
5·定位问题代码
当native代码出现异常时,我们能够从输出看到问题代码所属文件和行数,这就涉及到so库文件的编码和pc程序计数器运行原理。PC寄存器存储着处理器当前执行的指令的内存地址,获取这个内存地址之后,使用addr2line工具就能找到你地址对应的源码行数。
程序寄存器中存储的当前指令在内存中的绝对地址 ,而 addr2line 工具需要的是指令在指令所属的 so 中的相对地址,所以需要先获取出现异常的指令属于的共享库(so)被加载到内存的开始地址,然后使用 绝对地址 减去 开始地址 得出程序寄存器相对 开始地址 的偏移量: 相对地址 = 绝对地址(pc) - so被加载到的地址。通过 dladdr 库函数,可以找到一个绝对地址所属的 so, 以及 so 被加载到内存的位置

Dl_info dl_info;
LOG_D("calculate pc(%d)", absolute_pc);
int result = dladdr((void *) absolute_pc, &dl_info);
if (result && dl_info.dli_fname) {
    // so 加载到内存的地址
    uint base = reinterpret_cast<long>(dl_info.dli_fbase);
    // 当前 pc 属于的方法的名称
    LOG_D("symbol is %s", dl_info.dli_sname);
    // 计算相对位置
    uint relative_pc = absolute_pc - base;
    return relative_pc;
}
return 0;

Android Gradle Plugin 在 native 编译时会默认对 so 进行 strip 操作,so 中与调试相关的信息都被去掉了。所以可以在 debug 编译下禁用 strip:

android {
    ...
        buildTypes {
            ...
                debug {
                    packagingOptions {
                        doNotStrip "*/x86_64/*.so"
                    }
                }
        }
    ...
        }


示例:

  • 定义一个 NativeCatcher 空间来做native crash的收集
namespace NativeCatcher {
    // 只捕获会造成进程终止的几种异常
    const int SIGNALS_LEN = 7;
    const int signal_array[] = {SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS};
    // 储存系统默认的异常处理
    struct sigaction old_signal_handlers[SIGNALS_LEN];

    void init();

    void signal_handler(int, siginfo_t *, void *);

    void make_crash();
}
  • JNI_OnLoad的时候,调用init 设置异常处理函数
static jclass CLASS = nullptr;

extern "C" jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    NativeCatcher::init();
    //...
    return JNI_VERSION_1_4;
}

  • 注册异常处理函数, 并持有默认的处理函数。sigactionsigaction() 系统调用的参数, sa_flags 用于配置信号会携带的数据, 如果 sa_flags 含有SA_SIGINFO标志位, 则异常处理函数(sa_sigaction) 需要为 void (*sa_sigaction)(int, siginfo_t *, void *) 的函数指针,否则就需要为void (*sa_handler)(int)的函数指针。
void NativeCatcher::init() {
    struct sigaction handler = {
            .sa_sigaction = NativeCatcher::signal_handler,
            .sa_flags = SA_SIGINFO
    };
    for (int i = 0; i < SIGNALS_LEN; ++i) {
        sigaction(signal_array[i], &handler, &old_signal_handlers[i]);
    }
}
  • 设置异常处理函数
void NativeCatcher::signal_handler(int signal, siginfo_t *info, void *context) {
    // 自己做一些处理工作
    const int code = info->si_code;
    LOG_D("handler signal %d, code: %d, pid: %d, uid: %d, tid: %d",
          signal,
          code,
          info->si_pid,
          info->si_uid,
          info->si_tid
    );

    // 找到异常对应的默认处理函数
    int index = -1;
    for (int i = 0; i < SIGNALS_LEN; ++i) {
        if (signal_array[i] == signal) {
            index = i;
            break;
        }
    }
    if (index == -1) {
        LOG_E("Not found match handler");
        exit(code);
    }
    struct sigaction old = old_signal_handlers[index];
    // 调用默认的异常处理函数
    old.sa_sigaction(signal, info, context);
}
  • 模拟产生异常
void NativeCatcher::make_crash() {
    int a = 0;
    int i = 10 / a;
}

推荐阅读
Android 处理 Native Crash
Android Native Crash 收集
Android 平台 Native Crash 捕获原理详解

Native Crash捕获与解析

这里主要是下面的两个方法:

  1. 通过DropBox日志解析–适用于系统应用,Android Dropbox 是 Android 在 Froyo(API level 8) 引入的用来持续化存储系统数据的机制。主要用于记录 Android 运行过程中, 内核, 系统进程, 用户进程等出现严重问题时的 log, 可以认为这是一个可持续存储的系统级别的 logcat。相关文件记录存储目录:/data/system/dropbox
    1. 借助上述的ndk-stack工具,可以直接将DropBox下面的日志解析成堆栈
    2. Android/SDK/NDK提供的工具linux-android-addr2line ,aarch64-linux-android-addr2line -f -C -e libbreakpad-core.so 00000000000161a0
  2. 通过BreakPad捕获解析–适用于所有应用,非系统应用可以通过google提供的开源工具BreakPad进行监测分析,CrashSDK也是采用的此种方式,可以实时监听到NE的发生,并且记录相关的文件, 从而可以将崩溃和相应的应用崩溃时的启动、场景等结合起来上报。
    1. 也是通过inux-android-addr2line解析出错误的方法

推荐阅读:
Android NativeCrash 捕获与解析
Android 平台 Native Crash 问题分析与定位


相关文章:

  • 【glib】vs2022 v163 debug win32: meson构建 glib-2.67.6
  • JlinkV9的Vtref详解
  • Thinkphp5.1对接ueditor(自定义上传接口)
  • “双非”渣本投岗爱奇艺(Java),三轮技术面等消息,侥幸通过!
  • FlinkSQL系列04-CDC连接器
  • 包-node.js中的第三方模块
  • vscode 个人实用插件(资源集合)
  • GTOT-Toolkit模板参考
  • [贪心]Min-Max Array Transformation Codeforces1721C
  • 猿创征文|【算法入门必刷】数据结构-栈(二)
  • 数据结构-压缩软件核心(利用哈夫曼树进行编码,对文件进行压缩与解压缩)
  • 月薪12.8K,零基础转行软件测试5月斩获3份过万offer,分享一些我的秘招~
  • 推荐一款新式开源的反向代理工具(FRP)
  • 复习一:基本概念和术语
  • Vue基础自学系列 | webpack中的插件
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • canvas实际项目操作,包含:线条,圆形,扇形,图片绘制,图片圆角遮罩,矩形,弧形文字...
  • Docker: 容器互访的三种方式
  • java B2B2C 源码多租户电子商城系统-Kafka基本使用介绍
  • java中的hashCode
  • Laravel 菜鸟晋级之路
  • Laravel 中的一个后期静态绑定
  • PHP变量
  • SpingCloudBus整合RabbitMQ
  • tab.js分享及浏览器兼容性问题汇总
  • 从零开始的无人驾驶 1
  • 基于 Ueditor 的现代化编辑器 Neditor 1.5.4 发布
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 如何设计一个比特币钱包服务
  • 手写双向链表LinkedList的几个常用功能
  • 优化 Vue 项目编译文件大小
  • 资深实践篇 | 基于Kubernetes 1.61的Kubernetes Scheduler 调度详解 ...
  • #QT(TCP网络编程-服务端)
  • #宝哥教你#查看jquery绑定的事件函数
  • #大学#套接字
  • $redis-setphp_redis Set命令,php操作Redis Set函数介绍
  • (07)Hive——窗口函数详解
  • (LeetCode C++)盛最多水的容器
  • (九)c52学习之旅-定时器
  • (十八)三元表达式和列表解析
  • (原)本想说脏话,奈何已放下
  • (转)Oracle存储过程编写经验和优化措施
  • (转)大型网站架构演变和知识体系
  • (轉)JSON.stringify 语法实例讲解
  • **Java有哪些悲观锁的实现_乐观锁、悲观锁、Redis分布式锁和Zookeeper分布式锁的实现以及流程原理...
  • .Net接口调试与案例
  • /etc/sudoers (root权限管理)
  • [ vulhub漏洞复现篇 ] GhostScript 沙箱绕过(任意命令执行)漏洞CVE-2019-6116
  • [C# 开发技巧]实现属于自己的截图工具
  • [C#小技巧]如何捕捉上升沿和下降沿
  • [CERC2017]Cumulative Code
  • [DP 训练] Longest Run on a Snowboard, UVa 10285
  • [HEOI2013]ALO
  • [IE编程] 如何编程清除IE缓存
  • [iHooya]2023年1月30日作业解析