Android Native Crash 收集

#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 // 非法的系统调用

通常我们在做 crash 收集的时候,主要关注这几个信号量:

const int signal_array[] = {SIGILL, SIGABRT, SIGBUS, SIGFPE, SIGSEGV, SIGSTKFLT, SIGSYS};

对应的含义可以参考上文,

extern int sigaction(int, const struct sigaction*, struct sigaction*);

第一个参数 int 类型,表示需要关注的信号量
第二个参数 sigaction 结构体指针,用于声明当某个特定信号发生的时候,应该如何处理。
第三个参数也是 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]);
}
}

捕获到 Crash 的位置

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

void signal_handle(int code, siginfo_t *si, void *context) {
}

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]);
}
}

这样当发生 Crash 的时候就会回调我们传入的signal_handle()函数了。在signal_handle()函数中,我们得要想办法拿到当前执行的代码信息。

设置紧急栈空间

如果当前函数发生了无限递归造成堆栈溢出,在统计的时候需要考虑到这种情况而新开堆栈否则本来就满了的堆栈又在当前堆栈处理溢出信号,处理肯定是会失败的。所以我们需要设置一个用于紧急处理的新栈,可以使用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);
}

捕获出问题的代码

signal_handle() 函数中的第三个参数 contextuc_mcontext的结构体指针,它封装了 cpu 相关的上下文,包括当前线程的寄存器信息和奔溃时的 pc 值,能够知道崩溃时的pc,就能知道崩溃时执行的是那条指令,同样的,在本文顶部的那张图中寄存器快照就可以用如下代码获得。

char *head_cpu = nullptr;
asprintf(&head_cpu, “r0 %08lx r1 %08lx r2 %08lx r3 %08lx\n”
“r4 %08lx r5 %08lx r6 %08lx r7 %08lx\n”
“r8 %08lx r9 %08lx sl %08lx fp %08lx\n”
“ip %08lx sp %08lx lr %08lx pc %08lx cpsr %08lx\n”,
t->uc_mcontext.arm_r0, t->uc_mcontext.arm_r1, t->uc_mcontext.arm_r2,
t->uc_mcontext.arm_r3, t->uc_mcontext.arm_r4, t->uc_mcontext.arm_r5,
t->uc_mcontext.arm_r6, t->uc_mcontext.arm_r7, t->uc_mcontext.arm_r8,
t->uc_mcontext.arm_r9, t->uc_mcontext.arm_r10, t->uc_mcontext.arm_fp,
t->uc_mcontext.arm_ip, t->uc_mcontext.arm_sp, t->uc_mcontext.arm_lr,
t->uc_mcontext.arm_pc, t->uc_mcontext.arm_cpsr);

不过uc_mcontext结构体的定义是平台相关的,比如我们熟知的armx86这种都不是同一个结构体定义,上面的代码只列出了arm架构的寄存器信息,要兼容其他架构的 cpu 在处理的时候,就得要寄出宏编译大法,不同的架构使用不同的定义。

uintptr_t pc_from_ucontext(const ucontext_t *uc) {
#if (defined(arm))
return uc->uc_mcontext.arm_pc;
#elif defined(aarch64)
return uc->uc_mcontext.pc;
#elif (defined(x86_64))
return uc->uc_mcontext.gregs[REG_RIP];
#elif (defined(__i386))
return uc->uc_mcontext.gregs[REG_EIP];
#elif (defined (ppc)) || (defined (powerpc))
return uc->uc_mcontext.regs->nip;
#elif (defined(hppa))
return uc->uc_mcontext.sc_iaoq[0] & ~0x3UL;
#elif (defined(sparc) && defined (arch64))
return uc->uc_mcontext.mc_gregs[MC_PC];
#elif (defined(sparc) && !defined (arch64))
return uc->uc_mcontext.gregs[REG_PC];
#else
#error “Architecture is unknown, please report me!”
#endif
}

pc值转内存地址

pc值是程序加载到内存中的绝对地址,绝对地址不能直接使用,因为每次程序运行创建的内存肯定都不是固定区域的内存,所以绝对地址肯定每次运行都不一致。我们需要拿到崩溃代码相对于当前库的相对偏移地址,这样才能使用 addr2line 分析出是哪一行代码。通过dladdr()可以获得共享库加载到内存的起始地址,和pc值相减就可以获得相对偏移地址,并且可以获得共享库的名字。

Dl_info info;
if (dladdr(addr, &info) && info.dli_fname) {
void * const nearest = info.dli_saddr;
uintptr_t addr_relative = addr - info.dli_fbase;
}

获取 Crash 发生时的函数调用栈

