顯示具有 Android 標籤的文章。 顯示所有文章
顯示具有 Android 標籤的文章。 顯示所有文章

2013年1月11日 星期五

Address-sanitizer : 新的 Android 快速記憶體錯誤偵測工具


簡介

Android 在 Jerry Bean 的時候加入了 Address-sanitizer 的支援

這東西是什麼勒, 說白話一點就是個可以偵測一些記憶體使用錯誤的一個小工具,

有點類似 valgrind 中 memcheck 的功能但是更快更簡單(但相對的某些錯誤無法偵測)

偵測錯誤的類型

廢話不多說, 直接看範例程式跟範例輸出

越界存取(Out of bound)

Heap 越界存取
#include 
#include 
int main(int argc, char **argv) {
  char *x = (char*)malloc(10 * sizeof(char));
  memset(x, 0, 10);
  int res = x[argc * 10];  // BOOOM
  free(x);
  return res;
}
範例執行輸出:
=================================================================
==799== ERROR: AddressSanitizer heap-buffer-overflow on address 0x41255cca at pc 0x2a00055b bp 0xbeff6b0c sp 0xbeff6b08
READ of size 1 at 0x41255cca thread T0
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40023e77 (/system/lib/libasan_preload.so+0x9e77)
    #2 0x4001c947 (/system/lib/libasan_preload.so+0x2947)
    #3 0x2a000559 (/system/bin/heap-out-of-bounds+0x559)
    #4 0x4114371d (/system/lib/libc.so+0x1271d)
0x41255cca is located 0 bytes to the right of 10-byte region [0x41255cc0,0x41255cca)
allocated by thread T0 here:
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40022aff (/system/lib/libasan_preload.so+0x8aff)
    #2 0x2a0004e9 (/system/bin/heap-out-of-bounds+0x4e9)
    #3 0x4114371d (/system/lib/libc.so+0x1271d)
Shadow byte and word:
  0x0824ab99: 2
  0x0824ab98: 00 02 fb fb
More shadow bytes:
  0x0824ab88: 00 00 00 00
  0x0824ab8c: 04 fb fb fb
  0x0824ab90: fa fa fa fa
  0x0824ab94: fa fa fa fa
=>0x0824ab98: 00 02 fb fb
  0x0824ab9c: fb fb fb fb
  0x0824aba0: fa fa fa fa
  0x0824aba4: fa fa fa fa
  0x0824aba8: fa fa fa fa
Stats: 0M malloced (0M for red zones) by 36 calls
Stats: 0M realloced by 0 calls
Stats: 0M freed by 0 calls
Stats: 0M really freed by 0 calls
Stats: 2M (642 full pages) mmaped in 5 calls
  mmaps   by size class: 7:4095; 8:2047; 11:255; 12:128; 13:64; 
  mallocs by size class: 7:27; 8:4; 11:2; 12:2; 13:1; 
  frees   by size class: 
  rfrees  by size class: 
Stats: malloc large: 0 small slow: 5
==799== ABORTING
全域變數越界存取
int global_array[100] = {-1};
int main(int argc, char **argv) {
  return global_array[argc + 100];  // BOOM
}
範例執行輸出:
=================================================================
==7161== ERROR: AddressSanitizer global-buffer-overflow on address 0x2a002194 at pc 0x2a00051b bp 0xbeeafb0c sp 0xbeeafb08
READ of size 4 at 0x2a002194 thread T0
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40023e77 (/system/lib/libasan_preload.so+0x9e77)
    #2 0x4001c98f (/system/lib/libasan_preload.so+0x298f)
    #3 0x2a000519 (/system/bin/global-out-of-bounds+0x519)
    #4 0x4114371d (/system/lib/libc.so+0x1271d)
0x2a002194 is located 4 bytes to the right of global variable 'global_array (external/test/global-out-of-bounds.cpp)' (0x2a002000) of size 400
Shadow byte and word:
  0x05400432: f9
  0x05400430: 00 00 f9 f9
More shadow bytes:
  0x05400420: 00 00 00 00
  0x05400424: 00 00 00 00
  0x05400428: 00 00 00 00
  0x0540042c: 00 00 00 00
=>0x05400430: 00 00 f9 f9
  0x05400434: f9 f9 f9 f9
  0x05400438: 00 00 00 00
  0x0540043c: 00 00 00 00
  0x05400440: 00 00 00 00
Stats: 0M malloced (0M for red zones) by 35 calls
Stats: 0M realloced by 0 calls
Stats: 0M freed by 0 calls
Stats: 0M really freed by 0 calls
Stats: 2M (642 full pages) mmaped in 5 calls
  mmaps   by size class: 7:4095; 8:2047; 11:255; 12:128; 13:64; 
  mallocs by size class: 7:26; 8:4; 11:2; 12:2; 13:1; 
  frees   by size class: 
  rfrees  by size class: 
