uftrace を使ってプログラムの中身を解析/リバースエンジニアリングする (2) gnumericへ適用編

uftraceでgnumericの中を探る

前回の記事でuftraceについて紹介しました.

今回はそれを使ってgnumericの中身を探る, 具体的には,

セルに入力したときにそのセルの文字列を読み取っている関数を突き止める

ということを目標にします.

Linux上で表計算と言うと, LibreOfficeの方がよく使われているとおもいますが, gnumericの方が軽く, コンパイルもすぐに終わります (gnumericなら数分で終わりますが, LibreOfficeは数時間です). LibreOfficeにはCalc以外のプログラム(WriterやImpress)も含まれているでしょうから, フェアな比較ではありませんが, リバースエンジニアリングの例題としては gnumericの方が手軽です.

実行環境

gnumericのダウンロードとコンパイル

Gnumeric のホームページから, Donwload Gnumeric に行き, Source Code をダウンロードしてください. 2017年12月12日時点での最新版は バージョン1.12.37です.

解凍
$ tar xf gnumeric-1.12.37.tar.xz
configure
ポイントは唯一, CFLAGS=というオプションで-pg -g -O0を指定することです.
$ cd gnumeric-1.12.37
$ ./configure --prefix=$(pwd)/inst CFLAGS="-O0 -g -pg"
-pgは記録のために必須, -gは後にデバッガで実行を追うために必要です. -O0は必須ではありませんがデバッガでの追跡をわかりやすくするために, つけておくことを推奨します. おそらくconfigure時に, 色々足りないパッケージについて文句を言われます.
  • ./configure: line 3809: intltool-update: command not found
    checking for intltool >= 0.35.0...  found
    configure: error: Your intltool is too old.  You need intltool 0.35.0 or later.
    
  • configure: error: *** bison or equivalent is required
    
  • configure: error: *** zlib is required
    
とか. 足りないパッケージを適当にこれかな, といって入れるだけで解決しますが, たくさんあって面倒なので,
$ sudo apt build-dep gnumeric
でまとめて面倒見てもらえます. ちなみに筆者はこれをやっても
configure: error: itstool not found
というエラーが出たのでこれだけは別途インストールしました.
$ sudo apt install itstool
こうして数分間の格闘の後, configureが成功しました.
$ ./configure --prefix=$(pwd)/inst CFLAGS="-O0 -g -pg"
checking for a BSD-compatible install... /usr/bin/install -c
checking whether build environment is sane... yes
checking for a thread-safe mkdir -p... /bin/mkdir -p
checking for gawk... gawk

     ... 中略 ...

Configuration:

	Source code location:	.
	Compiler:		gcc
	Compiler flags:		-O0 -g -pg -Wall -Werror=init-self -Werror=missing-include-dirs -Wsign-compare -Werror=pointer-arith -Wchar-subscripts -Wwrite-strings -Wdeclaration-after-statement -Wnested-externs -Wmissing-noreturn -Werror=missing-prototypes -Werror=nested-externs -Werror=implicit-function-declaration -Wmissing-declarations -Wno-pointer-sign -Werror=format-security -Wstrict-prototypes -Wno-error=format-nonliteral
	Floating point type:	double

	UI:			Gtk

	Perl Support:		yes (using perl)
	Python Support:		yes (using python)

	GDA support:		NO.  libgda problem
	GNOME-DB support:	no
	Psiconv support:        missing dependencies

	PDF documentation:	No, not requested.
make
$ make
とすればビルドできますが, 有用なオプションが二つ
-j n
nプロセスまで同時に起動して並列コンパイル. マルチコア環境で使わない手はないです
V=1
コンパイルするときのコマンドラインを表示してくれます. 少し見にくくなりますが-pg -O0 -gオプションが無事行き渡っているか, などを確認したり, コンパイルに失敗したコマンドがどういうコマンドライン だったかを知るのに有用です
筆者のたった2コア(4ハードウェアスレッド)のラップトップで
$ make -j 3 V=1
で実行すると, 5分ほどで終了します.
make install
$ make install
とすると, configure時に--prefixで指定したディレクトリ(inst) の下にプログラムがインストールされます. なお, make installがエラーで終了しますが, これは--prefixをつけてもなお, 一部のファイルを/usr の下, 書き込みにroot権限が必要な場所へインストールしようとするからで, 放っておいてもプログラムは動作します. inst/bin/gnumeric-1.12.37が実行可能ファイル本体, inst/bin/gnumericはそのシンボリックリンクです. uftraceやデバッガにかけるときは後者を使います.

ためしにuftraceで実行

