스택을 얻는 것이 결코 쉽지 않은 이유

끽끽 우는 소리

글이 너무 지루해 보이지 않게 하기 위해 작가님이 생각을 하시다가 약간의 말을 덧붙였습니다! 이 글을 마지막으로 보낸 이후로, 검은 기술! Native Crash와 ANR이 발산할 곳이 없도록 하십시오! , 독자들에게 큰 인기를 끌고 있는데, 좋아요 수보다 즐겨찾기 수가 도대체 ​​무엇인지 헤헤! 제 입장에서는 Signal 의 원래 목적은 에어백과 유사한 장치를 구축하고 충돌 후 첫 번째 재시작 및 복구를 보장하고 애플리케이션 안정성의 목적을 달성하는 것이 었지만 천천히 작성하고 작성하면서 많은 것을 찾았습니다. 충돌 모니터링 플랫폼도 동일한 핵심 원칙을 사용하지만(대부분 아직 오픈 소스가 아님) 기능의 목적이 다르므로 Signal을 공통 기본 요소로 만들지 않겠습니까! 에어백이든 모니터링이든 사실 상층부 적용이 다릅니다! 음! 이 아이디어를 얻은 후 Signal 에 몇 가지 로그 모니터링 로직을 추가 하면 더 완벽해질 것입니다! 그래서 이 글을! 보충제입니다! 블랙 테크놀로지 를 안 보셨다면 ! Native Crash와 ANR이 발산할 곳이 없도록 하십시오! 이 기사의 새로운 친구는 먼저 읽어주세요! (ndk 개발 경험이 없어도 상관없고, 복잡한 c 지식도 필요 없습니다)

스택 가져오기

스택을 잡아라! 아마도 많은 새로운 친구들이 생각할 것입니다. 이게 뭐가 그렇게 어려운 일입니까! 새로운 Throwable을 직접 얻거나 Thread.currentThread().stackTrace(kotlin) 등을 얻을 수 있습니까? 음! 예! 우리는 일반적으로 Java 가상 머신의 설계와 Java 언어의 설계로부터 이점을 얻는 Java 계층에서 스택을 얻는 매우 고정된 방법을 가지고 있습니다. 현재 스택을 가져오는 비교적 통합된 API입니다. 이 스택은 또한 특히 자바 가상 머신 스택을 참조합니다!

그러나 기본 스택의 경우 문제가 발생합니다! 네이티브 레이어는 일반적으로 링커, 컴파일러 및 다양한 라이브러리 버전, 다양한 abi 등과 같은 많은 요소와 관련되어 있으며 너무 많은 요소가 간섭하기 때문에 스택 메시지를 얻는 것이 그렇게 간단하지 않다는 것을 알고 있습니다. 또한 역사의 부담! 그리고 우리 안드로이드의 경우 안드로이드가 공식적으로 스택을 획득하는 방식에 역사적 변화가 있습니다.

위와 아래에서 안드로이드 네이티브는 시스템과 함께 제공되는 것을 사용하는데 5.0 이후로는 시스템 4.1.1안드로이드 소스 코드의 상위 버전 이 없고 최적화된 버전을 대신 사용합니다 . 동시에 ndk의 경우 컴파일러 버전도 기본 gcc에서 clang으로 계속 변경됩니다(ndk >=13).많은 버전과 많은 요인에서 통일된 방법을 찾을 수 있음을 알 수 있습니다. 또한 정말 쉽지 않습니다! 하지만 그래! 2022년 오늘 Google은 이미 계획된 통합 라이브러리 브레이크패드를 출시했습니다 . 음! 기준이 될 수 있을지는 아직 미정이지만 생태학적 진보이기도 하다.5.0libcorkscrew.solibcorkscrew.solibunwind

시그널의 선택

