Android Native Crash 收集,Android开发必须会的技能

本文详细介绍了在Android开发中如何捕获和分析崩溃,包括使用uc_mcontext获取崩溃时的寄存器信息,通过pc_from_ucontext转换pc值,以及利用库获取函数调用栈。此外,作者还讨论了面试准备和知识体系的重要性,提倡系统化学习以提升技术水平。
摘要由CSDN通过智能技术生成

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 全都有,所以非常大,在使用的时候得把无关的平台剥离掉减小体积。

下面以第一种为例讲一下实现:
核心方法是使用<unwind.h>库提供的一个方法_Unwind_Backtrace()这个函数可以传入一个函数指针作为回调,指针指向的函数有一个重要的参数是_Unwind_Context类型的结构体指针。
可以使用_Unwind_GetIP()函数将当前函数调用栈中每个函数的绝对内存地址(也就是上文中提到的 pc 值),写入到_Unwind_Context结构体中,最终返回的是当前调用栈的全部函数地址了,_Unwind_Word实际上就是一个unsigned int
capture_backtrace()返回的就是当前我们获取到调用栈中内容的数量。

/**

  • callback used when using <unwind.h> to get the trace for the current context
    */
    _Unwind_Reason_Code unwind_callback(struct _Unwind_Context *context, void *arg) {
    backtrace_state_t *state = (backtrace_state_t *) arg;
    _Unwind_Word pc = _Unwind_GetIP(context);
    if (pc) {
    if (state->current == state->end) {
    return _URC_END_OF_STACK;
    } else {
    *state->current++ = (void *) pc;
    }
    }
    return _URC_NO_REASON;
    }

/**

  • uses built in <unwind.h> to get the trace for the current context
    */
    size_t capture_backtrace(void **buffer, size_t max) {
    backtrace_state_t state = {buffer, buffer + max};
    _Unwind_Backtrace(unwind_callback, &state);
    return state.current - buffer;
    }

当所有的函数的绝对内存地址(pc 值)都获取到了,就可以用上文讲的办法将 pc 值转换为相对偏移量,获取到真正的函数信息和相对内存地址了。

void *buffer[max_line];
int frames_size = capture_backtrace(buffer, max_line);
for (int i = 0; i < frames_size; i++) {
Dl_info info;
const void *addr = buffer[i];
if (dladdr(addr, &info) && info.dli_fname) {
void * const nearest = info.dli_saddr;
uintptr_t addr_relative = addr - info.dli_fbase;
}

Dl_info是一个结构体,内部封装了函数所在文件、函数名、当前库的基地址等信息

typedef struct {
const char dli_fname; / Pathname of shared object that
contains address */
void dli_fbase; / Address at which shared object
is loaded */
const char dli_sname; / Name of nearest symbol with address
lower than addr */
void dli_saddr; / Exact address of symbol named
in dli_sname */
} Dl_info;

有了这个对象,我们就能获取到全部想要的信息了。虽然获取到全部想要的信息,但<unwind.h>有个麻烦的就是不想要的信息也给你了,所以需要手动过滤掉各种系统错误,最终得到的数据,就可以上报到自己的服务器了。

数据回传到服务器

数据回传有两种方式,一种是直接将信息写入文件,下次启动的时候直接由 Java 上报;另一种就是回调 Java 代码,让 Java 去处理。用 Java 处理的好处是 Java 层可以继续在当前上下文上加上 Java 层的各种状态信息,写入到同一个文件中,使得开发在解决 bug 的时候能更方便。
这里就简单将数据写入文件了。
void save(const char *name, char *content) {
FILE *file = fopen(name, “w+”);
fputs(content, file);

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

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

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

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

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

如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
img

如何做好面试突击,规划学习方向?

面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。

学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。

同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节

image

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

image

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。

[外链图片转存中…(img-p9zeoG9G-1711617907362)]

网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。

本文已被CODING开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》收录

  • 11
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值