1. 程式人生 > >如何除錯Android Native Framework

如何除錯Android Native Framework

半年前寫了一篇文章,介紹 如何除錯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視窗執行:

1
2
3
(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. 觸發斷點之後,檢視當前堆疊:

      1
      2
      3
      4
      5
      
      (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. 檢視暫存器的值

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      
      	(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. 檢視執行的彙編程式碼

      1
      2
      3
      4
      5
      6
      
      (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外掛的原始碼;這其中碰的壁和踩過的坑不計其數。好在最後終於一一解決,可以睡個安穩覺了 ~_~

  1. Android Studio你不知道的除錯技巧
  2. 如何除錯Android Framework
  3. 如何除錯Android Framework Native

 

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