android ebpf实现栈回溯

栈回溯原理

利用栈帧

x86通常会使用ebp来保存栈帧,在函数头部首先会将ebp即调用者对应的栈帧保存,而调用者的返回地址就保存在此ebp对应的栈地址+4的栈地址中。这样经过多层函数调用,在内存中就会形成一个ebp链,只要知道当前ebp的值并遍历ebp链就可以找到每一层调用的返回地址,这样就可以完成函数调用的栈回溯。

arm64通常会使用x29来保存栈帧,通过在栈内存中会形成一条x29链,利用栈帧同样可以完成栈回溯。

对于MSVC编译器生成的x86,如果给MSVC编译器增加编译参数/Oy就可以不对栈帧进行保存,这样就可以省出一个寄存器ebp,同时可以减少指令的开销,但是这样就不能利用栈帧进行帧回溯了。

对于clang生成的arm64,增加编译参数-fomit-frame-pointer同样会舍弃栈帧。

基于异常处理的元数据

对于c++等有异常处理相关的语言而言,为了支持在异常处理过程中的栈展开会将堆栈展开的一些元数据保存到可执行文件中,而这些元数据中包含了一些函数调用链的信息。对于pe文件而言,这些栈展开相关的元数据会被保存在Exception Directory,对于elf文件而言,这部分数据会被保存在.eh_frame节区中。这些信息都会随着文件被加载到内存中,并且不能被去除。

对于arm平台而言.eh_frame中的元数据格式为dwarf,可以利用libunwindstack库进行解析完成栈回溯,关键函数是UnwindCallChain,其通过spmaps和寄存器的值完成栈回溯。

ebpf实现栈回溯

利用栈帧

ebpf内核程序可以使用bpf_get_stackid获取用户层函数的调用链,同时生成一个栈信息hash值stackid。调用链信息被保存在map中,对应的key就是steakid,用户层程序调用bpf_map_lookup_elem函数从map找到stackid对应的调用链信息。

bpf_get_stackid的调用过程为bpf_get_stackid-->get_perf_callchain-->perf_callchain_user,最后是通过栈帧生成对应的调用链

实际上还可以通过用户层调用perf_event_open传入PERF_SAMPLE_CALLCHAIN获取调用链,内核中bpf_perf_event_output函数经过如下调用过程bpf_perf_event_output-->__bpf_perf_event_output-->perf_event_output-->__perf_event_output-->perf_output_sample,最后perf_output_sample会判断
存在PERF_SAMPLE_CALLCHAIN的话就会将调用链信息输出到perf缓冲区中。

利用.eh_frame的dwarf

参考网上几位大佬的方法,通过ebpf获取pid,寄存器信息和栈信息后通过socket发送给守护进程利用libunwindstack完成栈回溯。

bool UnwindCallChain(int pid, uint64_t reg_mask, DataBuff *data_buf, int client_sockfd) {
    RegSet regs(data_buf->user_regs.abi, reg_mask, data_buf->user_regs.regs);
    LOG(DEBUG) << "abi:" << data_buf->user_regs.abi << ", arch:" << regs.arch;

    uint64_t sp_reg_value;
    if (!regs.GetSpRegValue(&sp_reg_value)) {
        std::cerr << "can't get sp reg value";
        return false;
    }
    LOG(DEBUG) << "sp_reg_value: 0x" << std::hex << sp_reg_value;
    
    uint64_t stack_addr = sp_reg_value;
    const char *stack = data_buf->user_stack.data;
    size_t stack_size = data_buf->user_stack.dyn_size;
    
    std::unique_ptr<unwindstack::Regs> unwind_regs(GetBacktraceRegs(regs));
    if (!unwind_regs) {
      return false;
    }
    std::shared_ptr<unwindstack::Memory> stack_memory = unwindstack::Memory::CreateOfflineMemory(
        reinterpret_cast<const uint8_t*>(stack), stack_addr, stack_addr + stack_size
    );

    std::string map_buffer;
    std::unique_ptr<unwindstack::Maps> maps;
    std::string proc_map_file = "/proc/" + std::to_string(pid) + "/maps";
    android::base::ReadFileToString(proc_map_file, &map_buffer);
    maps.reset(new unwindstack::BufferMaps(map_buffer.c_str()));
    maps->Parse();
    unwindstack::Unwinder unwinder(512, maps.get(), unwind_regs.get(), stack_memory);
    // default is true
    // unwinder.SetResolveNames(false);
    unwinder.Unwind();
    std::string frame_info = DumpFrames(unwinder);
    // LOG(DEBUG) << "frame_info:" << frame_info;
    int len = frame_info.length();
    send(client_sockfd, &len, 4, 0);
    send(client_sockfd, frame_info.c_str(), len, 0);
    return true;
}