ともかくuftraceで実行してみます.

$ cd inst/bin/
$ uftrace record ./gnumeric-1.12.37

するとgnumericが立ち上がります. 動作確認が目的なので, 少し何か入力したすぐに終了させます.

$ uftrace report

してみると, 多数(1366)の関数が実行されていることがわかります.

$ uftrace report
  Total time   Self time       Calls  Function
  ==========  ==========  ==========  =================================================
    9.756  s   38.206 us           1  main
    9.557  s    7.510  s           1  gtk_main
    1.659  s   14.529 us           1  wbc_gtk_close
    1.659  s   31.542 us           1  wbcg_close_if_user_permits
    1.606  s    1.533  s           1  wbcg_show_save_dialog
  222.785 ms   51.444 us           2  wbc_gtk_new
  136.786 ms  887.414 us           8  item_bar_draw_region
  133.912 ms  133.912 ms         308  ib_draw_cell
  128.812 ms  120.140 ms        1555  scg_scrollbar_config_real
  93.103 ms   65.638 ms           1  wbc_gtk_init

     ... 以降省略 ...

この関数名だけを眺めているだけでも答えらしきものが発見できる可能性はあります. が, 大量の関数名をただ眺めるだけでは辛いというときもあるでしょう. そこで前回紹介した, scriptを使って, ソースファイル名と行番号を表示させてみます.

$ uftrace script -S show_src_line.py > gnumeric_functions.txt

その結果を見ると, 関数名がひたすら並んでいるのと比べ, ファイル名が一種の分類指標になってくれていることがわかります. また, ファイル名だけを取り出すことも有用です.

$ cut -f 1 -d ' ' gnumeric_functions.txt > gnumeric_files.txt

結果は以下です(フルパス名は相対パスに置き換えて短縮しています).

gnumeric-1.12.37/src/application.c
gnumeric-1.12.37/src/auto-format.c
gnumeric-1.12.37/src/cell-draw.c
gnumeric-1.12.37/src/cell.c
gnumeric-1.12.37/src/cellspan.c
gnumeric-1.12.37/src/clipboard.c
gnumeric-1.12.37/src/collect.c
gnumeric-1.12.37/src/colrow.c
gnumeric-1.12.37/src/command-context-stderr.c
gnumeric-1.12.37/src/commands.c
gnumeric-1.12.37/src/complete-sheet.c
gnumeric-1.12.37/src/complete.c
gnumeric-1.12.37/src/dependent.c
gnumeric-1.12.37/src/dialogs/embedded-ui.c
gnumeric-1.12.37/src/expr-deriv.c
gnumeric-1.12.37/src/expr-name.c
gnumeric-1.12.37/src/expr.c
gnumeric-1.12.37/src/func-builtin.c
gnumeric-1.12.37/src/func.c
gnumeric-1.12.37/src/gnm-format.c
gnumeric-1.12.37/src/gnm-pane.c
gnumeric-1.12.37/src/gnm-plugin.c
gnumeric-1.12.37/src/gnm-sheet-slicer.c
gnumeric-1.12.37/src/gnm-so-filled.c
gnumeric-1.12.37/src/gnm-so-line.c
gnumeric-1.12.37/src/gnmresources.c
gnumeric-1.12.37/src/gnumeric-conf.c
gnumeric-1.12.37/src/gnumeric-simple-canvas.c
gnumeric-1.12.37/src/graph.c
gnumeric-1.12.37/src/gui-clipboard.c
gnumeric-1.12.37/src/gui-util.c
gnumeric-1.12.37/src/gutils.c
gnumeric-1.12.37/src/history.c
gnumeric-1.12.37/src/hlink.c
gnumeric-1.12.37/src/io-context-gtk.c
gnumeric-1.12.37/src/item-bar.c
gnumeric-1.12.37/src/item-cursor.c
gnumeric-1.12.37/src/item-edit.c
gnumeric-1.12.37/src/item-grid.c
gnumeric-1.12.37/src/libgnumeric.c
gnumeric-1.12.37/src/main-application.c
gnumeric-1.12.37/src/mathfunc.c
gnumeric-1.12.37/src/mstyle.c
gnumeric-1.12.37/src/number-match.c
gnumeric-1.12.37/src/parse-util.c
gnumeric-1.12.37/src/parser.c
gnumeric-1.12.37/src/parser.y
gnumeric-1.12.37/src/pattern.c
gnumeric-1.12.37/src/position.c
gnumeric-1.12.37/src/print-info.c
gnumeric-1.12.37/src/ranges.c
gnumeric-1.12.37/src/rendered-value.c
gnumeric-1.12.37/src/selection.c
gnumeric-1.12.37/src/session.c
gnumeric-1.12.37/src/sheet-autofill.c
gnumeric-1.12.37/src/sheet-control-gui.c
gnumeric-1.12.37/src/sheet-control.c
gnumeric-1.12.37/src/sheet-filter.c
gnumeric-1.12.37/src/sheet-merge.c
gnumeric-1.12.37/src/sheet-object-cell-comment.c
gnumeric-1.12.37/src/sheet-object-graph.c
gnumeric-1.12.37/src/sheet-object-image.c
gnumeric-1.12.37/src/sheet-object-widget.c
gnumeric-1.12.37/src/sheet-object.c
gnumeric-1.12.37/src/sheet-style.c
gnumeric-1.12.37/src/sheet-view.c
gnumeric-1.12.37/src/sheet.c
gnumeric-1.12.37/src/stf-export.c
gnumeric-1.12.37/src/stf.c
gnumeric-1.12.37/src/style-border.c
gnumeric-1.12.37/src/style-color.c
gnumeric-1.12.37/src/style.c
gnumeric-1.12.37/src/tools/gnm-solver.c
gnumeric-1.12.37/src/undo.c
gnumeric-1.12.37/src/validation.c
gnumeric-1.12.37/src/value.c
gnumeric-1.12.37/src/wbc-gtk-actions.c
gnumeric-1.12.37/src/wbc-gtk-edit.c
gnumeric-1.12.37/src/wbc-gtk.c
gnumeric-1.12.37/src/widgets/gnm-fontbutton.c
gnumeric-1.12.37/src/widgets/gnm-notebook.c
gnumeric-1.12.37/src/widgets/gnumeric-expr-entry.c
gnumeric-1.12.37/src/workbook-control.c
gnumeric-1.12.37/src/workbook-view.c
gnumeric-1.12.37/src/workbook.c
gnumeric-1.12.37/src/xml-sax-read.c
gnumeric-1.12.37/src/xml-sax-write.c
/usr/include/glib-2.0/glib/gstring.h
??
???

