スタックを取得するのが簡単ではなかった理由

おしゃべり

記事が退屈に見えないようにするために、著者はそれについて考え、少し話を追加しました!この記事が最後に送られた時から、ブラックテクノロジー!Native CrashとANRにベントする場所がないようにしましょう!、それは読者に非常に人気があります、何が好きな数よりもお気に入りの数が多いのですか、hehe!私の見解では、Signalの本来の目的は、エアバッグに似たデバイスを構築し、クラッシュ後の最初の再起動と回復を確実にし、アプリケーションの安定性の目的を達成することでしたが、ゆっくりと書き込みと書き込みを行うと、多くのことがわかりました。クラッシュモニタリングの概要プラットフォームも同じコア原則を使用していますが(それらのほとんどはまだオープンソースではありません)、機能の目的は異なります。Signalを一般的な基本部分にしてみませんか。エアバッグであろうとモニタリングであろうと、実際、上層の用途は異なります!えーと!このアイデアを思いついた後、 Signalにログ監視ロジックを追加すると、より完璧になります。したがって、この記事!サプリです!ブラックテクノロジーを見たことがないなら!Native CrashとANRにベントする場所がないようにしましょう!この記事の新しい友達、最初に読んでください!(ndk開発の経験がなくても問題ありません。また、非常に複雑なcの知識も必要ありません)

スタックを取得

スタックを入手してください!たぶん、多くの新しい友達が考えるでしょう、これについてとても難しいことは何ですか!新しいThrowableを直接取得することはできますか、またはThread.currentThread()。stackTrace(kotlin)などを取得できますか?えーと!はい!通常、Javaレイヤーでスタックを取得する方法は非常に固定されており、Java仮想マシンの設計とJava言語の設計の恩恵を受けています。マルチプラットフォームの下部の違いは保護されているため、次を使用できます。現在のスタックを取得するための比較的統一されたAPI。このスタックは、特にJava仮想マシンスタックも指します。

しかし、ネイティブスタックの場合、問題が発生します。ネイティブレイヤーは通常、リンカー、コンパイラ、さまざまなライブラリバージョン、さまざまなabiなどの多くの要因に関連していることがわかっています。また、干渉する要因が多すぎるため、スタックメッセージを取得するのはそれほど簡単ではありません。歴史の重荷でもあります!また、Androidの場合、Androidが正式にスタックを取得する方法に歴史的な変更があります。

4.1.1上記および下記で5.0、Androidネイティブはシステムに付属しているものを使用しますlibcorkscrew.so。5.0以降、システムlibcorkscrew.soにはAndroidソースコードの上位バージョンはなく、代わりに最適化されたバージョンが使用されますlibunwind同時に、ndkの場合、コンパイラのバージョンもデフォルトのgccからclangに絶えず変化しています(ndk> = 13)。多くのバージョンと多くの要因の下で統一された方法が見つかることがわかります。また、それは本当に簡単ではありません!しかし、ええ!今日、2022年に、Googleはすでに計画された統合ライブラリブレークパッドを立ち上げました。それが標準になることができるかどうかはまだ決定されていませんが、それは生態学的な進歩でもあります

シグナルの選択

以前に紹介された非常に多くのソリューションで、breakpadはSignalの最初の選択肢ですか?ブレークパッドは優れていますが、mac、window、その他の標準など、他の多くのシステムのコンパイルをカバーしています。オープンソースライブラリとして、これらのライブラリのインポートを減らしたいと考えています。ほとんどの主流のソリューションと同様に、 unwind.hを使用してスタック印刷を実装することを選択します。これは、デフォルトのコンパイルに直接組み込まれており、Androidでも使用できるためです。以下の実装を見てみましょう!つまり、Signalプロジェクトのunwind-utilsの実装です。では、何を考慮すべきでしょうか。

スタックサイズ

もちろん、ログでトレースバックスタックサイズを設定する必要があります。コンテンツが多すぎると(肥大化してトラブルシューティングが困難になり)、コンテンツが少なすぎると(キークラッシュスタックを見逃す可能性が非常に高くなります)、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<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去获取,这里不举例)

image.png

native堆栈产生过程

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

private external fun throwNativeCrash()

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

image.png

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

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

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

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

やっと

これを見て、読者や友人は、もちろん、ネイティブスタックの大まかなモデルを持っているはずです。恐れることはありません!Signalプロジェクトには、直接使用できる関連するunwind-utilsツールクラスが含まれていますが、現在印刷されている情報は比較的単純であり、実際の状況に応じてパラメーターを追加できます。コードはすべて内部にあり、スターを要求し、PRを要求します!もちろん、 Signalは、この記事を読んだ後、いいねやコメントを残すことを忘れないでください!

過去に推奨

ComposeとRecyclerViewの組み合わせは不快だと聞きましたが?

Androidgradleがktsに移行されました

ナゲッツテクノロジーコミュニティのクリエイター署名プログラムの募集に参加しています。リンクをクリックして登録し、送信してください。

おすすめ

転載: juejin.im/post/7118609781832548383