sigfillset(&sa.sa_mask);
sigaction(SIGSEGV, &sa, 0);
infinite_recursion(0);
}
捕获出问题的代码
signal_handle()
函数中的第三个参数 context
是uc_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
结构体的定义是平台相关的,比如我们熟知的arm
、x86
这种都不是同一个结构体定义,上面的代码只列出了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
,可以自己编译系统源码中的libunwind
。libunwind
是一个开源库,事实上高版本的安卓源码中就使用了他的优化版替换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移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip204888 (备注Android)
如何做好面试突击,规划学习方向?
面试题集可以帮助你查漏补缺,有方向有针对性的学习,为之后进大厂做准备。但是如果你仅仅是看一遍,而不去学习和深究。那么这份面试题对你的帮助会很有限。最终还是要靠资深技术水平说话。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。建议先制定学习计划,根据学习计划把知识点关联起来,形成一个系统化的知识体系。
学习方向很容易规划,但是如果只通过碎片化的学习,对自己的提升是很慢的。
同时我还搜集整理2020年字节跳动,以及腾讯,阿里,华为,小米等公司的面试题,把面试的要求和技术点梳理成一份大而全的“ Android架构师”面试 Xmind(实际上比预期多花了不少精力),包含知识脉络 + 分支细节。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。
在搭建这些技术框架的时候,还整理了系统的高级进阶教程,会比自己碎片化学习效果强太多。
[外链图片转存中…(img-p9zeoG9G-1711617907362)]
网上学习 Android的资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。希望这份系统化的技术体系对大家有一个方向参考。