これを眺めて検討がつくか, ケースバイケースでしょうが, expr.cなどはいかにも怪しそうです. expr.c 内に並んでいる関数もいかにもという感じで, 半ば答えは 出たと言って良いかもしれません. たとえば do_expr_as_stringとか. ここまでわかったらあとは, デバッガでこの関数にブレークポイントをかけて実行するなどすれば, その周辺の動きがわかっていくこととおもいます.

$ grep expr.c gnumeric_functions.txt
gnumeric-1.12.37/src/expr-name.c 212 gnm_named_expr_collection_new @ 0xcb7de 4
gnumeric-1.12.37/src/expr-name.c 238 gnm_named_expr_collection_free @ 0xcb863 7
gnumeric-1.12.37/src/expr-name.c 380 gnm_named_expr_collection_lookup @ 0xcbd35 5
gnumeric-1.12.37/src/expr-name.c 425 gnm_named_expr_collection_insert @ 0xcbe6f 6
gnumeric-1.12.37/src/expr-name.c 483 gnm_named_expr_collection_check @ 0xcc115 10
gnumeric-1.12.37/src/expr.c 78 gnm_expr_new_constant @ 0xc17e1 27
gnumeric-1.12.37/src/expr.c 95 gnm_expr_new_funcallv @ 0xc1853 3
gnumeric-1.12.37/src/expr.c 112 gnm_expr_new_funcall @ 0xc18e0 3
gnumeric-1.12.37/src/expr.c 212 gnm_expr_new_binary @ 0xc1c19 4
gnumeric-1.12.37/src/expr.c 465 gnm_expr_free @ 0xc2319 34
gnumeric-1.12.37/src/expr.c 713 handle_empty @ 0xc2cf1 5
gnumeric-1.12.37/src/expr.c 806 bin_arith @ 0xc2f7b 2
gnumeric-1.12.37/src/expr.c 1230 gnm_expr_eval @ 0xc417e 10
gnumeric-1.12.37/src/expr.c 1657 do_expr_as_string @ 0xc5737 3
gnumeric-1.12.37/src/expr.c 2354 gnm_expr_get_range @ 0xc7237 15
gnumeric-1.12.37/src/expr.c 2480 do_expr_walk @ 0xc75e9 27
gnumeric-1.12.37/src/expr.c 2622 gnm_expr_walk @ 0xc7ba5 25
gnumeric-1.12.37/src/expr.c 2836 gnm_expr_top_new @ 0xc8292 21
gnumeric-1.12.37/src/expr.c 2852 gnm_expr_top_new_constant @ 0xc82f4 10
gnumeric-1.12.37/src/expr.c 2858 gnm_expr_top_ref @ 0xc831c 3
gnumeric-1.12.37/src/expr.c 2866 gnm_expr_top_unref @ 0xc836d 21
gnumeric-1.12.37/src/expr.c 2946 gnm_expr_top_get_range @ 0xc85e2 12
gnumeric-1.12.37/src/expr.c 2965 gnm_expr_top_as_gstring @ 0xc869f 1
gnumeric-1.12.37/src/expr.c 2988 gnm_expr_top_equal @ 0xc87ba 1
gnumeric-1.12.37/src/expr.c 3117 gnm_expr_top_eval @ 0xc8d96 6
gnumeric-1.12.37/src/expr.c 3150 cb_referenced_sheets @ 0xc8eec 18
gnumeric-1.12.37/src/expr.c 3187 gnm_expr_top_referenced_sheets @ 0xc8fc5 18
gnumeric-1.12.37/src/expr.c 3260 cb_get_boundingbox @ 0xc9245 3
gnumeric-1.12.37/src/expr.c 3294 gnm_expr_top_get_boundingbox @ 0xc9306 1
gnumeric-1.12.37/src/expr.c 3331 gnm_expr_top_is_array_corner @ 0xc9467 2
gnumeric-1.12.37/src/expr.c 3338 gnm_expr_top_is_array_elem @ 0xc94bf 2
gnumeric-1.12.37/src/expr.c 3419 _gnm_expr_init @ 0xc9669 1
gnumeric-1.12.37/src/expr.c 3451 _gnm_expr_shutdown @ 0xc9756 1
gnumeric-1.12.37/src/gnm-pane.c 2313 gnm_pane_expr_cursor_stop @ 0xdf21b 28
gnumeric-1.12.37/src/parse-util.c 713 gnm_expr_char_start_p @ 0x14772c 58
gnumeric-1.12.37/src/parse-util.c 1595 gnm_expr_conv_quote @ 0x1496c9 3
gnumeric-1.12.37/src/sheet.c 2705 sheet_range_set_expr_cb @ 0x17446c 1