Stats: malloc large: 0 small slow: 5
==7161== ABORTING
區域變數越界存取
int main(int argc, char **argv) {
  int stack_array[100];
  stack_array[1] = 0;
  return stack_array[argc + 100];  // BOOM
}
範例執行輸出:
=================================================================
==7165== ERROR: AddressSanitizer stack-buffer-overflow on address 0xbed0bad4 at pc 0x2a000557 bp 0xbed0b914 sp 0xbed0b910
READ of size 4 at 0xbed0bad4 thread T0
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40023e77 (/system/lib/libasan_preload.so+0x9e77)
    #2 0x4001c98f (/system/lib/libasan_preload.so+0x298f)
    #3 0x2a000555 (/system/bin/stack-out-of-bounds+0x555)
    #4 0x4114371d (/system/lib/libc.so+0x1271d)
Address 0xbed0bad4 is located at offset 436 in frame 
of T0's stack: This frame has 1 object(s): [32, 432) 'stack_array' HINT: this may be a false positive if your program uses some custom stack unwind mechanism (longjmp and C++ exceptions *are* supported) Shadow byte and word: 0x17da175a: f4 0x17da1758: 00 00 f4 f4 More shadow bytes: 0x17da1748: 00 00 00 00 0x17da174c: 00 00 00 00 0x17da1750: 00 00 00 00 0x17da1754: 00 00 00 00 =>0x17da1758: 00 00 f4 f4 0x17da175c: f3 f3 f3 f3 0x17da1760: 00 00 00 00 0x17da1764: 00 00 00 00 0x17da1768: 00 00 00 00 Stats: 0M malloced (0M for red zones) by 35 calls Stats: 0M realloced by 0 calls Stats: 0M freed by 0 calls Stats: 0M really freed by 0 calls Stats: 2M (642 full pages) mmaped in 5 calls mmaps by size class: 7:4095; 8:2047; 11:255; 12:128; 13:64; mallocs by size class: 7:26; 8:4; 11:2; 12:2; 13:1; frees by size class: rfrees by size class: Stats: malloc large: 0 small slow: 5 ==7165== ABORTING

釋放後使用 (Use after free)

#include 
int main() {
  char *x = (char*)malloc(10 * sizeof(char*));
  free(x);
  return x[5];
}
範例執行輸出:
=================================================================
==7169== ERROR: AddressSanitizer heap-use-after-free on address 0x41255cc5 at pc 0x2a0004cd bp 0xbece1b1c sp 0xbece1b18
READ of size 1 at 0x41255cc5 thread T0
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40023e77 (/system/lib/libasan_preload.so+0x9e77)
    #2 0x4001c947 (/system/lib/libasan_preload.so+0x2947)
    #3 0x2a0004cb (/system/bin/use-after-free+0x4cb)
    #4 0x4114371d (/system/lib/libc.so+0x1271d)
0x41255cc5 is located 5 bytes inside of 40-byte region [0x41255cc0,0x41255ce8)
freed by thread T0 here:
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40022a97 (/system/lib/libasan_preload.so+0x8a97)
    #2 0x2a0004b1 (/system/bin/use-after-free+0x4b1)
    #3 0x4114371d (/system/lib/libc.so+0x1271d)
previously allocated by thread T0 here:
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40022aff (/system/lib/libasan_preload.so+0x8aff)
    #2 0x2a0004ab (/system/bin/use-after-free+0x4ab)
    #3 0x4114371d (/system/lib/libc.so+0x1271d)
Shadow byte and word:
  0x0824ab98: fd
  0x0824ab98: fd fd fd fd
More shadow bytes:
  0x0824ab88: 00 00 00 00
  0x0824ab8c: 04 fb fb fb
  0x0824ab90: fa fa fa fa
  0x0824ab94: fa fa fa fa
=>0x0824ab98: fd fd fd fd
  0x0824ab9c: fd fd fd fd
  0x0824aba0: fa fa fa fa
  0x0824aba4: fa fa fa fa
  0x0824aba8: fa fa fa fa
Stats: 0M malloced (0M for red zones) by 36 calls
Stats: 0M realloced by 0 calls
Stats: 0M freed by 1 calls
Stats: 0M really freed by 0 calls
Stats: 2M (642 full pages) mmaped in 5 calls
  mmaps   by size class: 7:4095; 8:2047; 11:255; 12:128; 13:64; 
  mallocs by size class: 7:27; 8:4; 11:2; 12:2; 13:1; 
  frees   by size class: 7:1; 
  rfrees  by size class: 
Stats: malloc large: 0 small slow: 5
==7169== ABORTING


使用方式

使用方式出乎意料的簡單
只要在你的 Android.mk 裡面加一行
LOCAL_ADDRESS_SANITIZER := true
主要原因在於 Address-sanitizer 也是 google 自家弄的東西, 所以整合的還算不錯