앞서 소개된 솔루션이 너무 많기 때문에 브레이크패드가 Signal의 첫 번째 선택입니까? breakpad는 훌륭하지만 mac, window 및 기타 표준과 같은 너무 많은 다른 시스템의 컴파일을 다루며 오픈 소스 라이브러리로서 여전히 이러한 라이브러리의 가져오기를 줄이기를 희망하므로 대부분의 주류 솔루션과 마찬가지로 unwind.h를 사용하여 스택 인쇄를 구현하도록 선택하십시오. 이는 기본 컴파일에 직접 내장되어 있고 Android에서도 사용할 수 있기 때문입니다! 아래의 구현을 살펴보자! 즉, Signal 프로젝트의 unwind-utils 구현입니다. 그래서 우리는 무엇을 고려해야 합니다!

스택 크기

물론 로그는 트레이스백 스택 크기를 설정해야 하며, 콘텐츠가 너무 많으면 좋지 않고(너무 부풀려 문제 해결이 어렵습니다) 콘텐츠가 너무 적으면 좋지 않습니다(핵심 충돌 스택을 놓칠 가능성이 매우 높음). 기본값은 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_역추적

_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<BacktraceState *>(args);
    uintptr_t pc = _Unwind_GetIP(context);
    if (pc) {
        if (state->current == state->end) {
            return _URC_END_OF_STACK;
        } else {
            *state->current++ = reinterpret_cast<void *>(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 << " #" << idx << ": " << addr << " " <<"  "<<symbol <<"\n" ;

最终我们可以通过dladdr,一一把保存的地址信息解析出来,打印到native日志中比如Signal中demo crash信息(如果需要打印so名称,也可以通过dli_fname去获取,这里不举例)

이미지.png

native堆栈产生过程

通过上面的日志分析(最好看下demo中的app演示crash),我们其实在MainActivity中设定了一个crash函数

private external fun throwNativeCrash()

按照堆栈日志分析来看,只有在第16条才出现了调用符号,这跟我们在日常java开发中是不是很不一样!因为java层的堆栈一般都是最近的堆栈消息代表着错误消息,比如应该是第0条才导致的crash,但是演示中真正的堆栈crash却隐藏在了日志海里面!相信有不少朋友在看native crash日志也是,是不是也感到无从下手,因为首条日志往往并不是真正crash的主因!我们来看一下真正的过程:我们程序从正常态到crash,究竟发生了什么!

이미지.png

可以看到,我们真正dump_stack前,是有很多前置的步骤,为什么会有这么多呢!其实这就涉及到linux内核中断的原理,这里给一张粗略图

이미지.png crash产生后,一般会在用户态阶段调用中断进入内核态,把自己的中断信号(这里区分一下,不是我们signal.h里面的信号)放在eax寄存器中(大部分,也有其他的寄存器,这里仅举例)

然后内核层通过传来的中断信号,找到信号表,然后根据对应的处理程序,再抛回给用户态,这个时候才进行sigaction的逻辑

所以说,crash产生到真正dump日志,其实会有一个过程,这里面根据sigaction的设置也会有多个变化,我们要了解的一点是,真正的crash信息,往往藏在堆栈海中,需要我们一步步去解析,比如通过addr2line等工具去分析地址,才能得到真正的原因,而且一般的android项目,都是依赖于第三方的so,这也给我们的排查带来难度,不过只要我们能识别出特定的so(dli_fname信息就有),是不是就可以把锅甩出去了呢,对吧!

마침내

이것을 보고 독자와 친구들은 기본 스택의 대략적인 모델을 갖게 될 것입니다. 물론 두려워하지 마십시오! Signal 프로젝트에는 직접 사용할 수 있는 관련 unwind-utils 도구 클래스가 포함되어 있지만 현재 인쇄되는 정보는 비교적 간단하며 실제 상황에 따라 매개변수를 추가할 수 있습니다! 코드는 모두 내부에 있습니다. 별을 요청하고 홍보를 요청하십시오! 시그널 은 물론 이 글을 읽으신 후 좋아요와 댓글도 잊지 말고 남겨주세요!

과거에 추천

Compose와 RecyclerView의 조합이 불편할 것이라고 들었습니다.

kts로 마이그레이션된 Android gradle

Nuggets Technology Community의 작성자 서명 프로그램 모집에 참여하고 있습니다. 링크를 클릭하여 등록하고 제출 하십시오.

рекомендация

отjuejin.im/post/7118609781832548383
рекомендация