実行の一部だけを記録する --- trigger関数を定義し, LD_PRELOADで注入する

以上で表示されたのはプログラムの開始から終了まで全期間で, 一度でも実行された関数全てです. 従って注目している, 「セル入力, 及びその値の評価」を する以外の部分が多く含まれています. 「セルに値を入力してから表示されるまで」 の期間のみを記録するために, uftraceの --disableおよび--triggerオプションを使ってみましょう. --triggerオプションを使うには, まず記録開始, 終了のきっかけとなる関数を定義しなくては なりません. gnumericのソースコード中に, 適当に空の関数を書き足してやってもいいのですが, ここでは ソースに手を触れないでも行える方法(LD_PRELOAD)を使います.

以下のようなソース(start_stop.c)を用意します.

/* start_stop.c */
void start_recording() { }
void stop_recording() { }
  

以下のようにコンパイルして, 共有オブジェクトファイル(.so)を作ります. ここでも-pgは必須です.

$ gcc -O0 -g -pg start_stop.c -fPIC -shared -o libstart_stop.so

gnumericを実行する際, 環境変数LD_PRELOADを セットして, libstart_stop.soにしておくと, プロセスにlibstart_stop.soが読み込まれ, 結果として, start_recording, stop_recording という二つの関数が定義されます. これらをuftraceの --triggerに指定してやれば良いです.

ためしに, --disable--triggerを以下のようにして実行すると, 何も記録されないということを確かめておきましょう.

$ LD_PRELOAD=libstart_stop.so uftrace record --disable --trigger=start_recording@trace_on --trigger=stop_recording@trace_off ./gnumeric-1.12.37
$ uftrace report
  Total time   Self time       Calls  Function
  ==========  ==========  ==========  ====================
  

--disableによって, 「プログラム開始時に記録を開始しない」 という動作になります. そしstart_recordingはこの実行では呼ばることは ありませんので, 結局記録は全くされないままプログラムが終了することになります. 残る問題は正しいタイミングで, start_recording, stop_recording を呼んでやることです. すなわち, gnumericがセルへの入力待ちになった ところでstart_recordingを, それが終わったところで stop_recordingを呼んでやりたいのですが, そのためにデバッガを使います.

デバッガでuftrace / gnumericを起動

さてここから先は, uftraceというよりも, 主にgdbの使いこなし方の話です.