获取函数调用栈是最麻烦的,至今没有一个好用的,全都要做一些大改动。常见的做法有四种:

  • 第一种:直接使用系统的<unwind.h>库,可以获取到出错文件与函数名。只不过需要自己解析函数符号,同时经常会捕获到系统错误,需要手动过滤。
  • 第二种:在4.1.1以上,5.0以下,使用系统自带的libcorkscrew.so,5.0开始,系统中没有了libcorkscrew.so,可以自己编译系统源码中的libunwindlibunwind是一个开源库,事实上高版本的安卓源码中就使用了他的优化版替换libcorkscrew
  • 第三种:使用开源库coffeecatch,但是这种方案也不能百分之百兼容所有机型。
  • 第四种:使用 Google 的breakpad,这是所有 C/C++堆栈获取的权威方案,基本上业界都是基于这个库来做的。只不过这个库是全平台的 android、iOS、Windows、Linux、MacOS 全都有,所以非常大,在使用的时候得把无关的平台剥离掉减小体积。

下面以第一种为例讲一下实现:
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

代码真的是重质不重量,质量高的代码,是当前代码界提倡的,当然写出高质量的代码肯定需要一个相当高的专业素养,这需要在日常的代码书写中逐渐去吸收掌握,谁不是每天都在学习呀,目的还不是为了一个,为实现某个功能写出高质量的代码。

所以,长征路还长,大家还是好好地做个务实的程序员吧。

最后,小编这里有一系列Android提升学习资料,有兴趣的小伙伴们可以来看下哦~

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

是为了一个,为实现某个功能写出高质量的代码。**

所以,长征路还长,大家还是好好地做个务实的程序员吧。

最后,小编这里有一系列Android提升学习资料,有兴趣的小伙伴们可以来看下哦~

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 27
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Android原生崩溃(android native crash)是指在Android平台上,由于代码执行错误或者资源耗尽等原因,导致应用程序无法正常运行而崩溃或者闪退的现象。原生崩溃产生的原因可能是由于C或者C++代码编写错误、内存溢出、线程竞争等。针对原生崩溃问题,开发人员需要使用调试工具进行定位和修复。 ### 回答2: Android Native Crash发生在安卓应用程序运行时,由于C或C++库的错误或者其他原因导致应用程序崩溃。有时候Native Crash可能会影响整个设备,尤其是当Native Crash发生在系统级别的代码中时。 产生Native Crash的原因通常包括以下几个方面: 1. 内存管理问题:Native Crash通常与内存管理问题相关,这可能是由于访问未初始化的内存,使用错误的指针或释放已释放的内存等原因引起的。 2. 硬件问题:Native Crash也可能与设备相关的硬件问题有关,例如访问不可用的硬件资源或硬件设备故障。 3. 应用程序代码问题:Native Crash可能发生在应用程序代码的错误、资源泄漏、堆栈溢出等问题引起的。 4. 第三方库问题:Native Crash也可能由第三方库中的错误或bug引起。这些库可能没有经过充分的测试,或者与设备硬件不兼容。 为了更好地解决Native Crash问题,开发者可以通过日志或崩溃报告(Crash Report)来检测和分析崩溃日志,并查看堆栈跟踪信息来确定导致Native Crash的来源。在开发过程中,经常使用除了自己编写的代码之外的第三方库时,还可以考虑使用崩溃的回溯工具,如Firebase Crashlytics等。 总之,Native CrashAndroid应用程序开发过程中经常遇到的问题,它可能会对用户体验和开发进度产生重大影响,因此开发者需要强化对Native Crash的理解和分析能力,以更好地解决Native Crash的问题。 ### 回答3: Android Native Crash,指的是在 Android 系统中发生的本地崩溃。本地崩溃是指应用程序使用本地代码,而不是 Java 代码,导致应用程序崩溃的问题。本地代码可以是编写在 C/C++ 等语言中的库,或是应用程序本身所编写的 Native 代码。 本地代码崩溃后,会在应用程序崩溃的同时发生。本地崩溃可发生在 Android 应用程序中的任何部分,比如,应用程序本地库、Android 系统库等等。大多数情况下,本地崩溃是由于访问无效内存、访问不合法指针、数组越界等问题引起的。 为了解决本地崩溃问题,Android 提供了一些工具和技术。比如,使用 ndk-stack 工具可以解析本地崩溃日志。Android Studio 也提供了一些工具来分析应用程序崩溃的原因。同时,我们也可以在应用程序中添加自定义的日志跟踪信息,以便更好地了解应用程序的崩溃原因。 还有一些其他的技术可以使用,如使用 Google 的 Crashlytics 来跟踪应用程序的崩溃问题。这个平台可以帮助开发者收集和分析应用程序在用户设备上的崩溃信息,并彻底解决这些问题。此外,Android 还提供了一些实用工具和技术,如 ANR(Application Not Responding)错误处理器、Tracer for OpenGL ES 和 Traceview 示例等。 总之,Android Native CrashAndroid 系统中常见的崩溃问题之一。了解它的原因并采用适当的解决方案可以使得我们更好地保持我们的应用程序的稳定性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值