google-perftoolsというx86,x86_64,ppcなUNIX向けのプロファイラの(cpu-profiler部分)を、armなLinuxに対応させてみました。何かの役に立つかもしれないので、patchおよびpatch作成作業のメモを載せます。arm-v5tアーキテクチャ(ARM9系)向けの移植です。

Linux/ARM向けのソフトウェアのパフォーマンスを解析したいなぁと思うことがあったのですが、OProfileはカーネル入れ替えがめんどくさい、gprofはプロファイル専用のバイナリを作成するのがめんどくさい、プロプラな奴は興味ないということで移植しました。移植の方がめんどくさいだろという話もありますが。perftools自体の説明はこちらが便利です。あーそういえばAndroidもARMでしたっけ?

パッチ

http://binary.nahi.to/google-perftools-0.93_armv5t_1.patch パッチの適用方法とmake方法は次のとおりです。 host% tar xvzf google-perftools-0.93.tar.gz host% cd google-perftools-0.93/ host% chmod +w aclocal.m4 (tarballのpermissionがおかしい模様) host% patch -p1 < google-perftools-0.93_armv5t_1.patch host% CC=armv5tel-redhat-linux-gnueabi-gcc CXX=armv5tel-redhat-linux-gnueabi-g++ ./configure \ --host=arm-linux --enable-frame-pointer --prefix=/path/to/nfs_root/ host% sudo make install 以上でインストールが完了します。ARMマシン(以下target)上で、.soをPRELOADして解析対象を実行すると、prof.outという解析結果ファイルが生成されます。 target# export CPUPROFILE=/root/prof.out target# export CPUPROFILE_FREQUENCY=100 (秒間最大何回プロファイルタイマをfireさせるか. デフォルト100, 最大4000) target# LD_PRELOAD=/lib/libprofiler.so.0 /path/to/program PROFILE: interrupts/evictions/bytes = 512/0/156 解析対象の /path/to/program は、-O0 または -O2 -fno-omit-frame-pointer でコンパイルされていることが必要です。stripされていてもOKです。

解析結果のprof.outファイルの内容を可視化するにはpprofコマンドを使います。pprofコマンドをARMマシン上で動かそうとすると、そちらにperl, file, binutils, graphvizくらいはインストールされていないとダメですが、これらを頑張ってARMマシンに入れなくても、次を満たしていればx86上でプロファイル結果を見られます。 次のファイルがx86からアクセス可能 prof.out 解析対象のprogram programが依存しているDSO群

crossのbinutilsがx86マシンにインストールされている これは便利。 host% pprof --tools=/usr/bin/armv5tel-redhat-linux-gnueabi- \ --lib_prefix=/usr/armv5tel-redhat-linux-gnueabi/sys-root/ \ /path/to/nfs_root/path/to/program /path/to/nfs_root/root/prof.out Welcome to pprof! For help, type 'help'. (pprof) top Total: 512 samples 251 49.0% 49.0% 251 49.0% c 152 29.7% 78.7% 152 29.7% b 96 18.8% 97.5% 96 18.8% a 13 2.5% 100.0% 13 2.5% wordexp 0 0.0% 100.0% 2 0.4% _dl_signal_error 0 0.0% 100.0% 510 99.6% __evoke_link_warning_getwd 0 0.0% 100.0% 499 97.5% main (pprof) pprofにかける際のprogramは、strip前のものにしてください。pprof結果が怪しいときは、./configure で --enable-old-sighandler してみてください。うまく動かなかったりしたら教えてください。私は主にqemuとFedora6-armで動作確認してます。

移植方法

同ソフトウェアはアーキテクチャ依存部分が分離されているので、移植作業は楽で、 ARMv5t用のアトミック演算関数を2種類書く スタックトレース関数を書く シグナルを受信したときのPCの値を得る関数を書く の3stepで大体終わりです。順に。

2. スタックトレース関数を書く (src/stacktrace_armv5t-inl.h)

次に、スタックのバックトレースを行なう関数 GetStackTrace() を書きます。バックトレースというのは、(gdb) bt すると表示されるあれのことです。この関数も1.同様、シグナルハンドラから呼ばれるので、シグナルセーフにつくるのがベターです。google-perftools-0.93/INSTALL 等にも、デッドロックの危険があるからGetStackTrace()でmallocは呼ばない方が良いのだと注記されていました。バックトレースを行なう関数というと、glibcにそのままの名前の関数 backtrace() というのがあり、#include するだけですぐ使えたりするんですが、この関数は内部でmallocを呼ぶことがあるのでできれば避けろと書かれてます。arm portでもそれに従い自作することにしました。ま、そんなに難しいわけではありません。