gdb (デバッガ)を使うと, プログラムを一旦停止させて, 元のプログラムにかかれていないコードを実行させることができます. その仕組みは, 通常変数や式の値を表示させるために使っている, print コマンドです. あれは実は, printコマンドの 引数に与えた式を, プログラムの中で実行しています. 従って, gnumericが入力待ちになったところで実行を止めて,

  
(gdb) p start_recording()

としてやればその時点から記録が開始されます.

ところでこの, 「gnumericが入力待ちになったところで実行を止め」る ために, 少し面倒なのは, 今の場合gnumericを直接起動しているのではなく, uftraceが子プロセスとして起動しているところです. したがって, 直接gnumericをデバッガで(runコマンドで)実行しても, 所望の動作 (トレースの記録)をしてくれません.

そのために, すでに走っているプロセスを, あとからデバッガの 制御下におく(attachする)機能を使います. つまり一旦 uftrace を普通にコマンドラインから実行して, gnumericが立ち上がったらそれに attachします.

  1. # 普通に(コマンドラインから)uftrace/gnumericを起動
    $ LD_PRELOAD=libstart_stop.so uftrace record --disable --trigger=start_recording@trace_on --trigger=stop_recording@trace_off ./gnumeric-1.12.37
    
  2. 立ち上がったところで, gnumericのプロセス番号を調べてから, gdbを立ち上げる(端末か, Emacs内のM-x gud-gdbなど)
    $ ps auxww | grep gnumeric
    tau       9949  0.0  0.0 100368  3420 pts/0    Sl   16:15   0:00 uftrace record --disable --trigger=start_recording@trace_on --trigger=stop_recording@trace_off ./gnumeric-1.12.37
    tau       9950 12.7  0.6 689508 51380 pts/0    Sl   16:15   0:22 ./gnumeric-1.12.37
    tau      10004  0.0  0.0  15264   968 pts/0    S+   16:18   0:00 grep gnumeric
    
    プロセス番号9950とわかったところで,
      (emacsで) M-x gud-gdb
      Run gud-gdb (like this): gdb --fullname ./gnumeric-1.12.37
      (gdb) attach 9950
    Attaching to program: /home/tau/public_html/lecture/dive_to_oss/homepage/public/blog/gnumeric-1.12.37/inst/bin/gnumeric-1.12.37, process 9950
    [New LWP 9952]
    [New LWP 9953]
    [New LWP 9954]
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    0x00007f3ba5f48d8d in poll () at ../sysdeps/unix/syscall-template.S:84
    84	../sysdeps/unix/syscall-template.S: No such file or directory.
    
    ここでは, gnumericが入力待ち状態になったところをデバッガで捕まえていることになります. gnumericが停止しているので, この状態ではgnumericに入力しても反応しませんし, しばらくするとウィンドウがグレーアウトします.
  3. gdb内で
    (gdb) p start_recording()
    
    を実行して, 記録を開始
  4. (gdb) continue
    
    を実行して, gnumericを走らせる. ここでグレーアウトしていた窓が復活して, 入力に反応するようになります.
  5. セルに何か入力 (例: "=1+2")する
  6. gdbにctrl-c を入力して, gnumericを再び止め, gdbに制御を戻す
     ... ctrl-c を入力 ...
    (gdb)    ## gdbに制御が戻り, gdbコマンドが入力可能に
    
  7. あとはcontinueしてgnumericを通常終了させてあげましょう.
    (gdb) continue   # そして, gnumericを普通に終了させてあげる
    

これで記録が取れました. 少し手間が増えますが, アプリケーション全体ではない, 少し良質の記録です. 以下に生データを載せますが, 要約すると,

です(カッコ内は, プログラム全体を記録したときの数). ので, 思ったほど劇的な効果はありませんでした. というよりも, セルに入力するだけで, 実行される関数の数が710, それが66のファイルにまたがっているというのは, なかなかドキドキさせられる数字です.

現時点でuftraceが対応していないプログラム

2017年12月12日の時点での情報です. C++の例外を, rethrow するプログラム (catchブロック内で受け取った例外をまたthrowする)が上手く動かないようです.

try {
  ...
} catch (const my_exception& ex) {
  ...
  throw;
}