另外有開 Address-sanitizer 有個副作用就是會強迫開啟 clang 模式

什麼意思勒? 就是你的 code 變得不是使用 gcc 編譯而是 clang/llvm

若 code 有用到 gcc 特有的東西的話可能就會炸掉:P

另外 libasan_preload.so 也記得要推到 /system/lib 的地方, 這是 Address-sanitizer run-time library, 可以去 out/target/product/*/system/lib/ 裡面撈撈

如果找不到開了 LOCAL_ADDRESS_SANITIZER := true 但建置系統跟你抱怨 libasan_preload.so 或 libasan.a 的話,
就切到 external/compiler-rt/lib/asan/ 自行建置一下, 或著是用 mmm 讓建置系統自己拉相依關係


錯誤報告的解讀

接著這部份最重要的就是他吐出那團錯誤報告要怎麼讀
就直接從最複雜的釋放後使用 (Use after free)的報告來講解

=================================================================
==7169== ERROR: AddressSanitizer heap-use-after-free on address 0x41255cc5 at pc 0x2a0004cd bp 0xbece1b1c sp 0xbece1b18
首先第一個部份是會跟你報告什麼類型的錯誤, 如這邊是 heap-use-after-free, 另外還有 stack-buffer-overflow, global-buffer-overflow, heap-buffer-overflow 這幾種錯誤類型, 分別對到 stack, global 及 heap 的越界存取

on address 0x41255cc5 部份是你要存取的記憶體位置
at pc 0x2a0004cd bp 0xbece1b1c sp 0xbece1b18 部份則是說發生錯誤的 program counter 值是多少, 以及 frame pointer 跟 stack pointer 的內容

READ of size 1 at 0x41255cc5 thread T0
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40023e77 (/system/lib/libasan_preload.so+0x9e77)
    #2 0x4001c947 (/system/lib/libasan_preload.so+0x2947)
    #3 0x2a0004cb (/system/bin/use-after-free+0x4cb)
    #4 0x4114371d (/system/lib/libc.so+0x1271d)
0x41255cc5 is located 5 bytes inside of 40-byte region [0x41255cc0,0x41255ce8)
接著很貼心的也會將你炸掉的地方的 call stack 吐出來
只有 pc 值不知道在哪的話就呼叫 addr2line 這個工具來幫忙就可以了
例如你想查 /system/bin/use-after-free+0x4cb 對應的行號是多少則輸入以下指令即可
addr2line -e out/target/product/*/symbols/system/bin/use-after-free -a 0x4cb
* 自行帶入目前建置的 device name, -e 後面塞檔案, -a 後面塞位址, call stack 加號後面的東西

freed by thread T0 here:
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40022a97 (/system/lib/libasan_preload.so+0x8a97)
    #2 0x2a0004b1 (/system/bin/use-after-free+0x4b1)
    #3 0x4114371d (/system/lib/libc.so+0x1271d)
然後也會吐出 free 的 call stack, 解讀方式同上
previously allocated by thread T0 here:
    #0 0x40022a4b (/system/lib/libasan_preload.so+0x8a4b)
    #1 0x40022aff (/system/lib/libasan_preload.so+0x8aff)
    #2 0x2a0004ab (/system/bin/use-after-free+0x4ab)
    #3 0x4114371d (/system/lib/libc.so+0x1271d)
malloc 的地方也會有 call stack 吐出來
Shadow byte and word:
  0x0824ab98: fd
  0x0824ab98: fd fd fd fd
More shadow bytes:
  0x0824ab88: 00 00 00 00
  0x0824ab8c: 04 fb fb fb
  0x0824ab90: fa fa fa fa
  0x0824ab94: fa fa fa fa
=>0x0824ab98: fd fd fd fd
  0x0824ab9c: fd fd fd fd
  0x0824aba0: fa fa fa fa
  0x0824aba4: fa fa fa fa
  0x0824aba8: fa fa fa fa
Stats: 0M malloced (0M for red zones) by 36 calls
Stats: 0M realloced by 0 calls
Stats: 0M freed by 1 calls
Stats: 0M really freed by 0 calls
Stats: 2M (642 full pages) mmaped in 5 calls
  mmaps   by size class: 7:4095; 8:2047; 11:255; 12:128; 13:64; 
  mallocs by size class: 7:27; 8:4; 11:2; 12:2; 13:1; 
  frees   by size class: 7:1; 
  rfrees  by size class: 
Stats: malloc large: 0 small slow: 5
==7169== ABORTING
最後這團東西看不懂就算了, Address-sanitizer 的內部資訊, 有興趣去挖他們簡報出來就有詳細一點的範例加解釋了

副作用及使用限制

