获取堆栈!可能很多新朋友看到这个就会想,这有什么难的嘛!直接new 一个Throwable获取不就可以了嘛,或者Thread.currentThread().stackTrace(kotlin)等等也可以呀!嗯!是的!我们在java层通常会有很固定的获取堆栈方式,这得益于java虚拟机的设计,也得益于java语言的设计,因为屏蔽了多平台底层的差异,我们就可以用相对统一的api去获取当前的堆栈。这个堆栈也特指java虚拟机堆栈!
但是对于native的堆栈,问题就来了!我们知道native层通常跟很多因素有关,比如链接器,编译器,还有各种库的版本,各种abi等等影响,获取一个堆栈消息,可没有那么简单,因为太多因素干扰了,这也是历史的包袱!还有对于我们android来说,android官方在对堆栈获取的方式,也是有历史变化的
在4.1.1
以上,5.0
以下,android native使用系统自带的libcorkscrew.so
,5.0开始,系统中没有了libcorkscrew.so
高版本的安卓源码中就使用了他的优化版替换libunwind
。同时对于ndk来说,编译器的版本也在不断变化,从默认的gcc变成clang(ndk >=13),可以看到,我们会在众多版本,众多因素下,找一个统一的方式,还真的不简单!不过呀!在2022的今天,google早已推出了一个计划统一库 breakpad,嗯!虽然能不能成为标准还未定,但是也是一个生态的进步
前面介绍了这么多方案,breakpad是不是Signal的首选呢!虽然breakpad不错,但是里面覆盖了太多其他系统的编译,比如mac,window等等标准,还有就是作为一个开源库,还是希望减少这些库的导入,所以跟大多数主流方案一直,我们选择用unwind.h去实现堆栈打印,因为这个就直接内置在我们的默认编译中了,而且这个在在android也能用!下面我们来看一下实现!即Signal项目的unwind-utils的实现。那么我们要考虑一些什么呢!
堆栈大小日志当然需要设定追溯的堆栈大小,内容太多不好(过于臃肿,排查困难),内容太少也不好(很有可能漏掉关键crash堆栈),所以Signal默认设置30条,可以根据实际项目修改
std::string backtraceToLogcat() {
默认30个
const size_t max = 30;
void *buffer[max];
//ostringstream方便输出string
std::ostringstream oss;
dumpBacktrace(oss, buffer, captureBacktrace(buffer, max));
return oss.str();
}
_Unwind_Backtrace
_Unwind_Backtrace是unwind提供给我们堆栈回溯函数
_Unwind_Reason_Code _Unwind_Backtrace(_Unwind_Trace_Fn, void *);
那么这个_Unwind_Trace_Fn是个啥,其实点进去看
typedef _Unwind_Reason_Code (*_Unwind_Trace_Fn)(struct _Unwind_Context *,
void *);
其实这就代表一个函数,对于我们常年写java的朋友有点不友好对吧,以java的方式,其实意思就是传xxx(随便函数名)( _Unwind_Context *,void *)这样的结构的函数即可,这里的意思就是一个callback函数,当我们获取到地址信息就会回调该参数,第二个就是需要传递给参数一的参数,这里有点绕对吧,我们怎么理解呢!参数一其实就是一个函数的引用,那么这个函数需要参数怎么办,就通过第二个参数传递!
我们看个例子:这个在Signal也有
static _Unwind_Reason_Code unwindCallback(struct _Unwind_Context *context, void *args) {
BacktraceState *state = static_cast(args);
uintptr_t pc = _Unwind_GetIP(context);
if (pc) {
if (state->current == state->end) {
return _URC_END_OF_STACK;
} else {
*state->current++ = reinterpret_cast(pc);
}
}
return _URC_NO_REASON;
}
size_t captureBacktrace(void **buffer, size_t max) {
BacktraceState state = {buffer, buffer + max};
_Unwind_Backtrace(unwindCallback, &state);
// 获取大小
return state.current - buffer;
}
struct BacktraceState {
void **current;
void **end;
};
我们定义了一个结构体BacktraceState,其实是为了后面记录函数地址而用,这里有两个作用,end代表日志限定的大小,current表示实际日志条数大小(因为堆栈条数可能小于end)
_Unwind_GetIP我们在unwindCallback这里拿到了系统回调给我们的参数,关键就是这个了 _Unwind_Context这个结构体参数了,这个参数的作用就是传递给_Unwind_GetIP这个函数,获取我们当前的执行地址,即pc值!那么这个pc值又有什么用呢!这个就是我们获取堆栈的关键!native堆栈的获取需要地址去解析!(不同于java)我们先有这个概念,后面会继续讲解
dladdr经过了_Unwind_GetIP我们获取了pc值,这个时候就用上dladdr函数去解析了,这个是linux内核函数,专门用于地址符号解析
The function dladdr() determines whether the address specified in
addr is located in one of the shared objects loaded by the
calling application. If it is, then dladdr() returns information
about the shared object and symbol that overlaps addr. This
information is returned in a Dl_info structure:
typedef struct {
const char *dli_fname; /* Pathname of shared object that
contains address */
void *dli_fbase; /* Base address at which shared
object is loaded */
const char *dli_sname; /* Name of symbol whose definition
overlaps addr */
void *dli_saddr; /* Exact address of symbol named
in dli_sname */
} Dl_info;
If no symbol matching addr could be found, then dli_sname and
dli_saddr are set to NULL.
可以看到,每个地址会的解析信息会保存在Dl_info中,如果有运行符号满足,dli_sname和dli_saddr就会被设定为相应的so名称跟地址,dli_fbase是基址信息,因为我们的so库被加载到程序的位置是不固定的!所以一般采用地址偏移的方式去在运行时寻找真正的so库,所以就有这个dli_fbase信息。
Dl_info info;
if (dladdr(addr, &info) && info.dli_sname) {
symbol = info.dli_sname;
}
os
关注
打赏
最近更新
- 深拷贝和浅拷贝的区别(重点)
- 【Vue】走进Vue框架世界
- 【云服务器】项目部署—搭建网站—vue电商后台管理系统
- 【React介绍】 一文带你深入React
- 【React】React组件实例的三大属性之state,props,refs(你学废了吗)
- 【脚手架VueCLI】从零开始,创建一个VUE项目
- 【React】深入理解React组件生命周期----图文详解(含代码)
- 【React】DOM的Diffing算法是什么?以及DOM中key的作用----经典面试题
- 【React】1_使用React脚手架创建项目步骤--------详解(含项目结构说明)
- 【React】2_如何使用react脚手架写一个简单的页面?