このissueは ファイル済みで, うまくすればすぐになおるかもしれません. uftraceは関数の先頭と終了で記録のためのコードを実行します. 先頭に関しては, GCC-pgオプションをつけて実行すると, mcount()という関数の呼び出しが挿入されるので, mcount関数をuftrace提供のものに置き換えて, そこで記録を実行しています. 終了時は特にそのようなコードは入れてくれないので, uftraceでは関数の戻り番地を変更して関数が戻るとそこで uftraceに制御が渡るような仕掛けが入っています. そのためデバッガで見るとスタックトレースが正しく表示されませんし, スタックトレースを追うことに依存した関数(backtraceとか)も, そのままでは正しく動きません. 実は例外処理もその一つです. uftraceではbacktrace関数や例外処理を正しく動作させるために, それらの関数が呼び出されたところで戻り番地を, 元の(uftrace 無しで動作しているのと同じ)状態に戻してから実行する, ということをしています.

実はこのissueにより, LibreOffice Calcのトレースに失敗します. 実はこれが, 今回LibreOfficeではなくgnumericを対象にした裏事情です.

追記:

上記issueをファイルしてから数日で, 著者から連絡がありました. rethrowをするプログラムに対応したというものです. 早速やってみました. ファイルしたモデルケースについては治っていましたが, 残念ながらLibreOffice Calcのトレースは依然としてできませんでした (それがrethrowのせいか, 別のissueかも未調査).

おわりに

ソフトの中身を解析するリバースエンジニアリングツールとしては, デバッガを使うのが一般的でしょう. ある機能に関連した関数名が最初からわかっていればそこにブレークポイントを 貼るなどすればよいわけですが, それがわからない場合, デバッガでは, main関数から順に実行していくのが基本になります. しかしそれでは目標とする場所になかなか到達できない場合があります.

  • 純粋にステップ数が多すぎるという場合もあるでしょう
  • また, 既存ライブラリなど, デバッグシンボルのない状態でパッケージ化された関数は追跡できません
  • 従ってそのようなライブラリからコールバックされる関数へ到達するのも困難です (注: これ自身は, デバッグシンボルパッケージとソースパッケージを入れることで原理的には 解決可能だが, ステップ数が多すぎて手間がかかりすぎるという問題は解決できない)
  • 他にも複数スレッドや複数プロセスからなるプログラムなど, デバッガで丁寧に追いかけていくアプローチが苦しい状況が存在します

GUIプログラムはこの典型で, 何しろプログラムの大部分はGUIライブラリからのコールバックとして実行されます. しかもセルへ入力費とするだけでも, やれwindowにフォーカスが来ただの, 外れただの, 無関係なイベントが大量に実行されます. 従ってデバッガでステップ実行して所望の場所を特定するのが困難です. そんな時, 実行された関数を片っ端から記録, 表示してくれるプロファイラ, トレーサは, 非常に有用なツールになります. 一旦関係しそうな関数を突き止めたら, デバッガでブレークポイントを貼るなどして, 詳細調査ができます.

大規模ソフトウェアを手探る という授業をやっており, オープンソースのソフトウェアに機能拡張などの変更を加える, ということを課題にしています. uftraceは, LibreOffice Calcのように上手く行かなかった例もありますが, 上手く動けばそのための強力なリバースエンジニアリングツールとなりそうです.

uftrace を使ってプログラムの中身を解析/リバースエンジニアリングする (1) uftrace 紹介編

はじめに

Linuxuftraceというツールを使うと, あるプログラムを実行している際の関数呼び出しの回数や 呼び出しの履歴(呼び出された順番)を記録, 表示することができます.

この記事では, uftraceの使い方と, それを使って中身のよくわからない プログラムの中身を調査する(リバースエンジニアリングする) 方法を紹介します. 例題として, スプレッドシートプログラムgnumericのセルに 式を入力した際, その中身を読み取っている関数名や, そのソースコードの場所を突き止めることを目標にします (例えばgnumericに新しい関数を付け加えるという変更を したいと想像してください).

このように, あるプログラムの機能変更や拡張をしたいというときに, ある機能を担っているのは, どのソースファイルや関数なのか を調べるという調査がしばしば必要になります. この時, uftraceのプロファイリング機能(具体的には呼び出された 関数を表示してくれる機能)が有用になります.

もともとプロファイリングという機能は各関数の実行時間 を記録するなどして, プログラム最適化の助けとするというのが 普通の使い方ですが, ここで紹介するような, 「ある期間に実行された関数名を列挙したい」というような 目的にも使うことができます.

GCCのプロファイル機能(-pg)と少し似ていますが, GCCのプロファイル機能は, 共有ライブラリ のプロファイリングには対応していないという 致命的な欠点があります. uftraceは共有ライブラリ にも対応していますし, プロファイリングだけでなく, トレーシング(関数が呼び出された順番を全て記録する) も行うことができます. プログラムの一部だけを記録する 方法もしっかり定義されていますし, Pythonスクリプトと組み合わせて表示をカスタマイズする こともできる, 有用なツールです.