ebpf从perf_buffer中读取的PERF_RECORD_SAMPLE类型数据是一个可变长度的结构,在调用perf_event_open的时候传入不同的参数其会返回不同结构的数据。libbpf在调用perf_event_open的时候会传入PERF_SAMPLE_RAW,而获取pid需要传入PERF_SAMPLE_TID,获取寄存器信息需要传入PERF_SAMPLE_REGS_USER,获取堆栈信息需要传入PERF_SAMPLE_STACK_USER

所以在调用perf_event_open的时候传入了PERF_SAMPLE_RAW | PERF_SAMPLE_TID | PERF_SAMPLE_REGS_USER | PERF_SAMPLE_STACK_USER参数后,对应PERF_RECORD_SAMPLE类型数据的格式为下

struct {
    struct perf_event_header header;
    u32    pid, tid;           /* if PERF_SAMPLE_TID */
    u32    size;               /* if PERF_SAMPLE_RAW */
    char   data[size];         /* if PERF_SAMPLE_RAW */
    u64    abi;                /* if PERF_SAMPLE_REGS_USER */
    u64    regs[weight(mask)]; /* if PERF_SAMPLE_REGS_USER */
    u64    size;               /* if PERF_SAMPLE_STACK_USER */
    char   data[size];         /* if PERF_SAMPLE_STACK_USER */
    u64    dyn_size;           /* if PERF_SAMPLE_STACK_USER && size != 0 */
};

libbpf调用perf_event_open的过程为perf_buffer__new-->__perf_buffer__new-->perf_buffer__open_cpu_buf-->syscall(__NR_perf_event_open),在perf_buffer__new函数中默认只设置了PERF_SAMPLE_RAW标志,需要为其增加其他三个标志。

libbpf获取perfbufferPERF_RECORD_SAMPLE类型的数据过程如下perf_buffer__poll-->perf_buffer__process_records-->perf_event_read_simple-->perf_buffer__process_record,默认情况下其只会将PERF_SAMPLE_RAW数据传递给用户层程序设置的handle_printf回调函数打印从内存层调用bpf_perf_event_output传递的数据,现在要增加对另外三种数据PERF_SAMPLE_TID | PERF_SAMPLE_REGS_USER | PERF_SAMPLE_STACK_USER的解析并传给守护进程解析栈回溯信息,最后将其打印出来。

libbpf的修改对应的commit为https://github.com/revercc/libbpf/commit/938b7cdfebe37e11faddba81bb8402dec2f1fb63

测试

最后同时使用上述两种方法同时打印栈回溯,效果如下。

可以发现神奇的现象,利用栈帧打印的栈回溯在打印到art_quick_generic_jni_trampoline函数的时候就出错了,这是因为art_quick_generic_jni_trampoline是java层调用jni方法的跳板函数,此跳板函数的X29并不会指向保存在栈中的调用者的x29,中间会差若个的栈空间。

在编译器增加了-fomit-frame-pointer参数时,利用栈帧进行栈回溯的方式也会失效,而利用.eh_framedwarf进行栈回溯可以正常运行。

参考:
http://www.manongjc.com/detail/23-sngpoeiuxmgaizw.html
https://blog.seeflower.dev/archives/175/#title-10
https://bbs.kanxue.com/thread-274546.htm#msg_header_h2_2
https://man7.org/linux/man-pages/man2/perf_event_open.2.html

posted @ 2023-11-05 05:49  怎么可以吃突突  阅读(760)  评论(0编辑  收藏  举报