フレームポインタが省略されていないと仮定すると、arm-gccでコンパイルしたコードの、関数の入口部分は次のようになってます。 % armv5tel-redhat-linux-gnueabi-objdump -d example ... 0000853c <main>: 853c: e1a0c00d mov ip, sp 8540: e92dd800 push {fp, ip, lr, pc} objdumpのバージョンによっては stmdb sp!, {fp, ip, lr, pc} と表示される場合もありますが、単に表記が違うだけでマシン語のバイト列は同じです。2行目のpushが実行された後のスタックのレイアウトは、こんな感じです。なので、bt関数は #define OFFSET_FP_TO_SAVED_FP (-3) #define OFFSET_FP_TO_LR 2 static void **NextStackFrame(void **old_sp) { void **new_sp = (void **) *old_sp; // initial value of fp regigster (at _start()) is 0x0. if ((uintptr_t)new_sp == 0) return NULL; new_sp += OFFSET_FP_TO_SAVED_FP; // Check that the transition from frame pointer old_sp to frame // pointer new_sp isn't clearly bogus if (new_sp <= old_sp) return NULL; if ((uintptr_t)new_sp - (uintptr_t)old_sp > 100000) return NULL; return new_sp; } int GetStackTrace(void **result, int max_depth, int skip_count) { void **sp; int n = 0; uintptr_t fp = 0; __asm__ __volatile__ ("mov %0, fp" : "=r"(fp)); sp = (void **)fp; if ((uintptr_t)sp == 0x0) return 0; sp += OFFSET_FP_TO_SAVED_FP; while (sp && n < max_depth) { if ((uintptr_t)sp > 0xc0000000) { break; } if (skip_count > 0) { skip_count--; } else { result[n] = *(sp + OFFSET_FP_TO_LR); ++n; } sp = NextStackFrame(sp); } return n; } でよろしいのではないかと。説明端折りすぎ。x86版を多少書き換えただけです。インラインアセンブラ部分など。

3. シグナルを受信したときのPCの値を得る関数を書く (src/get_pc.h)

最後に、シグナルを食らったとき、どの命令を実行していたか(program counterの値)を戻す関数GetPCを書きます。シグナルハンドラをsigaction()で登録する際、SA_SIGINFOというフラグを指定しておくと、シグナルハンドラの第三引数としてucontext_t*という型の構造体を得ることができますので、この構造体の中に保存されているPC値を戻してやればOKです。handlerのシグネチャ等の詳細はman sigaction。 inline void* GetPC(const ucontext_t& signal_ucontext) { return (void*)signal_ucontext.uc_mcontext.arm_pc; } これだけ。ucontext_tの内容は、sys/ucontext.h (glibc-ports-2.7/sysdeps/unix/sysv/linux/arm/sys/ucontext.h) とか、asm/sigcontext.h (linux-2.6.2x/include/asm-arm/sigcontext.h) を参照すればわかります。

ただ、カーネルが古いと(?)、SA_SIGINFOのucontext_t経由ではPCの値が取れないようです。手元の2.6.10カーネルではダメでした。catchsegvコマンドのソースコードである glibc-2.7/debug/segfault.c を参考に、SA_SIGINFOを指定しないでsigaction()し、glibc-ports-2.7/sysdeps/unix/sysv/linux/arm/sigcontextinfo.h のSIGCONTEXTマクロやGET_PC()マクロを使う方法なら、きちんとPCを取ることができました。詳しい事情をだれか教えて的。とりあえず、configureオプションで、こちらの実装も選択できるようにしておきました。詳細はパッチ見てください。

以上です。以下はおまけ。

perftoolsのしくみと、i386向けGetPC実装について

arm版GetPC関数の実装は上に書いたとおりの簡素なものですが、実はx86版のGetPCの実装はもうすこし凝っています。その概要をメモとして残しておきます。google-perftoolsは、測定対象のプログラムにLD_PRELOAD等で割り込んで、main関数の前でsetitimer(ITIMER_PROF)システムコールを呼び、測定対象プロセスに一定周期でSIGPROFシグナルが飛んでくるようにします。SIGPROFのハンドラもperftoolsが用意していて、その中でさきほどのGetStackTrace()を使ったbacktrace処理が行なわれます。main()がfoo()を呼び、foo()がbar()を呼び、bao()の中でSIGPROFを食らったとすると、backtraceは当然、 prof_handler() <-- シグナルハンドラ bar() foo() main() となるわけですが、bar()関数のプロローグ処理の前や、エピローグ処理の後にシグナルを食らうと、foo関数用のスタックフレームが構築前あるいは解体後であるため、バックトレースが prof_handler() <-- シグナルハンドラ foo() main() となってしまいます。

perftoolsは、バックトレースの先頭の2関数を無視し、残りのトレースとGetPC()で取得したPCが指している関数をつないでコールグラフを作成するので、後者の場合だと、mainが直接barを呼ぶ、おかしなコールグラフが作成されてしまいます。これを避けるために、x86版のGetPCでは、フレーム構築前・解体後にGetPCされたときは、bar内のアドレスでは無く、foo内のアドレスを戻すように細工が行なわれます。arm版ではこの処理は行なっていません。TODOということで。