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 时:

  1. 通过 fp 可以得到当前 stack base
  2. *(fp) 即上一个 stack frame 的 fp
  3. *(fp-4) 即 ra

1.1.2. 基于 dwarf

1.1.3. 基于代码分析

当 elf 既没有使用 frame pointer, 又没有提供 eh_debug 时, 如何进行 stack unwindling?

libcorkscrew 的方法大概是:

  1. 通过 signal 或 ptrace 获得当前 frame 的 pc, sp
  2. ra <- pc
  3. 从 ra 开始, 向后依次读取每条指令, 当读到 `add sp, imm` 时, 认为 imm 即是 stack size, 把 sp+imm 做为当前 frame 的基址 BP
  4. 继续向后扫描到 `sw ra, imm(sp)`, 确定 ra 的 offset 为 imm, 则 BP+imm 的值即是 ra
  5. 重复 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);
}
  1. libcorkscrew 拿到的 addr 是进程中的绝对地址
  2. 通过读取 /proc/pid/maps 获得 maps, 根据 addr 获得它位哪个 map
  3. 通过 addr - mi->start 获得相对地址
  4. 用这个相对地址在 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 有关

Author: [email protected]
Date: 2020-11-26 Thu 00:00
Last updated: 2024-02-01 Thu 14:04

知识共享许可协议