Backtrace
Table of Contents
1. Backtrace
1.1. unwind_backtrace
stack unwindling 的目的是为了得到保存在每个 stack frame 里的 ra (return addr), 然后在 elf 的符号表中可以 ra 对应的符号
1.1.1. 基于 frame pointer
当编译器没有使用 omit-frame-pointer 时, 函数调用时大约会这样:
sw $fp, 0($sp) # 保存 fp move $fp, $sp # fp 指向当前 stack base sub $sp, 12 # 分配 stack frame sw $ra, -4($fp) # 保存 ra, 也可以用 sw $ra, 4($sp) sw $s1, -8($fp) # 保存 callee saved reg, 也可以用 sw $ra, 0($sp)
所以 stack unwindling 时:
- 通过 fp 可以得到当前 stack base
- *(fp) 即上一个 stack frame 的 fp
- *(fp-4) 即 ra
1.1.2. 基于 dwarf
1.1.3. 基于代码分析
当 elf 既没有使用 frame pointer, 又没有提供 eh_debug 时, 如何进行 stack unwindling?
libcorkscrew 的方法大概是:
- 通过 signal 或 ptrace 获得当前 frame 的 pc, sp
- ra <- pc
- 从 ra 开始, 向后依次读取每条指令, 当读到 `add sp, imm` 时, 认为 imm 即是 stack size, 把 sp+imm 做为当前 frame 的基址 BP
- 继续向后扫描到 `sw ra, imm(sp)`, 确定 ra 的 offset 为 imm, 则 BP+imm 的值即是 ra
- 重复 3, 处理下一个 frame
static ssize_t unwind_backtrace_common(const memory_t* memory, const map_info_t* map_info_list, unwind_state_t* state, backtrace_frame_t* backtrace, size_t ignore_depth, size_t max_depth) { size_t ignored_frames = 0; size_t returned_frames = 0; for (size_t index = 0; returned_frames < max_depth; index++) { uintptr_t pc = state->pc; frame = add_backtrace_entry(pc, backtrace, ignore_depth, max_depth, &ignored_frames, &returned_frames); for (addr = state->pc; maxcheck-- > 0 && !found_start; addr -= 4) { uint32_t op; if (!try_get_word(memory, addr, &op)) break; /* gcc 编译器会生成下面的代码: * sw ra, imm(sp) * addiu sp, imm * 即反向查找指令时, 会先找到 0x27bd0000, 再找到 0xafbf0000 * */ switch (op & 0xffff0000) { case 0x27bd0000: // addiu sp, imm { // looking for stack being decremented int32_t immediate = ((((int)op) << 16) >> 16); if (immediate < 0) { stack_size = -immediate; found_start = true; ALOGV("@0x%08x: found stack adjustment=%d\n", addr, stack_size); } } break; case 0xafbf0000: // sw ra, imm(sp) ra_offset = ((((int)op) << 16) >> 16); ALOGV("@0x%08x: found ra offset=%d\n", addr, ra_offset); break; default: break; } } /* NOTE: 向后扫描指令时会修正 state->sp, 然后再读取 ra_offset 处的值 */ if (ra_offset) { uint32_t next_ra; if (!try_get_word(memory, state->sp + ra_offset, &next_ra)) break; state->ra = next_ra; } if (stack_size) { if (frame) frame->stack_size = stack_size; state->sp += stack_size; } state->pc = state->ra; } return returned_frames; }
1.2. find_symbol
stack unwindling 确定了每个 frame 对应的 pc, 通过查找 elf 符号表 (symtab, dynsym) 来确定每个 frame 对应哪个函数.
这一步只需要符号表, 并不需要 debug 信息, 所以无法确定具体的行号.
例如:
62: 0000000000001160 22 FUNC GLOBAL DEFAULT 16 main
0x1160 main 在 elf 中的 相对 地址, 22 是 main 的 大小
void find_symbol_ptrace(const ptrace_context_t* context, uintptr_t addr, const map_info_t** out_map_info, const symbol_t** out_symbol) { const map_info_t* mi = find_map_info(context->map_info_list, addr); symbol = find_symbol(data->symbol_table, addr - mi->start); }
- libcorkscrew 拿到的 addr 是进程中的绝对地址
- 通过读取 /proc/pid/maps 获得 maps, 根据 addr 获得它位哪个 map
- 通过 addr - mi->start 获得相对地址
- 用这个相对地址在 elf 符号表中查找它对应的符号
1.3. misc
1.3.1. 使用了 omit-frame-pointer 同时又调用了 alloca 的代码如何 unwindle
使用了 alloca 的代码会忽略 omit-frame-pointer…
$> cat test.c void foo() { alloca(0xff); } $> gcc test.c -c -fomit-frame-pointer -O0 $> objdump -d test.o 00000000 <foo>: 0: 27bdffe8 addiu sp,sp,-24 4: afbe0014 sw s8,20(sp) 8: 03a0f025 move s8,sp ;; s8 即 fp c: 27bdff40 addiu sp,sp,-256 ;; alloca 10: 00000000 nop 14: 03c0e825 move sp,s8 18: 8fbe0014 lw s8,20(sp) 1c: 27bd0018 addiu sp,sp,24 20: 03e00008 jr ra 24: 00000000 nop $> cat test.c void foo() {} $> gcc test.c -c -O0 $> objdump -d test.o 00000000 <foo>: 0: 27bdfff8 addiu sp,sp,-8 4: afbe0004 sw s8,4(sp) 8: 03a0f025 move s8,sp c: 00000000 nop 10: 03c0e825 move sp,s8 14: 8fbe0004 lw s8,4(sp) 18: 27bd0008 addiu sp,sp,8 1c: 03e00008 jr ra 20: 00000000 nop $> gcc test.c -c -O0 -fomit-frame-pointer $> objdump -d test.o 00000000 <foo>: 0: 00000000 nop 4: 03e00008 jr ra 8: 00000000 nop
Backlinks
RISC-V Tutorial (RISC-V Tutorial > RISC-V Assembly > Register): - fp 是 frame pointer, 主要和 Backtrace 有关