Address-sanitizer 使用時主要有一些額外的負擔:
  1. 每次 new/malloc 會多出額外的 128-255 byte 作為警戒區(Red Zone), 也就是越界時爆炸的引發器:P
  2. Stack 則是每個區域變數會吃掉 32~63 byte 作為警戒區(Red Zone)
  3. 全域變數也是每個變數會吃掉 32~63 byte 作為警戒區(Red Zone)
  4. 一般而言會吃掉 2~4 倍的記憶體, 但最差情況也有可能高達 20 倍的記憶體消耗
  5. Stack 會幾乎長三倍大以上
  6. Address Space 會直接被吃掉八分之一, 32 位元情況下會吃掉 0.5 G, 64 位元則會吃掉 16T, 但僅消耗定址空間, 不會實際耗費掉記憶體 (該區域被設定為不可讀不可寫, 存取到會直接炸掉)
其最大的使用限制是當 Address-sanitizer 偵測到任何記憶體錯誤時就會馬上中斷程式的執行,

這是正常現象, 也因為這樣的設計使得此工具簡單快速又方便

與 Valgrind 的比較表格

Valgrind Address-sanitizer
Heap out-of-bounds
Heap 越界存取
Yes Yes
Stack out-of-bounds
Stack 越界存取
No Yes
Global out-of-bounds
全域變數越界存取
No Yes
Use-after-free
釋放(free/delete)後使用
Yes Yes
Use-after-return
回傳後使用
(例如回傳區域變數指標)
No Sometimes/YES
Uninitialized reads
讀取未初始化的值
Yes No(註1)
Overhead
對程式影響(變慢幾倍)
10x-30x 1.5x-3x
Platforms
可使用平台
Linux, Mac Same as GCC/LLVM (註2)
註1: 這部份有 Address-sanitizer 的好兄弟 Memory-sanitizer 可以處理, 但尚未納入 Android 中
註2: GCC 部份於 2012/11/01 正式納入 Address-sanitizer[4], 可用度沒玩過不太確定, 在 Android 中要開 Address-sanitizer 就是要用 clang 就是了:P...

小結

Address-sanitizer 算是一個輕量級的好用小工具, 根據開發者所釋放出來的簡報 Google 內部自己也有使用此工具並且抓出了上千個 bug, 包含 llvm, gcc, vim, firefox 等大型 Open Source 軟體的記憶體使用 bug, 目前 Address-sanitizer 還有一些相關的兄弟如 Memory-sanitizer 及 Thread-sanitizer, 分別可以偵測 Uninitialized reads 及 Race condition, 不過在 Android 中目前還沒引進, 但都 Google 自家人弄的, 可以期待之後應該是會引進到 Android 中, 讓一些常見的記憶體錯誤可以早期發現:)

參考鍊結


[1] http://code.google.com/p/address-sanitizer
[2] http://llvm.org/devmtg/2011-11/Serebryany_FindingRacesMemoryErrors.pdf
[3] http://address-sanitizer.googlecode.com/files/address_sanity_checker.pdf
[4] http://gcc.gnu.org/ml/gcc/2012-11/msg00016.html

2012年4月17日 星期二

Android C Library: Bionic 成長計畫

今年 OSDC 2012 所分享的 take, 內容主要包含對於 Bionic C Library 的一些改進, 一般而言, 看到 C Library 人們直覺可能會想到 printf 等等標準函數庫所提供的函式,但事實上除此之外 C Library 也通常會附贈許多額外功能, 例如 Dynamic Linker 以及一些 Profiling 的支援, 以一般 Linux 常見的 Glibc 來說 其中就包含了 Dynamic Linker, gprof 及 sprof 等額外功能 而在 Bionic 中在這部份而言相對貧乏, 雖然有包含 Dynamic Linker 但其實作相對 Glibc 少上許多功能, 因此我們所改進的方向是往補強功能的部份發展, 而在主題當中主要包含下列幾個部份
  1. GNU-Style Hash 支援
  2. Prelink 的改善(更多細節可參照Android Prelinker : Apriori)
  3. Aprof(更多細節可參照)
  4. Checkpoint 簡介
其中 Checkpoint 的部份與 C Library 較無直接關聯, 但事實上 Checkpoint 過程中會與 C Library 及 Dynamic Linker 實作有相當大的依存性, 因此我們的長期目標是將 Checkpoint 功能整合進 Bionic 裡面, 以期更簡潔快速的 Checkpoint 功能實作


2012年2月1日 星期三

Aprof : Android Profiler - A profiling tool for android native code


目前 Android 在 profiling tool 這邊大致上有 traceview 及 oprofile 可用, 但 traceview 是針對於一般的 DVM 程式, 而 oprofile 又相對的較複雜, 且缺乏類似 gprof 的簡易工具來觀測效能

