Signal Handling in DBT
Table of Contents
- 1. Signal Handling in DBT
- 1.1. testing code
- 1.2. tombstone info
- 1.3. before SIGSEGV
- 1.4. page fault
- 1.5. prepare to invoke the master_signal_handler
- 1.6. master_signal_handler
- 1.7. sigreturn
- 1.8. dispatcher_trampoline
- 1.9. dispatcher
- 1.10. translated sa_handler
- 1.11. ARM_RT_SIGRETURN_TRAMPOLINE
- 1.12. svc_trampoline
- 1.13. SIGSEGV again
- 1.14. back to debuggerd32
- 1.15. To summaries
1. Signal Handling in DBT
1.1. testing code
int main(int argc, char *argv[]) { __asm__( "mov r0, #100\n\t" \ "mov r1, #100\n\t" \ :); int x = *((int *)0); return 0; }
1.2. tombstone info
tombstone generated by debuggerd32 when running the previous testing code with arm_translator:
01-19 22:25:39.278 E/DEBUG ( 2381): ABI: arm 01-19 22:25:39.279 F/DEBUG ( 2381): pid: 3867, tid: 3867, name: a.out >>> ./arm_translator <<< 01-19 22:25:39.281 E/DEBUG ( 2381): AM write failed: Broken pipe 01-19 22:25:39.282 F/DEBUG ( 2381): signal 11 (SIGSEGV), code 2 (SEGV_ACCERR), fault addr 0x0 01-19 22:25:39.288 F/DEBUG ( 2381): r0 00000064 r1 00000064 r2 bffddb2c r3 00000000 01-19 22:25:39.289 F/DEBUG ( 2381): r4 bffddb2c r5 bffddb24 r6 00000001 r7 80000334 01-19 22:25:39.289 F/DEBUG ( 2381): r8 00000000 r9 00000000 sl 00000000 fp bffddaec 01-19 22:25:39.290 F/DEBUG ( 2381): ip bf7a55e0 sp bffddad8 lr bf74635f pc 7f4239f0 cpsr 40000000 01-19 22:25:39.292 F/DEBUG ( 2381): 01-19 22:25:39.292 F/DEBUG ( 2381): backtrace: 01-19 22:25:39.293 F/DEBUG ( 2381): #00 pc 7f4239f0 <unknown> 01-19 22:25:39.294 F/DEBUG ( 2381): #01 pc 0001735d /system/lib/libc.so (__libc_init+44) 01-19 22:25:39.294 F/DEBUG ( 2381): #02 pc 007ffb18 <unknown> 01-19 22:25:39.331 F/DEBUG ( 2381): 01-19 22:25:39.331 F/DEBUG ( 2381): Tombstone written to: /data/tombstones/tombstone_03
1.3. before SIGSEGV
before crash, it should be running in the code cache:
3867| DEBUG(translate): emit: 0x7f7f42088c: movz w0, #0x64 3867| DEBUG(translate): emit: 0x7f7f420890: movz w1, #0x64 3867| DEBUG(translate): emit: 0x7f7f420894: movz w3, #0 3867| DEBUG(translate): emit: 0x7f7f420898: ldr w3, [x3]
which is translated from:
3867| DEBUG(translate): 80000348: mov r0, #0x64 3867| DEBUG(translate): 8000034c: mov r1, #0x64 3867| DEBUG(translate): 80000350: mov r3, #0 3867| DEBUG(translate): 80000354: ldr r3, [r3]
when executing the translated instruction
0x7f7f420898: ldr w3,[x3]
a page fault will be triggerd
1.4. page fault
current user space context (gp[0..15], pc, sp, …) will be stored in the kernel stack by hardware and kernel.
ptrace(PTRACE_GETREGS…) will fetch register from the kernel stack, which is shown in the tombstone
1.5. prepare to invoke the master_signal_handler
page fault will deliver SIGSEGV to the process itself. before return to userspace, the master_signal_handler registered by the translator need to be invoked first.
kernel will setup a sigframe on user stack. the sigframe contains:
- retcode, which is sigreturn in vdso
- ucontext, which is filled with the kernel stack content (or the user space context before the page fault)
- siginfo
- signo
then, the pc stored in kernel stack is changed to the master_signal_handler, so that ret_from_intr will pop the kernel stack then jump to the master_signal_handler
1.6. master_signal_handler
master_signal_handler(signo, siginfo, uc): dbt_handle_signal(siginfo, uc) /* uc->uc_mcontext.pc is the pc that caused the SIGSEGV in * translated code, which is saved on the kernel stack and copied * to uc*/ fragment_lookup_result_t result = fragment_lookup_by_addr(frag_abs_to_offset((void*)uc->uc_mcontext.pc)); saved_desc = result.frag->desc; /* saved_desc.pc is the entry address in the guest code */ recover_fault_info(uc, &saved_desc, fault_offset, metadata) // uc->uc_mcontext.pc is chaged to the dispatcher_trampoline uc->uc_mcontext.pc = (uintptr_t)frag_offset_to_abs(dispatcher_trampoline); uc->uc_mcontext.regs[16] = DISPATCH_DATA_ABORT;
1.7. sigreturn
when the master_signal_handler returns, it will call sigreturn, which is setup by the kernel when setting up sigframe.
sigreturn will fetch the sigframe on the user stack and recover the `origin` kernel stack, but since we have changed the uc_mcontext.pc to dispatcher_trampoline, when sigreturn returns to user space, pc will point to `dispatcher_trampoline`
1.8. dispatcher_trampoline
the dispatcher_trampoline is in the begining of code cache, it generally works like this:
/* save all 15 gp registers to dbt_context_t */ for (unsigned i = 0; i < 15; i++) emit(trans, aarch64_encode_LDR_STR_unsigned_immed(2, 0, 0, (offsetof(dbt_context_t, gp) >> 2) + i, 31, i)); // STR Wi, [SP, #gp[i]] /* load dispatcher to x4 and branch to it */ emit_load_immediate64(trans, (uint64_t)dispatcher, 4); // MOV X4, #dispatcher emit(trans, aarch64_encode_BLR(4)); // BLR X4 emit(trans, aarch64_encode_logical_reg(1, 1, 0, 0, 0, 0, 31, 16)); // MOV X16, X0 /* restore gp registers from dbt_context_t */ for (unsigned i = 0; i < 15; i++) emit(trans, aarch64_encode_LDR_STR_unsigned_immed(2, 0, 1, (offsetof(dbt_context_t, gp) >> 2) + i, 31, i)); // LDR Wi, [SP, #gp[i]] /* branch to the PC value returned by the dispatcher */ emit(trans, aarch64_encode_BR(16)); // BR X16
1.9. dispatcher
1.9.1. dispatcher
dispatcher(dbt_context_t): if (reason == DISPATCH_DATA_ABORT || reason == DISPATCH_PENDING_SIGNAL): /* saved_desc.pc point to the guest code that cause the abort */ desc = saved_desc; switch (reason): case DISPATCH_DATA_ABORT: target_regs_t regs; ctx_to_target_regs(®s, ctx, &desc); for (unsigned i = 0; i < 15; i++): regs->gp[i] = ctx->gp[i]; regs->pc = desc->pc & ~1; raise_sync_signal(®s, &abort_siginfo, &abort_extra_siginfo) /* raise_sync_signal will put the real sa_handler (in guest code) * in regs->pc */ target_regs_to_ctx(ctx, &desc, ®s); for (unsigned i = 0; i < 15; i++) ctx->gp[i] = regs->gp[i]; if (regs->pstate & BIT(5)) desc->pc = regs->pc | 1; else desc->pc = regs->pc & ~3; // desc->pc now points to the sa_handler in guest code fragment_lookup_result_t result = fragment_lookup_by_desc(&desc, flexible_bindings); if (!result.frag && !result.persist_frag): result.frag = translate_basic_block(&desc); frag_offset_t next_pc = result.frag->start // return the sa_handler in code cache, the dispatcher_trampoline // will branch to it return frag_offset_to_abs(next_pc);
1.9.2. raise_sync_signal
raise_sync_signal do_signal(regs, si, ...) /*setup a sigframe in the user stack as the kernel will do */ target_setup_sigframe() sp -= sizeof(target_rt_sigframe_t); rt_frame = (target_rt_sigframe_t*)sp; /* remember that regs->pc points to guest code, so sig handler * could get the `real` pc (pc in guest code) from * uc_mcontext.arm_pc */ safe_write(&frame->uc.uc_mcontext.arm_pc, regs->pc) regs->gp[0] = si->si_signo; regs->gp[1] = (uintptr_t)&rt_frame->info; regs->gp[2] = (uintptr_t)&frame->uc; regs->gp[14] = (sa->sa_flags & SA_SIGINFO) ? ARM_RT_SIGRETURN_TRAMPOLINE : ARM_SIGRETURN_TRAMPOLINE; /* regs->pc now points to the sig handler in guest code */ regs->pc = sa->sa_handler_;
1.10. translated sa_handler
the translated sal_handler will notify debuggerd32 to ptrace it and debuggerd will wait on waitpid for another signal, then sal_handler will branch to something (maybe the ret_r14_trampoline?) to make a branch_return to ARM_RT_SIGRETURN_TRAMPOLINE (setup by target_setup_sigframe)
1.11. ARM_RT_SIGRETURN_TRAMPOLINE
ARM_RT_SIGRETURN_TRAMPOLINE is a trampoline in the guest memory, which is:
MOV R7, #TARGET_NR_rt_sigreturn SVC #0
somthing (maybe the ret_r14_trampoline?) will translate the ARM_RT_SIGRETURN_TRAMPOLINE and branch to the translated code, which will be a svc_trampoline
1.12. svc_trampoline
svc_trampoline will invoke:
dispatcher_trampoline(): dispatcher() do_syscall() do_sigreturn() target_restore_sigframe() safe_read(&frame->uc.uc_mcontext.arm_pc, regs->pc)
the last `target_restore_sigframe` will restore target_regs with the sigframe setup by target_setup_sigframe.
note that target_regs.pc is same as the arm32 pc that cause the SIGSEGV
as shown in dispatcher, dispatcher will find the corresponding arm64 pc of arm32 pc by fragment_lookup_by_desc or translate_basic_block. then branch to the arm64 pc
1.13. SIGSEGV again
the arm64 pc will cause SIGSEGV again
1.14. back to debuggerd32
the second SIGSEGV will wake up debuggerd32 from waitpid, then debuggerd32 will use ptrace(PTRACE_GETREGS,…) to dump registers, which is shown in the tombstone info
1.15. To summaries
- master_signal_handler found the arm32 pc that cause the SIGSEGV by fragment_lookup_by_addr
- master_signal_handler branch to the dispatcher_trampoline by modifying the uc_mcontext->pc
- dispatcher_trampoline will:
- set up the sigframe in user stack
- find the arm64 pc of the translated sa_handler by fragment_lookup_by_desc or translate_basic_block
- branch to translated sa_handler
- return address of the translated sa_handler will branch to the svc_trampoline corresponding to rt_sigreturn_trampoline
- translated rt_sigreturn_trampoline will call do_sigreturn, which will call target_restore_sigframe to restore the context of translated code
- dispatcher will find fragment corresponding to arm32 pc that cause the SIGSEGV and branch to it, which will cause another SIGSEGV
- the second SIGSEGV will wake up debuggerd32 and debuggerd32 will dump the tombstone
Backlinks
RISU (RISU > Implementation details > risu.c): 1. master 和 apprentice 通信 2. 通过 sighandler 触发通信和检查, 并且用 sighandler 的 ucontext 读写寄存器 (参 考 kernel signal, signal handling in DBT) 3. 检查寄存器, 内存是否一致