如何調試Android Native Framework

qqxiaoming 7年前發布 | 10K 次閱讀 安卓開發 Android開發 移動開發

半年前寫了一篇文章,介紹 如何調試Android Framework ,但是只提到了Framework中Java代碼的調試辦法,但實際上有很多代碼都是用C++實現的;無奈當時并并沒有趁手的native調試工具,無法做到像Java調試那樣簡單直觀(gdb+eclipse/ida之流雖然可以但是不完美),于是就擱置下了。

Android Studio 2.2版本帶來了全新的對Android Native代碼的開發以及調試支持,另外LLDB的Android調試插件也日漸成熟,我終于可以把這篇文章繼續下去了!本文將帶來Android Framework中native代碼的調試方法。

在正式介紹如何調試之前,必須先說明一些基本的概念。調試器在調試一個可執行文件的時候,必須知道一些調試信息才能進行調試,這個調試信息可多可少(也可以沒有)。最直觀的比如行號信息,如果調試器知道行號信息,那么在進行調試的時候就能知道當前執行到了源代碼的哪一行,如果調試器還知道對應代碼的源文件在哪,那么現代IDE的調試器一般就能順著源碼帶你飛了,這就是所謂的源碼調試。相反,如果沒有行號和源碼信息,那么只能進行更低級別的調試了,調試器只能告訴你一些寄存器的值;而當前運行的代碼也只是PC寄存器所指向的二進制數據,這些數據要么是虛擬機指令,要么是匯編指令;這就是所謂的無源碼調試。顯然無源碼調試相比源碼級別的調試要麻煩的多;接下來將圍繞這兩個方面分別介紹。

用Android Studio進行源碼調試