而事實上已經有人開始注意到此事並提出方法, 最直接的方式是實作 mcount 函數並透過 gcc -pg 選項來使用, 如此一來變不須更動太多部份,例如 android-ndk-profiler 便是採用此方式, Aprof 也將尋此方式來進行擴充, 但我們所採用的方式並非單純將 gprof 功能實作, 而是進一步擴充並且為 Android 系統量身打造.

Aprof 簡介


有鑑於目前 android 平台上對於 native code 的 profiler 較於缺乏, 因此我們提出實作一個為 Android 量身訂作的 Profiler, 取名為 aprof, 主要功能與 gprof 相仿, 提供 Call Graph 與 Time Sample Info.

在 aprof 設計時的幾個主要目標:

  1. 採用舊有 gprof 的 mcount 界面, 避免修改 toolchain
  2. 能夠支援 Shared Library
  3. 可以對 JNI 的 Shared Library 進行 Profiling

在 aprof 中主要也是透過 mcount 函數及 gcc -pg 選項來使用, 但在這邊我們所採取的方式是與 gprof 輸出格式不相容, 主要原因在於 gprof 輸出格式無法支援 shared library 的 profiling, 若沿用原本格式將造成功能上的限制.

Aprof 使用方法


若要使用 Aprof 來 profile 執行檔的話只要在 Android.mk 加入以下的選項開啟, 對於一般的 static library 也是採用相同方式即可

LOCAL_ENABLE_APROF := true


但要注意的是若是要使用 Aprof 來 profile Shared Library 的話主要分為兩種情況使用

針對一般 Non-JNI 使用的 Shared Library 採用與一般執行檔的開啟方式即可, 但要執行檔也開啟 Aprof 時才會啟動 profiling

若是對於 JNI 使用的 Shared Library 則在 Android.mk 加入以下的選項, 其 profiling 資訊將會於 Activity 於 Life Cycle 中的 stop 時寫入檔案, 值得注意的是若程式已經開啟 Aprof 則會以 Non-JNI aprof mode 來進行 profiling

LOCAL_ENABLE_APROF_JNI := true


在開啟 Aprof 並且重新建置程式後, 便可開始準備 profiling, 開始的方式則是執行一次該程式, 在開啟 profiling 的情況下會比未開啟的情況下慢上許多, 在執行完成程式後變可以到 /sdcard/ 裡面檢查 profiling 的輸出, 預設檔名是 $(progname).aprof

接著 profiling file 撈回 Host 端, 也就是使用 adb 去下載, 方式可參考以下指令:

adb pull /sdcard/$(progname).aprof
 
# 例如進行 Profiling 的程式是 foo
adb pull /sdcard/foo.aprof

然後使用 aprof 來讀取 profiling file 的資訊, 使用方式如下:


# aprof $(prog_file) $(prof_file)
aprof foo foo.aprof

下面則是範例輸出:

  %      cumulative     self                 self       total
 time     time          time      calls    ms/call    ms/call   name
 99.52       2170       2140    2178309          0          0   fib
  0.00       2170          0          1          0        217   main
  0.48          0         30          0          0          0   <libc.so>
 
Call graph (explanation follows)
 
-------------------------------------------------------------
Image           : foo
Cumulative time : 2170 ms
Self time       : 2140 ms
  Function  % time  cumulative        self       Count  Call by
 fib                      2170        2140
            100.00        2170           0           1  main
            100.00        2170        2140     2178308  fib
 main                     2170           0
            100.00        2170           0           1  <libc.so>


上半部份主要是顯示各個函數所佔用的時間與百分比, cumulative time 表示包含子函數的時間, 而 self time 則表示該函數所有消耗時間, 例如 a() call b(), a() 花了 1 sec, b() 花了 2 sec, 則 a() 的 cumulative time 為 3 sec, self time 為 2 sec, calls 代表該函數總共被呼叫的次數, self ms/call 及 total ms/call 分別代表每次呼叫約花費多少時間.

但在上面的例子中可以發現有 的這個符號, 主要原因是 aprof 沒有載入該 shared library 的符號資訊, 我們可以透過 -L 來指定 Shared Library 的搜索路徑, 一般搜索路徑設置在 Android 的目錄下的 out/target/product/panda/symbols/system/lib (以 panda board 為例)即可.

# aprof $(prog_file) $(prof_file) -L$(lib_path)
aprof foo foo.aprof -L out/target/product/panda/symbols/system/lib

一般而言在載入了符號後資訊會較為詳細, 但仍有些不足的地方, 例如在 calls 的部份若該 Shared Library 建置時並無開啟 aprof 則無法得到此部份的資訊, 也因此 cumulative time 部份會無法計算, 不過 Self time 的部份在有了 symbol 資訊後則可正確定位.

  %      cumulative     self                 self       total
 time     time          time      calls    ms/call    ms/call   name
 99.52       2170       2140    2178309          0          0   fib
  0.00       2170          0          1          0        217   main
  0.32          0         20          0          0          0   write
  0.16          0         10          0          0          0   memcpy
 
 