uftraceのインストール

方法1: パッケージ管理システムで導入
Ubuntuであれば,
$ sudo apt install uftrace
で導入できます(Ubuntu 17.04で確かめています. 他のバージョンや他のディストロの状況は知りません. 悪しからず).
方法2: ソースからビルド
$ git clone https://github.com/namhyung/uftrace.git (またはgit@github.com:namhyung/uftrace.git)
$ cd uftrace
$ ./configure --prefix=PREFIX
$ make
$ make install
make installが成功した時点で PREFIX/bin/uftrace, PREFIX/lib/libmcount.so などのファイルができているはずです.

uftraceの実行

あるプログラムをuftraceで調査するのは簡単で,

  1. プログラムをコンパイルする際に GCC-pgオプションを付けてコンパイルする
  2. できたプログラムを実行する際に,
    $ uftrace record コマンドライン
    
    として実行する
  3. 記録を表示するために,
    $ uftrace report
          
    とする.

だけです. -pgオプションを付けるところまでは, GCCのプロファイル機能と同じです. uftrace recordは, デフォルトではuftrace.dataという フォルダを作りその中に色々な情報を記録します. uftrace report はそれを読んで表示しています.

実行例

  • 例として以下のプログラムを準備
    int g() {
    
    }
    
    int f(int n) {
      for (int i = 0; i < n; i++) {
        g();
      }
    }
    
    int main() {
      int n = 100;
      for (int i = 0; i < n; i++) {
        f(i);
      }
      return 0;
    }
    
  • コンパイル
    $ g++ -O0 -g -pg a.c
    
    中身が空の関数呼び出し(g()など) が最適化によって消されないようにするため, -O0をつけています.
  • 実行
    $ uftrace record ./a.out
    $ ls
    a.c  a.out*  uftrace.data/  uftrace.html
    
    実行後, uftrce.dataができていることを確認しています.
  • 表示
          
    $ uftrace report
      Total time   Self time       Calls  Function
      ==========  ==========  ==========  ====================
      747.367 us    8.668 us           1  main
      738.699 us  524.547 us         100  f
      214.152 us  214.152 us        4950  g
        4.657 us    4.657 us           1  __monstartup
        0.859 us    0.859 us           1  __cxa_atexit
    
    fが100回, gが4950 (= 0 + 1 + 2 + ... + 99)回 実行されていることが確認できました.

トレースの表示

uftraceは, 各関数の呼び出し回数だけでなく, どの関数がどの順番に実行されたか(トレース)までを記録しています. それを表示するには, uftrace replayというコマンドを使います.

