uftrace を使ってプログラムの中身を解析/リバースエンジニアリングする (2) gnumericへ適用編
uftraceでgnumericの中を探る
前回の記事でuftraceについて紹介しました.
今回はそれを使ってgnumericの中身を探る, 具体的には,
セルに入力したときにそのセルの文字列を読み取っている関数を突き止める
ということを目標にします.
Linux上で表計算と言うと, LibreOfficeの方がよく使われているとおもいますが, gnumericの方が軽く, コンパイルもすぐに終わります (gnumericなら数分で終わりますが, LibreOfficeは数時間です). LibreOfficeにはCalc以外のプログラム(WriterやImpress)も含まれているでしょうから, フェアな比較ではありませんが, リバースエンジニアリングの例題としては gnumericの方が手軽です.
実行環境
- Ubuntu 17.04 です
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オプションが無事行き渡っているか, などを確認したり, コンパイルに失敗したコマンドがどういうコマンドライン だったかを知るのに有用です
$ 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します.
-
# 普通に(コマンドラインから)uftrace/gnumericを起動 $ LD_PRELOAD=libstart_stop.so uftrace record --disable --trigger=start_recording@trace_on --trigger=stop_recording@trace_off ./gnumeric-1.12.37
- 立ち上がったところで, 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に入力しても反応しませんし, しばらくするとウィンドウがグレーアウトします. - gdb内で
(gdb) p start_recording()
を実行して, 記録を開始 -
(gdb) continue
を実行して, gnumericを走らせる. ここでグレーアウトしていた窓が復活して, 入力に反応するようになります. - セルに何か入力 (例: "=1+2")する
- gdbにctrl-c を入力して, gnumericを再び止め, gdbに制御を戻す
... ctrl-c を入力 ... (gdb) ## gdbに制御が戻り, gdbコマンドが入力可能に
-
(gdb) p stop_recording()
を実行して記録を停止します. - あとは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のように上手く行かなかった例もありますが, 上手く動けばそのための強力なリバースエンジニアリングツールとなりそうです.