Call graph (explanation follows)
 
-------------------------------------------------------------
Image           : foo
Cumulative time : 2170 ms
Self time       : 2140 ms
  Function  % time  cumulative        self       Count  Call by
 fib                      2170        2140
            100.00        2170           0           1  main
            100.00        2170        2140     2178308  fib
 main                     2170           0
            100.00        2170           0           1  __libc_init
-------------------------------------------------------------
Image           : foo
Cumulative time : 2170 ms
Self time       : 30 ms
 write                       0          20
 memcpy                      0          10

接下來的部份則是介紹對於將結果視覺化呈現的方式, aprof 支援 dot 格式的輸出, 可透過 -d 來輸出 dot 格式, 可透過 pipeline 到 dot (通常包含於 graphviz 套件)直接將其結果輸出成圖檔, 使用方式參考下列指令

# aprof $(prog_file) $(prof_file) -L$(lib_path)
aprof foo foo.aprof -L out/target/product/panda/symbols/system/lib -d | dot -Tpng -o foo.aprof.png

在這邊附上一個 busybox 的 md5sum 的 aprof 輸出圖檔供參考:

圖 1. md5sum 的 Call Graph 輸出


Aprof 的使用方式及輸出資訊大致介紹到這個部份.

Aprof 實作


Aprof 為了能夠支援較完整的 Shared Library Profiling 機制, 所以有一大部分是實作在 linker 中, 這樣的作法主要是參考自 sprof 的實作方式, 而在這邊為何要重新打造一個 Profiler 主要有兩個原因:

  1. sprof 實作於 glibc 的 dynamic linker, 其授權是採用 GPL, 因此無法直接採用
  2. 整合 gprof 與 sprof, 並為 Android 系統的需求設計

為何分 JNI 及 Non-JNI mode ?

對於 Shared Library , aprof 提供兩種模式可進行 Profiling, 兩者之間的行為差別在於 JNI mode 下會各個 Shared Library 會分別 Profiling , 分別輸出檔案, 並且最重要的是執行檔不需要開啟 Aprof 也可進行 Profiling.

non-JNI mode 則是必須執行檔開啟 Aprof 時才會進行 Profiling, 這樣的設計主要是因為避免其他 Link 到該 Shared Library 的執行檔皆受到無差別的效能攻擊.

JNI mode 主要存在的意義就在於如其命名一樣, 針對 JNI 的 Shared Library 來使用, 在作為 JNI 的 Shared Library 通常是被 DVM (app_process / zygote)所開啟, 而要等到其執行結束則會有點困難, 故在這邊的設計是採用於 Activity 於 Life Cycle 中的 stop 時輸出 Profiling File.

另外需要注意的是若用在 JNI 上的話, 必須確保該應用程式有寫入的權限, 也就是記得在 AndroidManifest.xml 中加入 android.permission.WRITE_EXTERNAL_STORAG


AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
      package="com.example.android.simplejni">
    <application android:label="Simple JNI">
        <activity android:name="SimpleJNI">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
</manifest>

目前狀態

若使用 AOSP JB 或 ICS 的話 Aprof 僅需要再使用以下 patch 即可
Aprof 也支援及 Gingerbread, 但如果你使用的是 Linaro, CyanogenMod (包含其延伸) 或 Gingerbread, 則需要額外多加入這幾個 patch 才可正常運作:

2012年1月31日 星期二

Android Prelinker : Apriori

雖然 apriori 於 ICS 中被移除了, 但於 gingerbread 中還是長存著, 寫這篇文章順便為自己之前修改 Apriori 的部分發篇文介紹一下做個小結案

簡介
Prelink 的機制主要目的在於減少 Dynamic Linker 載入程式時 link 所花的時間已達到加速程式啟動, 目前在一般 Linux 有由Redhat 的 Jakub 所提出的 prelink[1], 並可有效減少 link time, 而該方法需要於檔案中塞入額外的資訊, 因此較不適用於 Android 的用途上, 也因此 Android 自己弄了一套 Prelink 機制.


Dynamic Linking


在開始說明 Prelink 前先來說明, dynamic linking 的流程與用途, 避免無相關背景知識讀者看無. 而為了簡化說明這邊僅介紹採用 ELF 格式下的情況.


一般而言我們在撰寫程式的時候會使用到一些函數庫, 例如 printf, scanf 等等各式各樣的函數, 但這些函數都經常被各個程式使用, 若每個程式都包含了一份各字的 printf, scanf 那麼整個系統將會變得相當龐大, 而 Dynamic Linking 就是在面對這樣的問題底下所出現一種解法, 將一些常用的函數抽出來變成一個函數庫, 並且函數庫可以在各個程式間共用, 避免不必要的浪費.


