Android崩潰日志獲取與解析


  在程序界面有一句話很流行,那就是不要重復造輪子。現在市面上有很多的崩潰日志抓取工具,比如騰訊的bugly,不管是eclipse還是Android Studio,集成都是非常簡單,他可以抓取到JAVA的崩潰,同樣也可以抓取到NDK代碼的崩潰。

  Java的崩潰就沒有什么好說的,集成的步驟以及實現的原理太簡單,下面我們來看看如何集成NDK崩潰的抓取

  1. 首先在c/c++代碼的任意位置添加代碼const char SO_FILE_VERSION[]  __attribute__ ((section (".bugly_version"))) = "1.0.1",注意,如果是cpp文件得話必須加上extern "C",這一點騰訊給出來的文檔里面沒有說明。如果不加的話,我們編譯出來的動態庫是沒有版本號信息的,為什么是.bugly_version,這個只有騰訊知道,我們知道騰訊定義了這個一個符號,用來讀取出動態庫的版本號來。
  2. 編譯出so后,我們可以查看版本號,使用NDK工具arm-linux-androideabi-readelf.exe來讀取。命令行工具下運行arm-linux-androideabi-readelf.exe -p .bugly_version libXXXX.so就可以讀取到第一步中寫入的version name了。Windows系統arm-linux-androideabi-readelf.exe的路徑為android-ndk-r10e\toolchains\arm-linux-androideabi-4.x\prebuilt\windows-x86_64\bin。
  3. 從編譯結果的obj目錄取出對應的動態庫,利用騰訊提供的批處理文件生成map文件,並將map文件上傳給騰訊,這樣我們的apk里面發布的動態庫是release版本的,但是通過騰訊的網頁看到的堆棧還是可以定位到崩潰代碼的行數以及所在的文件。

  集成就這么結束了,還是很簡單的,如下是我測試程序產生的崩潰日志。非常清晰的可以看到代碼崩潰的位置、動態庫支持的平台、動態庫的版本

#00 pc 00000cd4 libErrorReport.so crash (E:/workspace/BuglyErrorTest/app/src/main/jni/ErrorReport.cpp:14-16) [armeabi-v5te] [1.0.1]
#01 pc 00000cdb libErrorReport.so Java_com_openflight_bugly_JniTest_nativeCrash (E:/workspace/BuglyErrorTest/app/src/main/jni/ErrorReport.cpp:21) [armeabi-v5te] [1.0.1]
#02 pc 000d3051  /data/dalvik-cache/arm/data@app@com.openflight.bugly-2@base.apk@classes.dex

  bugly的集成就講到這里了,那么問題來了,bugly是怎么做到的呢?以上都是基於我們的APP運行在有網絡的環境下,那么如果我們開發的APP是要運行在一個沒有網絡的環境中呢,怎么辦?怎么辦?怎么辦?很悲劇,本人是做車載導航開發的,而很多車載設備是沒有網絡的,那么就只能是抓取log保存在本地,然后取出對應的log來給開發人員分析了。JAVA的崩潰很好辦,我們實現一下UncaughtExceptionHandler,然后將crash信息保存到本地的文件中就好了。那么NDK的崩潰呢?我們要感謝偉大的google把google-breakpad開源出來了。那接下來我們來看一下breakpad的集成吧。本例采用eclipse工程的方式,為啥不用Android Studio?因為breakpad給android提供的編譯方式就是使用mk文件來編譯的,而且個人感覺用eclipse來做NDK開發更方便。

  先看一下jni目錄的結構

  

  google-breakpad-master中有提供一個android的例子,先不管那么多,直接進入到例子的目錄,ndk-build一下再說,什么?編譯不過,怎么可能,google一下為什么,好吧,真的是編譯不過,為什么,因為谷歌的工程師在寫breakpad的mk文件得時候居然漏了一些東西,好吧,我們都給補上。修改google-breakpad-master/android/google-breakpad/Android.mk,把LOCAL_SRC_FILES修改為

LOCAL_SRC_FILES := \
    src/client/linux/crash_generation/crash_generation_client.cc \
    src/client/linux/handler/exception_handler.cc \
    src/client/linux/handler/minidump_descriptor.cc \
    src/client/linux/log/log.cc \
    src/client/linux/minidump_writer/linux_dumper.cc \
    src/client/linux/minidump_writer/linux_ptrace_dumper.cc \
    src/client/linux/minidump_writer/minidump_writer.cc \
    src/client/linux/microdump_writer/microdump_writer.cc \
    src/client/linux/dump_writer_common/ucontext_reader.cc \
    src/client/linux/dump_writer_common/seccomp_unwinder.cc \
    src/client/linux/dump_writer_common/thread_info.cc \
    src/client/minidump_file_writer.cc \
    src/common/android/breakpad_getcontext.S \
    src/common/convert_UTF.c \
    src/common/md5.cc \
    src/common/string_conversion.cc \
    src/common/linux/elfutils.cc \
    src/common/linux/file_id.cc \
    src/common/linux/guid_creator.cc \
    src/common/linux/linux_libc_support.cc \
    src/common/linux/memory_mapped_file.cc \
    src/common/linux/safe_readlink.cc

  好了,再來一次ndk-build,這次沒有問題了,可以正常編譯,把編譯的結果push到手機上,運行一下,生成了一個dmp文件,恩,這個dmp文件就是我們要的東西了。可以開始移植了,jni的代碼和mk文件也都非常簡單,直接貼出來

  Application.mk

