迫于 NDK 自带的 _Unwind_Backtrace 之前发生过莫名其妙的 Crash,又没有办法控制这部分的代码,以至于直接把栈回溯功能下掉了。听说 xCrash 里自主实现了一套 dwarf 的解析,迫于需求来研究一下这块原理,当一个调包侠。
DWARF
DWARF 实际上是一套用来存储调试信息的数据格式,到目前已经发布了 5 个版本,具体标准可以到 https://dwarfstd.org/Download.php 下载到。
具体的标准我不是很关心,因为乍一看 PDF 里的东西还是挺多的,包括各种宏、类型、行号、对象、调用等等的信息都会在里面。跟调用栈有关数据类型,可以看《AARCH64平台的栈回溯》,主要用到的是:
-
FDE(Frame Description Entry): 存了每一帧覆盖的代码地址、以及 CFI,可以用来从 PC 找到当前帧(函数) -
CFI(Call Frame Instructions): 是一种抽象指令,用来表示开栈、压栈时的寄存器操作,栈回溯时要解释执行这些指令来还原每一帧的具体寄存器值。
获取 DWARF
xCrash 真正的回溯过程可以从 xcd_elf.c
的 xcd_elf_step
开始看,算起来有 5 种获取 DWARF 数据的途径,可以适配不同编译器,分别是从 ELF 的 .debug_frame
.eh_frame
以及从 .gnu_debugdata
里面加载一个压缩的 ELF,里面也可能包含 .debug_frame
.eh_frame
,arm 下还会再尝试使用 .ARM.exidx
:
intxcd_elf_step(xcd_elf_t *self, uintptr_t rel_pc, uintptr_t step_pc, xcd_regs_t *regs, int *finished, int *sigreturn){ *finished = 0; *sigreturn = 0;...//try DWARF (.debug_frame and .eh_frame)if (0 == xcd_elf_interface_dwarf_step(self->interface, step_pc, regs, finished)) return0;//create GNU interface (only once)if (NULL == self->gnu_interface && 0 == self->gnu_interface_created) { self->gnu_interface_created = 1; self->gnu_interface = xcd_elf_interface_gnu_create(self->interface); }//try DWARF (.debug_frame and .eh_frame) in GNU interfaceif (NULL != self->gnu_interface)if (0 == xcd_elf_interface_dwarf_step(self->gnu_interface, step_pc, regs, finished))return0;//try .ARM.exidx#ifdef __arm__if(0 == xcd_elf_interface_arm_exidx_step(self->interface, step_pc, regs, finished)) return0;#endif...}
DWARF 回溯
回溯过程大概分为几步:
-
获取完整的寄存器信息(ucontext) -
根据当前的 PC,去获取 FDE -
将寄存器信息代入,解释执行 FDE 里的 CFI 指令 -
当每一帧的 CFI 指令执行结束时,代表此时 PC 已经到达了 caller -
直到 PC 为 0 代表结束
intxcd_dwarf_step(xcd_dwarf_t *self, xcd_regs_t *regs, uintptr_t pc, int *finished){xcd_dwarf_fde_t *fde = NULL;xcd_dwarf_loc_t *loc = NULL;int r = XCC_ERRNO_NOTFND;//find FDE & CIE from PCif(NULL == (fde = xcd_dwarf_get_fde(self, pc))) {#if XCD_DWARF_DEBUG XCD_LOG_DEBUG("DWARF: get FDE failed, step_pc=%"PRIxPTR, pc);#endifgoto end; }//find LOCATION in the FDE from PCif(NULL == (loc = xcd_dwarf_get_loc(self, fde, pc))) {#if XCD_DWARF_DEBUG XCD_LOG_DEBUG("DWARF: get LOC failed, step_pc=%"PRIxPTR, pc);#endifgoto end; }//eval the actual registersif(0 != (r = xcd_dwarf_eval(self, fde, loc, regs, finished))) {#if XCD_DWARF_DEBUG XCD_LOG_DEBUG("DWARF: eval failed, step_pc=%"PRIxPTR, pc);#endifgoto end; }
getcontext
由上可知,DWARF 回溯需要获取完整的寄存器信息,xCrash 里提供两种方法,一种是 PTRACE_GETREGS
voidxcd_thread_load_regs(xcd_thread_t *self){uintptr_t regs[64]; //big enough for all architecturessize_t regs_len;#ifdef PTRACE_GETREGSif(0 != ptrace(PTRACE_GETREGS, self->tid, NULL, ®s)) { XCD_LOG_ERROR("THREAD: ptrace GETREGS failed, errno=%d", errno); self->status = XCD_THREAD_STATUS_REGS;return; } regs_len = XCD_REGS_USER_NUM;#elsestructioveciovec; iovec.iov_base = ®s; iovec.iov_len = sizeof(regs);if(0 != ptrace(PTRACE_GETREGSET, self->tid, (void *)NT_PRSTATUS, &iovec)) { XCD_LOG_ERROR("THREAD: ptrace GETREGSET failed, errno=%d", errno); self->status = XCD_THREAD_STATUS_REGS;return; } regs_len = iovec.iov_len / sizeof(uintptr_t);#endif xcd_regs_load_from_ptregs(&(self->regs), regs, regs_len);}
不过 ptrace 还得 attach,我的场景跟 crash 不太一样,不适合去 attach。 另一种是从 ucontext 加载
voidxcd_thread_load_regs_from_ucontext(xcd_thread_t *self, ucontext_t *uc){ xcd_regs_load_from_ucontext(&(self->regs), uc);}
这块本意是留给 signal
用的,我们自己做一个 ucontext
也能用,不过 Android 并不原生支持 getcontext
,需要借助 Google 的 breakpad
,里面实现了一个 breakpad_getcontext
,据说 Chrome 里用的也是这个,应该是比较靠谱的,直接嫖了。
调包侠
xCrash 的接口都是输出到一个 fd 里,并且访问不到 xcd_frame_t
的 fields
,得先改造一下,把 struct 定义从 *.c
里挪到 *.h
里, 然后调包侠如下:
size_tbacktrace_xcrash(void **buffer, size_t max){xcd_thread_t t;xcd_maps_t * _map;ucontext_t ctx;pid_t pid = getpid();pid_t tid = gettid(); breakpad_getcontext(&ctx); xcd_maps_create(&_map, pid); xcd_thread_init(&t, pid, tid); xcd_thread_load_regs_from_ucontext(&t, &ctx); xcd_thread_load_frames(&t, _map); xcd_maps_destroy(&_map);if(t.frames == nullptr) return0;xcd_frame_t * frame = t.frames->frames.tqh_first;int idx = 0;while (frame != nullptr && idx < max) { buffer[idx++] = (void*) frame->pc; frame = frame->link.tqe_next; }return idx;}
当然这个代码有点缺陷,每次都要 xcd_maps_create
刷新和解析一下 ELF 视图,没有什么缓存(不过性能瓶颈倒也不在这,不刷新的话大概快个 20%,还是在同一个速度量级上,暂时没有性能要求,就先不管了,不要放到高频函数里就可以了。
原文始发于微信公众号(秃头的逆向痴想):DWARF 栈回溯 & xCrash 调包侠
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论