有了基本的概念後直接來看個 Hello World 的例子 :

#include <stdio.h>

int main(){
    printf("Hello world!\n");
    return 0;
}

其中的 printf 很明顯的不存在以上的程式中, 而是來自於 C library, 因此在編譯好的可執行檔中有紀錄幾個資訊
  1. 相依到哪些 Shared library
  2. 有哪些 Symbol (函數/變數皆屬於一個 Symbol)是來自外部 shared library (要注意並無紀錄 Symbol 是由哪個 shared library 來)
在執行程式時, 系統可根據在執行檔中的資訊去幫你進行 Dynamic Linking, 這個動作在 Linux 當中通常由 ld.so 來處理, 在 Android 中則由 /system/bin/linker 處理.


前面提到執行檔中僅紀錄有哪些 Symbol 是外來的, 並無紀錄其歸屬因此 Dynamic Linker 的主要工作流程如下
  1. 將執行檔載入記憶體中
  2. 載入所有相依的 Shared library (包含相依的相依)
  3. 查找 Symbol 在哪個  Shared library 中
  4. 將查找結果更新(Relocation)
  5. 所有 Symbol 處理完畢註1後才開始執行程式
因此程式的啟動時間總共是 1~4 在 Prelink 中主要可以加速的是 3+4 的部份.


其中 4  的更新的步驟主要是將  Shared library 的基底位置(Base Address, 指載入到的記憶體區段開頭)在加上 Symbol 在該動態函式庫的位址.

註解

註1 : 有些例外, 但這邊不探討, keyword 為 weak symbol


Prelink


在有了 Dynamic Linker 運作的基本流程後, 接著我們可以來探討 Prelink 的機制, Prelink 機制前面有提到主要目的在於減少查找 Symbol 在哪個  Shared library 的時間, 也就是先把所有符號查起來並且將結果存起來.


那該如何達到這點呢?首先我們所遇到的第一個問題會是 Shared library 的基底位置該如何處理, Shared library 在設計時變考量到必須可載入任何位置皆可以動(PIC), 否則將會因此浪費記憶體定址空間, 且也可能因此衝突.


所以有幾個方法可以處理這個問題 1. 掃描所有的 Shared library 並分配位址空間 2. 為 Shared library 預分配位址, 在 Android 中所採用的方法為後者也就是透過 prelink map 的方式來設定該 shared library.


在 Shared library 的基底位置決定後, 便可以先處理所有的 Relocation, 但要注意的是 Shared library 也因此喪失部份的彈性, 變得必須載入至固定位址, 因此在 Dynamic Linker 中也必須支援.


但相對的也可得到一些其他的優勢, 例如 Relocation 都處理完的話, 那其資訊也可以直接丟掉了, 在檔案大小上會小上一些, 間接的會再度加速整個程式載入的流程.


不過上敘的部份是針對 Android Prelink 機制而言 linux 上 prelink 完後會塞入一些額外資訊, 檔案反而會變肥.


Android Prelink 實作


在 Android 上的 Prelinker 實作相對於 linux 的 prelink 來說相當輕薄短小, 並且設計簡單, 執行流程如下

  1. 載入要 Prelink 的 Shared Library
  2. 查找 Prelink map 或從參數設定 Base Address
  3. 查找 Symbol , 若找的到即更新其 Address
  4. 處理掉相對位址的 Relocation
  5. 將已經處理完的 Relocation 從檔案中移除
  6. 把  Base Address 塞到檔案最後面

而其中第 6 項目, 個人猜測是 Android 為啥 ICS 以前不採用 strip 而採用自家的 soslim 來為  Shared library 瘦身的原因

但因不明原因 Android Prelink 機制只做半套, 只有 Local Prelink, 意思是只能處理同檔案下的 Symbol 而已, 因此能 Prelink 的有限.

在  Dynamic Linker (bionic/linker) 方面載入 Shared Library 的工作流程如下
  1. 開啟 Shared Library 檔案
  2. 判斷檔案後面有沒有塞 Prelink 資訊
  3. 有的話讀出其 Base Address
  4. 接著按照其資訊載入預定記憶體位置



Android Global Prelink


前面提到 Apriori 只有做 Local Prelink, 因此我們對於 Apriori 進行補完(實際上內部程式碼已經有 Global Prelink 的雛型, 但未完成)
  1. 載入要 Prelink 的 Shared Library
  2. 查找 Prelink map 或從參數設定 Base Address
  3. 載入相依的 Prelink 過的 Shared Library,  Non-prelink 的 Shared Library 將直接掠過
  4. 查找 Symbol , 若在已 Prelink 過的相依  Shared Library  找的到即更新其 Address
  5. 處理掉相對位址的 Relocation
  6. 將已經處理完的 Relocation 從檔案中移除
  7. 把  Base Address 塞到檔案最後面