$ uftrace replay
   4.657 us [ 8756] | __monstartup();
   0.859 us [ 8756] | __cxa_atexit();
            [ 8756] | main() {
   0.074 us [ 8756] |   f();
            [ 8756] |   f() {
   0.068 us [ 8756] |     g();
   0.318 us [ 8756] |   } /* f */
            [ 8756] |   f() {
   0.047 us [ 8756] |     g();
   0.043 us [ 8756] |     g();
   0.432 us [ 8756] |   } /* f */
            [ 8756] |   f() {
   0.046 us [ 8756] |     g();
   0.053 us [ 8756] |     g();
   0.045 us [ 8756] |     g();
   0.598 us [ 8756] |   } /* f */
            [ 8756] |   f() {
   0.045 us [ 8756] |     g();
   0.043 us [ 8756] |     g();
   0.036 us [ 8756] |     g();
   0.044 us [ 8756] |     g();
   0.620 us [ 8756] |   } /* f */
      ...  以下省略 ...
  

recordとreplayを一回のコマンドで行うコマンド(live)もありますし, 実はそれが省略時の動作です.

関数の定義場所(ソースコードの位置)を取得する

uftraceは呼び出された関数名を表示してくれます. リバースエンジニアリングに使う際は, 関数名を見ただけで 目的の関数の見当がつく場合も有りますが, そのソースコード上の位置(このファイルの何行目)情報も 表示できるとなお有用になります. uftraceにはそのための 組み込みの機能はありませんが, トレース結果をPython scriptに渡してくれるという仕組みがあります.

$ uftrace script -S my_script.py

とすると, uftrace.dataフォルダ内に保存されたトレース記録 (uftrace recorduftrace replayが参照するのと同じデータ) を, Pythonスクリプトに順々に渡してくれます. その中に, 関数のアドレスが 入っており, それを上手く調理することでソースコード上の位置を取得できます.

なお, uftrace自身を自分でビルドした人は, ビルド時に libpython2.7 というパッケージがないと機能が無効化されていて, libpython2.7がない, みたいなエラーメッセージが出てきます.

$ uftrace script
WARN: script command is not supported due to missing libpython2.7.so

uftrace script の詳細は このページ にあります. また, それを使って関数のソースコード上の位置を表示するスクリプト を作りました. この詳細説明は割愛します. uftrace recordの代わりに, uftrace script -S show_src_line.pyとしてください. なおこのスクリプトは, アドレスからソースファイル名+行番号への変更を, binutilsパッケージの addr2lineコマンドを使ってやっていますのでインストールされていなかったらしてください.

$ sudo apt install binutils
$ which addr2line
/usr/bin/addr2line
$ uftrace record ./a.out
$ uftrace script -S show_src_line.py
src_file line_no fun_name @ addr count
/home/tau/.../blog/a.c 1 start_recording @ 0x7ca 1
/home/tau/.../blog/a.c 2 stop_recording @ 0x7d7 1
/home/tau/.../blog/a.c 6 g @ 0x7e4 4950
/home/tau/.../blog/a.c 8 f @ 0x7f5 100
/home/tau/.../blog/a.c 15 main @ 0x828 1
?? 0 __cxa_atexit @ 0x628 1
?? 0 __monstartup @ 0x620 1

(表示のうちのパス名の部分は適当に編集しています). 1カラム目がファイル名, 2カラム目が行番号です. 最後のカラムが呼び出し回数です.

実行の一部だけを記録する

uftraceのデフォルトの動作ではプログラム実行全体に渡り記録がとられますが, 一部の区間だけを記録したいということもしばしばあります. 性能測定であれば特定の関数の実行中だけを記録したいということがあるでしょうし, ある機能を実現している関数を見つける(リバースエンジニアリング)目的では, その機能が呼び出されている間だけを記録したいということになります. uftraceはそのような測定にも対応してくれており,

--disable :
プログラム開始時に測定を開始しない
--trigger=関数名@trace_on, --trigger=関数名@trace_off :
指定された関数が呼び出されたところで測定開始
--trigger=関数名@trace_on :
指定された関数が呼び出されたところで測定終了

を組み合わせて使います.

例えば我々の例題プログラムでf(0), f(1), f(2), ..., f(99)fが100回呼び出されるうちの, f(10)のところだけを 記録したいという場合であれば以下のようにします.

  • プログラムを以下のように修正 そしてコンパイル
    void start_recording() { }
    void stop_recording() { }
    
    int g() {
    
    }
    
    int f(int n) {
      for (int i = 0; i < n; i++) {
        g();
      }
    }
    
    int main() {
      int n = 100;
      for (int i = 0; i < n; i++) {
        if (i == 10) start_recording();
        f(i);
        if (i == 10) stop_recording();
      }
      return 0;
    }
    
    $ gcc -pg -O0 -g a.c
    
  • uftrace実行時に以下のようにして実行
    $ uftrace record --disable --trigger=start_recording@trace_on --trigger=stop_recording@trace_off ./a.out
    
  • すると結果は以下のようになり, たしかにf(10)だけが 記録されているということがわかります.
    $ uftrace report
      Total time   Self time       Calls  Function
      ==========  ==========  ==========  ====================
        3.514 us    2.651 us           1  f
        0.863 us    0.863 us          10  g
        0.232 us    0.232 us           1  start_recording
    
    $ uftrace replay
    # DURATION    TID     FUNCTION
       0.232 us [14846] | start_recording();
                [14846] | f() {
       0.100 us [14846] |   g();
       0.101 us [14846] |   g();
       0.074 us [14846] |   g();
       0.077 us [14846] |   g();
       0.089 us [14846] |   g();
       0.080 us [14846] |   g();
       0.076 us [14846] |   g();
       0.094 us [14846] |   g();
       0.086 us [14846] |   g();
       0.086 us [14846] |   g();
       3.514 us [14846] | } /* f */
    

ここまでのまとめ

  • uftraceは偉大な性能測定/リバースエンジニアリングツール
  • 対象プログラムを gcc -pgコンパイル
  • uftrace record コマンド ... 」で実行
  • uftrace report またはuftrace replayで表示
  • ソースファイル名や行番号を表示したいなら, script機能を使う
  • 一部だけを記録したいなら, uftrace --disable --trigger=F@trace_on --trigger=G@trace_offとして, 記録開始時にF, 記録終了時にGを呼ぶ. 中身はなんでも(空でも)良い.

次回はこれを使って, gnumericをリバース・エンジニアリングします.