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
,其通过sp
,maps
和寄存器的值完成栈回溯。
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
获取perfbuffer
中PERF_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_frame
的dwarf
进行栈回溯可以正常运行。
参考:
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