主要在流程中插入載入相依的 Shared Library, 使得能處理的 Relocation 數激增, 在沒有 Undefined Weak Symbol 及 Multiple Definition 的情況下, 可以完全處理完畢, 與原本的 Apriori 相比有相當卓越的效果(文章最後面會附上實驗數據).



Android Partial Global Prelink



而 Apriori 的 prelink 是依賴 Prelink map 來實行, 未列於 Prelink map 的 Shared Library 將不會進行任何的 Prelink, 但經過觀察我們發現, 並非完全無法處理, 因為 Prelink 的必要條件在於有 Base Address 及 Symbol Offset, 但未列於 Prelink map 的 Shared Library 僅缺乏自身的 Base Address, 其相依的 Shared Library 已經 Prelink, 則其 Base Address 已經決定, 因此有相當大一部份是可以 Prelink 的, 也因此這個部份稱之為 Partial Global Prelink, 其運作流程如下:
  1. 載入要 Prelink 的 Shared Library
  2. 載入相依且 Prelink 過的 Shared Library,  Non-prelink 的 Shared Library 將直接掠過
  3. 查找 Symbol , 若在已 Prelink 過的相依  Shared Library  找的到即更新其 Address
  4. 將已經處理完的 Relocation 從檔案中移除, 相對位址類型的 Relocation 將不做任何處理
由流程可以看到, 相較於一般 Global Prelink 的行為較為簡化, 在這邊舉一個例子, 假設我們有一個 libfoo.so, 其原始碼如下:

#include <stdio.h>

int bar() {
    return 1;
}

int foo(){
    printf("Hello world!\n");
    return bar();
}

編譯出來後因為 libfoo.so 有用到 printf 因此會相依到 libc.so
而 libfoo.so 中有兩個 relocation 要處理, foo 函數中的 printf 及 bar
假設 libc.so 已經有 prelink 過那麼 libc.so 則已經有 Base Address,
因此如果我們對 libfoo.so 進行 Partial Global Prelink 後, relocation 將僅剩下 foo 函數中的 bar 要處理.

由以上例子可以看出此方式可大大減少 dynamic link time, 而且 bionic/linker 會優先查找自己的 symbol, 因此剩餘的 relocation 也可以快速的處理掉.



Android Executable Prelink

這部份就比較單純了, 因為 executable 不是 PIC, 其 Base Address 已經固定為 0, 所以直接來看流程吧.
  1. 載入要 Prelink 的 Executable
  2. 載入相依的 Prelink 過的 Shared Library,  Non-prelink 的 Shared Library 將直接掠過
  3. 查找 Symbol , 若在已 Prelink 過的相依  Shared Library  找的到即更新其 Address
  4. 處理掉相對位址的 Relocation
  5. 將已經處理完的 Relocation 從檔案中移除
與 Global Prelink 部份差不多, 但不用去 Prelink Map 查找, 也不用將 Base Address 塞到檔案尾巴.


實驗數據

最重要的部份就是效果到底如何勒, 圖表中的神秘代號解釋如下 :
  1. lp : Local Prelink
  2. gp : Global Prelink
  3. re : Reorder DT_NEEDED entries
  4. pe : Prelink executable
  5. pgp : Partial global prelink
  6. are : Aggressive reorder look-up order 

圖1. Normalized Dynamic Link time

圖2. Normalized Number of Symbol Lookup

可由圖中觀察到 Global Prelink 以及 Executable Prelink 對於 Dynamic Link Time 有相當不錯的進步, 若原本 Dynamic Link Time 越長則效果會越好, Partial Global Prelink 在這份圖表中看不出什麼效果, 主要原因是採用的實驗對象是 Android 開機會開到的幾隻程式, 其中幾乎沒有包含 Non-Prelink 的 Shared Library.


Future Work

自從 Prelink 在 ICS 被拔掉後, 以上的東西就用不到了, 預期之後將會發展一套 cache 機制來加速 Dynamic Link Time, 來避免掉 Prelink 的缺點, 以及想辦法加速 Dynamic Link Time.


附註

以上的修改(包含只出現在圖表中的方法), 目前皆可在 0xdroid 抓的到程式碼, 分佈在 bionic 及 build 裡面.

參照
[1] http://people.redhat.com/jakub/prelink.pdf
[2] 程式設計師的自我修養:連結、載入、程式庫

2011年12月14日 星期三

Android prelink in ICS

在 Android ICS 中已經將 apriori 及 soslim 都丟掉
言下之意就是 prelink 在 Android 中不再出現

根據 Commit 裡面的機制就是說硬體已經進步
所以不缺那點時間跟省下來的空間XD...

而且不是 gcc 那串 toolchain 的東西, 要分開維護很麻煩

並且對於 Address-Space-Layout Randomization 的有效性大大降低