APP_STL := stlport_static
APP_ABI := armeabi

  Android.mk

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_MODULE    := ErrorReport
LOCAL_SRC_FILES := ErrorReport.cpp
LOCAL_STATIC_LIBRARIES += breakpad_client
LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog
LOCAL_STATIC_LIBRARIES += libgcc

include $(BUILD_SHARED_LIBRARY)

ifneq ($(NDK_MODULE_PATH),)
  $(call import-module,google_breakpad)
else
  include $(LOCAL_PATH)/google-breakpad-master/android/google_breakpad/Android.mk
endif

  ErrorReport.cpp

#include <jni.h>
#include <stdio.h>
#include <android/log.h>

#include "google-breakpad-master/src/client/linux/handler/exception_handler.h"
#include "google-breakpad-master/src/client/linux/handler/minidump_descriptor.h"

#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, "ErrorReport", __VA_ARGS__)

extern "C" {
    JNIEXPORT jint JNICALL Java_com_openflight_errorreport_CrashHandler_nativeCrash(
            JNIEnv* env, jobject thiz);
    JNIEXPORT jint JNICALL Java_com_openflight_errorreport_CrashHandler_setNativeCrashDir(
            JNIEnv* env, jobject thiz, jstring path);
}

static google_breakpad::ExceptionHandler *handler = NULL;
bool DumpCallback(const google_breakpad::MinidumpDescriptor& descriptor,
                  void* context,
                  bool succeeded) {
    LOGD("Dump path: %s", descriptor.path());
    return succeeded;
}

void crash(){
    volatile int* a = reinterpret_cast<volatile int*>(NULL);
    *a = 1;
}

JNIEXPORT jint Java_com_openflight_errorreport_CrashHandler_nativeCrash(
            JNIEnv* env, jobject thiz){
    crash();
}

// 設置dmp文件保存路徑
JNIEXPORT jint JNICALL Java_com_openflight_errorreport_CrashHandler_setNativeCrashDir(
        JNIEnv* env, jobject thiz, jstring path){
    const char *filePath = env->GetStringUTFChars(path, 0);
    google_breakpad::MinidumpDescriptor descriptor(filePath);
    handler = new google_breakpad::ExceptionHandler(descriptor, NULL, DumpCallback, NULL, true, -1);
}

  其中有幾個需要注意的地方,大家可能看到Android.mk中加了一句LOCAL_STATIC_LIBRARIES += libgcc,這一句在例子中是沒有的,為啥呢,因為本人手上兩台手機,N5以及Note2,如果如加這一句的話Note2一運行就崩潰。為啥要用static google_breakpad::ExceptionHandler *handler呢,例子里面是直接在main函數里面聲明並初始化的,好吧,我最開始也是這么認為的,直接放在Jni_OnLoad里面,結果dmp文件無法生成。因為例子中crash是在main函數里面的,handler的作用域是整個main函數,所以他可以生成。如果你把這段代碼放到Jni_OnLoad中,那么他的作用域也是Jni_OnLoad,Jni_OnLoad返回之后就沒有效果了,這顯然不是我們想要的,我們希望它的作用域跟APP的生命周期是一樣的,所以把他定義為static。

  工作都做完了,拿台手機來跑吧,調用crash方法會崩潰,崩潰之后我們可以看到在我們設置的目錄下面已經有一個dmp文件生成了,那dmp文件這么解析呢?windows實在是太不方便了, 下面還是給出ubuntu系統的解析方法吧。

  1. 下載google-breakpad源代碼,編譯linux版本,找到以下兩個文件
    • google-breakpad-read-only/src/tools/linux/dump_syms/dump_syms
    • google-breakpad-read-only/src/processor/minidump_stackwalk
  2. 任意位置建立一個文件夾,文件夾中包含dump_syms、minidump_stackwalk、*.dmp、所有的動態庫文件(多個動態庫需要重復3、4步驟)
  3. 執行命令./dump_syms libXXX.so > libXXX.so.sym
  4. 創建文件夾 symbols/libgame.so/6D7D7B4FAAE9D2686CF45FA12A9E3AD30,並將生成的libXXX.so.sym拷貝到該文件夾中,6D7D7B4FAAE9D2686CF45FA12A9E3AD30為生成的libXXX.so.sym的第一行的內容
  5. 執行命令./minidump_stackwalk XXXXXXXXXXX.dmp symbols/ > result.txt

  大功告成,我們來看一下解析出來的result.txt里面崩潰的堆棧