如上文所述,如果需要實現源碼調試,必須知道足夠的調試信息;在native調試中就是所謂的「調試符號」。但是release版本的動態鏈接庫或者可執行文件一般并不會包含我們需要的調試信息,在Android系統中, /system/lib/* 目錄下的那些系統so并沒有足夠的調試信息,因此如果要進行源碼調試,必須自己編譯Android源代碼,才能獲取調試信息,進而讓調試器協助我們調試。

Android源碼編譯是個麻煩事兒,我寫過一篇文章介紹 如何使用Docker調試 ;但是,Android版本眾多,如果真的需要調試各個版本,在本地進行編譯幾乎是不可能的——一個版本約占60G空間,如果每個版本都編譯,你的Mac還有空間可用嗎?因此比較推薦使用云服務進行源碼編譯;比如使用阿里云的ECS,20M的網速15分鐘就能下載完源碼;編譯速度還勉強,4核8G兩個半小時。扯遠了 :) 如果你沒有精力編譯Android源碼,我這個 Demo工程 可以讓你嘗嘗鮮,里面包含一些調試的必要文件,可以體會一下Native調試的感覺。

如果我們已經擁有了調試符號,那么還需要保證你的符號文件和設備上真正運行的動態鏈接庫或者可執行文件是對應的,不然就是雞同鴨講了。最簡單的辦法就是使用模擬器。我們編譯完源碼之后,一個主要的編譯產物就是 system.img ,這個 system.img 會在啟動之后掛載到設備的 /system 分區,而system分區包含了Android系統運行時的絕大部分可執行文件和動態鏈接庫,而這些文件就是我們的編譯輸出,正好可以與編譯得到的調試符號進行配合調試。模擬器有一個 -system 選項用來指定模擬器使用的 system.img文件;于是這個問題也解決了。

最后一個問題就是,既然是源碼調試,當然需要源碼了;我們可以在 AOSP 上下載需要的源碼即可;需要注意的是,在check分支的時候,必須保證你的分支和編譯源碼時候的分支是一致的。

需要說明的是,雖然我們使用Android Studio調試,但是其背后的支撐技術實際上是 LLDB 。LLDB是一個相當強大的調試器,如果你現在還不知道它為何物,那真的是孤陋寡聞了!建議先簡單學習一下 教程

萬事俱備,Let’s go!

建立Android Studio工程

實際上任何Android Studio工程都可以進行native源碼調試,但是為了方便還是新建一個工程;這個工程是一個空工程,沒有任何實際用途;為了體驗方便,你可以使用我的這個 Demo 工程,里面包含了調試符號以及模擬器需要使用的system.img。一定要注意Android Studio的版本必須是2.2以上(我的是2.2.3穩定版)。

下載需要調試模塊的源碼

如果你本地編譯了Android源碼,那么就不需要這一步了;但是更多的時候我們只是想調試某一個模塊,那么只需要下載這個模塊的源碼就夠了。我這里演示的是調試 ART 運行時,因此直接下載ART模塊的源碼即可,我編譯的Android源碼版本是 android-5.1.1_r9 ,因此需要check這個分支的源碼,地址在這里: ART-android-5.1.1_r9

運行模擬器

由于我們的調試符號需要與運行時的動態鏈接庫對應,因此我們需要借助模擬器;首先創建一個編譯出來的調試符號對應的API版本的模擬器,我這里提供的是5.1.1也就是API 22;然后使用編譯出來的 system.img 啟動模擬器([Demo]工程的image目錄有我編譯出來的文件,可以直接使用。):

emulator -avd 22 -verbose -no-boot-anim -system /path/to/system.img

這個過程灰常灰常長!!我啟動這個模擬器花了半個多小時,也是醉。現在是2017年,已經是Android創建的第十個年頭,ARM模擬器還是爛的一塌糊涂,無力吐槽。一個能讓它快一點的訣竅是創建一個小一點的SD card;我設置的是10M。

開始調試

選擇native調試模式

首先我們對調試的宿主工程設置一下,選擇native調試功能。點擊運行下面的按鈕 Edit Configuration :

然后在debugger欄選擇Native:

然后我們點擊旁邊的 Debug 小按鈕運行調試程序:

設置調試符號以及關聯源碼

在運行程序之后,我們可以在Android Studio的狀態欄看到,LLDB調試插件自動幫我們完成了so查找路徑的過程,這一點比gdb方便多了!在Android Studio的Debug窗口會自動彈出來,如下:

我們點擊那個 pause program 按鈕,可以讓程序暫停運行:

上圖左邊是正在運行的線程的堆棧信息,右邊有兩個tab,一個用來顯示變量的值;一個是lldb交互式調試窗口!我們先切換到lldb窗口,輸入如下命令設置一個斷點:

(lldb) br s -n CollectGarbageInternal

Breakpoint 2: where = libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool), address = 0xb4648c20

可以看到,斷點已經成功設置;這個斷點在libart.so中,不過現在還沒有調試符號信息以及源碼信息,我們只知道它的地址。接下來我們設置調試符號以及關聯源碼。

接下來我們把編譯得到的符號文件 libart.so 告訴調試器(符號文件和真正的動態鏈接庫這兩個文件名字相同,只不過一個在編譯輸出的symbols目錄) ;在lldb窗口執行:

(lldb) add-dsym /Users/weishu/dev/github/Android-native-debug/app/symbols/libart.so
symbol file '/Users/weishu/dev/github/Android-native-debug/app/symbols/libart.so' \
has been added to '/Users/weishu/.lldb/module_cache/remote-android/.cache/C51E51E5-0000-0000-0000-000000000000/libart.so'

注意后面那個目錄你的機器上與我的可能不同,需要修改一下。我們再看看有什么變化,看一下剛剛的斷點:

(lldb) br list 2

2: name = ‘CollectGarbageInternal’, locations = 1, resolved = 1, hit count = 0

2.1: where = libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool) at heap.cc:2124 , address = 0xb4648c20, resolved, hit count = 0

行號信息已經加載出來了!!在 heap.cc 這個文件的第2124行。不過如果這時候斷點命中,依然無法關聯到源碼。我們看一下調試器所所知道的源碼信息:

(lldb) source info

Lines found in module `libart.so

[0xb4648c20-0xb4648c28): /Volumes/Android/android-5.1.1_r9/art/runtime/gc/heap.cc :2124

納尼??這個目錄是個什么鬼,根本沒有這個目錄好伐?難道是調試器搞錯了?

在繼續介紹之前我們需要了解一些關于「調試符號」的知識;我們拿到的調試符號文件其實是一個DWARF文件,只不過這個文件被嵌入到了ELF文件格式之中,而其中的調試符號則在一些名為 .debug_* 的段之中,我們可以用 readelf -S libart.so 查看一下:

編譯器在編譯libart.so的時候,記錄下了 編譯時候 源代碼與代碼偏移之間的對應關系,因此調試器可以從調試符號文件中獲取到源碼行號信息;如下:

這下我們明白了上面那個莫名其妙的目錄是什么了;原來是在編譯 libart.so 的那個機器上存在源碼。那么問題來了,我們絕大多數情況下是使用另外一臺機器上的源碼進行調試的——比如我提供的那個 Demo工程 包含的帶符號libart.so里面保存的源文件信息的目錄實際上是我編譯的電腦上的目錄,而你調試的時候需要使用自己電腦上的目錄。知道了問題所在,解決就很簡單了,我們需要映射一下;在Android Studio的Debug 窗口的lldb 那個tab執行如下命令:

(lldb) settings set target.source-map /Volumes/Android/android-5.1.1_r9/ /Users/weishu/dev/github/Android-native-debug/app/source/

第一個參數的意思是編譯時候的目錄信息,第二個參數是你機器上的源碼存放路徑;設置成自己的即可。

這時候,我們再觸發斷點(點擊demo項目的Debug按鈕),看看發生了什么?!

至此,我們已經成功滴完成了在Android Studio中Native代碼的源碼調試。你可以像調試Java代碼一樣調試Native代碼,step/in/out/over,條件斷點,watch point任你飛。你可以借助這個工具去探究Android底層運行原理,比如垃圾回收機制,對象分配機制,Binder通信等等,完全不在話下!

無源碼調試

接下來再介紹一下操作簡單但是使用門檻高的「無源碼調試」方式;本來打算繼續使用Android Studio的,但是無奈現階段還有BUG,給官方提了issue但是響應很慢: https://code.google.com/p/android/issues/detail?id=231116。因此我們直接使用 LLDB 調試;當然,用gdb也能進行無源碼調試,但是使用lldb比gdb的步驟要簡單得多;不信你可以看下文。

安裝Android LLDB工具

要使用lldb進行調試,首先需要在調試設備上運行一個lldb-server,這個lldb-server attach到我們需要調試的進程,然后我們的開發機與這個server進行通信,就可以進行調試了。熟悉gdb調試的同學應該很清楚這一點。我們可以用Android Studio直接下載這個工具,打開SDK Manager:

如上圖,勾選這個即可;下載的內容會存放到你的 $ANDROID_SDK/lldb 目錄下。

使用步驟

安裝好必要的工具之后,就可以開始調試了;整體步驟比較簡單:把lldb-server推送到調試設備并運行這個server,在開發機上連上這個server即可;以下是詳細步驟。

在手機端運行lldb-server

如果你的調試設備是root的,那么相對來說比較簡單;畢竟我們的調試進程lldb-server要attach到被調試的進程是需要一定權限的,如果是root權限那么沒有限制;如果沒有root,那么我們只能借助 run-as 命令來調試自己的進程;另外,被調試的進程必須是debuggable,不贅述。以下以root的設備為例(比如模擬器)

  1. 首先把lldb-server push到調試設備。lldb-sever這個文件可以在 `$ANDROID_SDK/lldb/<版本號數字>/android/ 目錄下找到,確認你被調試設備的CPU構架之后選擇你需要的那個文件,比如大多數是arm構架,那么執行:

    adb push lldb-server /data/local/tmp/

  2. 在調試設備上運行lldb-server。

    adb shell /data/local/tmp/lldb-server platform \

    –server –listen unix-abstract:///data/local/tmp/debug.sock

    如果提示 /data/local/tmp/lldb-server: can’t execute: Permission denied,那么給這個文件加上可執行權限之后再執行上述命令:

    adb shell chmod 777 /data/local/tmp/lldb-server

    這樣,調試server就在設備上運行起來了,注意要這么做需要設備擁有root權限,不然后面無法attach進程進行調試;沒有root權限另有辦法。另外,這個命令執行之后所在終端會進入阻塞狀態,不要管它,如下進行的所有操作需要重新打開一個新的終端。

連接到lldb-server開始調試

首先打開終端執行lldb(Mac開發者工具自帶這個,Windows不支持),會進入一個交互式的環境,如下圖:

  1. 選擇使用Android調試插件。執行如下命令:

    platform select remote-android

    如果提示沒有Android,那么你可能需要升級一下你的XCode;只有新版本的lldb才支持Android插件。

  2. 連接到lldb-server

    這一步比較簡單,但是沒有任何官方文檔有說明;使用辦法是我查閱Android Studio的源碼學習到的。如下:

    platform connect unix-abstract-connect:///data/local/tmp/debug.sock

    正常情況下你執行lldb-server的那個終端應該有了輸出:

  3. attach到調試進程。首先你需要查出你要調試的那個進程的pid,直接用ps即可;打開一個新的終端執行:

    ~ adb shell ps | grep lldbtest

    u0_a53 2242 724 787496 33084 ffffffff b6e0c474 S com.example.weishu.lldbtest

    我要調試的那個進程pid是 2242 ,接下來回到lldb的那個交互式窗口執行:

    process attach -p 2242

    如果你的設備沒有root,那么這一步就會失敗——沒有權限去調試一個別的進程;非root設備的調試方法見下文。

    至此,調試環境就建立起來了。不需要像gdb那樣設置端口轉發,lldb的Android調試插件自動幫我們處理好了這些問題。雖然說了這么多,但是你熟練之后真正的步驟只有兩步,灰常簡單。

  4. 斷點調試

    調試環境建立之后自然就可以進行調試了,如果進行需要學習lldb的使用方法;我這里先演示一下,不關心的可以略過。

    1. 首先下一個斷點:

      (lldb) br s -n CollectGarbageInternal

      Breakpoint 1: where = libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool), address = 0xb4648c20

    2. 觸發斷點之后,查看當前堆棧:

      (lldb) bt
      * thread #8: tid = 2254, 0xb4648c20 libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool), name = 'GCDaemon', stop reason = breakpoint 1.1
      * frame #0: 0xb4648c20 libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool)
      frame #1: 0xb464a550 libart.so`art::gc::Heap::ConcurrentGC(art::Thread*) + 52
      frame #2: 0x72b17161 com.example.weishu.lldbtest
      
    3. 查看寄存器的值

          (lldb) reg read
      General Purpose Registers:
         r0 = 0xb4889600
         r1 = 0x00000001
         r2 = 0x00000001
         r3 = 0x00000000
         r4 = 0xb4889600
         r5 = 0xb4835000
         r6 = 0xb47fcfe4  libart.so`art::Runtime::instance_
         r7 = 0xa6714380
         r8 = 0xa6714398
         r9 = 0xb4835000
         r10 = 0x00000000
         r11 = 0xa6714360
         r12 = 0xb47fbb28  libart.so`art::Locks::logging_lock_
         sp = 0xa6714310
         lr = 0xb464a551  libart.so`art::gc::Heap::ConcurrentGC(art::Thread*) + 53
         pc = 0xb4648c20  libart.so`art::gc::Heap::CollectGarbageInternal(art::gc::collector::GcType, art::gc::GcCause, bool)
          cpsr = 0x20000030
      

      我們可以看到寄存器 r0 的值為 0xb4889600 ,這個值就是 `CollectGarbageInternal

      函數的第一個參數,this指針,也就是當前Heap對象的地址。在ARM下,r0~r4存放函數的參數,超過四個的參數放在棧上,具體如何利用這些寄存器的信息需要了解一些ARM匯編知識。

    4. 查看運行的匯編代碼

      (lldb) di -p
          libart.so`art::gc::Heap::CollectGarbageInternal:
      ->  0xb4648c20 <+0>:  push.w {r4, r5, r6, r7, r8, r9, r10, r11, lr}
          0xb4648c24 <+4>:  subw   sp, sp, #0x52c
          0xb4648c28 <+8>:  ldr.w  r9, [pc, #0xa9c]
          0xb4648c2c <+12>: add    r4, sp, #0x84
      

沒有root設備的調試辦法

如果沒有root權限,那么我可以借助run-as命令。run-as可以讓我們以某一個app的身份執行命令——如果我們以被調試的那個app的身份進行attach,自然是可以成功的。

假設被調試的app包名為 com.example.lldb ,那么首先想辦法把 lldb-server 這個文件推送到這個app自身的目錄:

  1. adb push 直接這么做不太方便(還需要知道userid),我們先push到 /data/local/tmp/

    adb push lldb-server /data/local/tmp/

  2. 然后執行adb shell,連接到Android shell,執行

    run-as com.example.lldb`

  3. 拷貝這個文件到本App的目錄,并修改權限;(由于有的手機沒有cp命令,改用cat)

    cat /data/local/tmp/lldb-server > lldb-server

    chmod 777 lldb-server

  4. 運行lldb-server

    lldb-server platform –listen unix-abstract:///data/local/tmp/debug.sock

接下來的步驟就與上面root設備的調試過程完全一樣了 :)

后記

終于完成了Android調試這一系列的文章,時間跨度長達一年;從Java到C/C++再到匯編級別的調試,從有源碼到無源碼,從Application層到Framework層,任何代碼都可以進行調試。借助強大的IDE以及調試器,我們不僅可以快速定位和解決問題,還可以深入學習任何一個復雜的模塊。尤記得用探索用lldb進行native調試的過程,網上沒有任何android方面的教程,唯一的學習資料就是Android Studio調試模塊的源碼以及LLDB Android插件的源碼;這其中碰的壁和踩過的坑不計其數。好在最后終于一一解決,可以睡個安穩覺了 ~_~

 

來自:http://weishu.me/2017/01/14/how-to-debug-android-native-framework-source/

 

 本文由用戶 qqxiaoming 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!