Thread 0 (crashed)
 0  libErrorReport.so!_Z5crashv + 0x3
     r0 = 0x41803818    r1 = 0x59600019    r2 = 0x00000001    r3 = 0x00000000
     r4 = 0x00000000    r5 = 0x57ac67b8    r6 = 0x00000000    r7 = 0x57765dac
     r8 = 0xbea0a510    r9 = 0x57765da4   r10 = 0x57765da0   r12 = 0x5e0b6555
     fp = 0xbea0a524    sp = 0xbea0a508    lr = 0x5e0b655b    pc = 0x5e0b6550
    Found by: given as instruction pointer in context

  這是什么鬼,_Z5crashv是什么東西?應該是armv5架構的CPU,crash函數中崩潰,而且該函數的返回值是void,就這些信息,如果NDK代碼多的話, 還是很難定位到崩潰的行數,而且0x3還需要通過idaq之類的軟件去解析一下,才能定位到行數 ,意義不是很大吶,好吧,這個是從libs中取出來的so庫,那么我們試試obj中取出來的so去解析結果是怎么樣的。

Thread 0 (crashed)
 0  libErrorReport.so!crash [ErrorReport.cpp : 27 + 0x4]
     r0 = 0x41803818    r1 = 0x59600019    r2 = 0x00000001    r3 = 0x00000000
     r4 = 0x00000000    r5 = 0x57ac67b8    r6 = 0x00000000    r7 = 0x57765dac
     r8 = 0xbea0a510    r9 = 0x57765da4   r10 = 0x57765da0   r12 = 0x5e0b6555
     fp = 0xbea0a524    sp = 0xbea0a508    lr = 0x5e0b655b    pc = 0x5e0b6550
    Found by: given as instruction pointer in context
 1  libErrorReport.so!Java_com_openflight_errorreport_CrashHandler_nativeCrash [ErrorReport.cpp : 32 + 0x3]
     r4 = 0x00000000    r5 = 0x57ac67b8    r6 = 0x00000000    r7 = 0x57765dac
     r8 = 0xbea0a510    r9 = 0x57765da4   r10 = 0x57765da0    fp = 0xbea0a524
     sp = 0xbea0a508    pc = 0x5e0b655b
    Found by: call frame info

  這個結果就比較完美了,c++中崩潰的堆棧都有了,哪個文件哪一行都有,但是呢,跟bugly比起來還是差了一些,因為bugly中可以看到java層調用的堆棧。結果是比較完美了,可是可是可是操作還是麻煩了一點,如果我的應用有很多個動態庫的話,那解析一個dmp文件就要好久了,很不幸,我曾經開發的應用里面包含了將近10個庫,天啊,這就算再熟練解析一個文件也要好幾分鍾啊,而且中途還不能出錯,殺了我吧,我不干了。我的理念是,凡是重復的工作都讓電腦去做,想了多種方法,感覺最簡單的還是shell腳本比較簡單,所以就這么干了,腳本的代碼也直接放出來。

#!/bin/bash

#[usage]
#將本腳本、dump_sys、minidump_stackwalk放在同級目錄下,並創建libs文件夾,所有動態庫放到libs文件夾內
#./dump2txt.sh [dmp文件路徑] [生成的txt文件路徑]

LIBRARY_DIRECTORY="libs"
LIBRARY_EXTENDNAME=".sym"
LIBRARY_KEYPOS=3

Check()
{
    if [ $# -ne 2 ];then
        echo please input two param,the first param is the dmp file path,the second param is txt file path
        exit
    fi

    if [ ! -f "$1" ]; then
        echo $1 is not exsit
        exit
    fi
}

DealLibrary()
{
    if [ ! -f "$LIBRARY_DIRECTORY/$1" ]; then
        echo $LIBRARY_DIRECTORY/$1 is not exsit
        return
    fi

    SYM_NAME=$1$LIBRARY_EXTENDNAME
    ./dump_syms libs/$1 > $SYM_NAME
    
    cat $SYM_NAME | while read line
    do
        LIBRARY_CODE=$line
        ARR=($LIBRARY_CODE)
        LIBRARY_CODE="${ARR[$LIBRARY_KEYPOS]}"
        mkdir -p symbols/$1/$LIBRARY_CODE
        mv $SYM_NAME symbols/$1/$LIBRARY_CODE
        break
    done
}

Main()
{
    #檢查參數 $1:dmp文件路徑;$2:生成的txt文件的路徑
    Check $1 $2
    echo "start convert "$1" to "$2"...."

    #創建解析dmp文件相關的目錄以及文件
    rm -rf symbols
    for file in $LIBRARY_DIRECTORY/*
    do
        DealLibrary ${file:5}
    done

    #生成txt文件
    ./minidump_stackwalk $1 symbols/ > $2
    echo $2 is generated!!!!
}
Main $1 $2

  好了,寫到這里本文也就接近尾聲了,shell腳本怎么在windows中運行?不好意思,沒辦法,只能在linux下才能運行,裝個Ubuntu的虛擬機吧。其實android手機也是可以的,但是minidump_stackwalk和dump_syms這兩個文件我們只編譯了linux版本的,要在手機上運行的話還需要armeabi版本的才行,本人沒有研究過,有興趣的同學可以研究一下。其實windows系統上也是可以的,如果編寫一個windows的C/S程序給測試的同學使用還是蠻方便的,但是工作量比較大,本人沒有研究,有興趣的同學也可以研究一下。

2015.12.27

------End------


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM