前回はgdbと接続できたが,continueをしても動作継続できないという問題があった. その原因はなんだろうか?
前回の最後のほうに書いたけれど,continue を実行すると
[$M8048088,1:cc#1e](+)($T0b4:5ce3bfbf;5:78e3bfbf;8:ab8b0408;#66)[+]というgdbコマンドが発行されている.これがヒントなのだけど, どうでしょう,わかるでしょうか?
もうちょっとまわりを詳しく見てみよう.continue した直後には,以下のような gdbコマンドが発行されている.
[$m8048088,1#3e](+)($55#6a)[+] [$X8048088,0:#62](+)($#00)[+] [$M8048088,1:cc#1e](+)($T0b4:5ce3bfbf;5:78e3bfbf;8:ab8b0408;#66)[+]上のコマンド群は,こーいうふうに読みます.
で,問題は 0x8048088 に何を書き込もうとしているのかなのだけど, これを調べるには,実行形式 koz のメモリマッピングを知る必要がある. これは readelf コマンドで知ることができる.
% readelf -a ./kozで,ELF形式とか readelf とかについて説明しようかなーとも思ったのだけど, 実はこのへんはこの記事で すっげー詳しく説明されていて,まあぜひそっちを読んでください. ちなみに以下が,readelf の出力結果.
結論から言ってしまうと,0x8048088 はテキスト領域だ.うーんわかりやすくいうと, 実行形式の機械語命令がマッピングされている領域だ.
で,gdbが機械語命令部分になにを書こうとしているかというと, 0xccという命令を書きにいっているわけなのだが, i386のアセンブラがよくわからんのでこれがなんの命令なのかがちょっとわからない. まあ調べてもいいのだけど,ここでもっと気をつけなければならないのは,
「gdbの実機制御の都合上,命令書き換えなどをgdbが勝手に行うことがある」
という事実だ.実際にリモートデバッグをいろいろやってみるとわかるのだけど, gdbはまあいろいろなことを裏でやっていて,命令書き換えなどはあたりまえのように やっている.
たとえばブレークポイントで停止して continue するような動作について説明しよう. ブレークポイントの設定からcontinueまでの一連の動作を説明すると, なんと以下のようなことをやることになる.
というわけで何が言いたいかというと,上のようにgdbが制御の都合上, 裏でいろんなことをやるので,命令書き換えなどができるようになっていないと ダメだということだ.ところがFreeBSD上,ていうか普通の汎用OS上では, 仮想メモリが動作して機械語コード(テキスト領域)には書き込み不可の ガードがかかるのが普通だ.で,書き込もうとすると segmentation fault で 落ちることになる.これをなんとかしなければならない.
これをどうするかなのだけど,ここでも この記事が出てくる. この連載中に,ELF形式を解釈してテキスト領域を書き込み可にして 命令書き換えを行うという,まんまそのままの記事があるのだな. まあ自分で書いた記事なのだが,ぜひ読んでください.
で,詳しい説明は上記記事に譲るとして,ELF形式のプログラムヘッダを読んで テキスト領域を書き込み可にするツールをちょちょっと作ってみた.
使いかたは実行形式を指定するだけ. で,t2w を使うように Makefile を修正したのが以下のソースコード.(2009/04/10 ライセンスに関する文書として,KL-01とLICENSEを追加. 詳しくは第43回を参照)
前回からの差分は Makefile だけで,t2wを使うようにしただけなので まあ見ておいてほしい.前回からの差分は,上記ソースコードの diff.txt 参照.
ちなみに以下が,t2wで書き込み可にしたあとの実行形式kozの readelf の出力結果.
まああんましよくわからんかもしれないが,Program Headers: Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align LOAD 0x000000 0x08048000 0x08048000 0x6779a 0x6779a RWE 0x1000 LOAD 0x0677a0 0x080b07a0 0x080b07a0 0x0201c 0x1629c RW 0x1000という部分で,LOADされるひとつめの領域のフラグがRWEとなっていて, 書き込み可になっていることに注目してほしい.(さっきはREだった)
では,実行してみよう. まずはgdbで接続するまでは前回と同じ.
前回と同じところ(stubdのaccept()の直後の kz_break())でブレークしている.
continue してみよう.
Thu Nov 8 00:28:39 2007 Thu Nov 8 00:28:40 2007 Thu Nov 8 00:28:41 2007 Thu Nov 8 00:28:43 2007 Thu Nov 8 00:28:44 2007 Thu Nov 8 00:28:45 2007 Thu Nov 8 00:28:46 2007 Thu Nov 8 00:28:47 2007 Thu Nov 8 00:28:48 2007 Thu Nov 8 00:28:49 2007koz 側で時刻表示が始まった.gdb側も,continue のあとに待ち状態になっている. 無事に continue できたようだ.
telnet接続してみよう.
% telnet 192.168.0.3 20001 Trying 192.168.0.3... Connected to 192.168.0.3. Escape character is '^]'. > date Thu Nov 8 00:29:38 2007 OK >おー,ちゃんと接続できた. dateで時刻表示もできている.
次に,down コマンドで segmentation fault を発生させてみる.
> date Thu Nov 8 00:29:38 2007 OK > down
おー,gdb側でコマンドプロンプトが出てきた. スタブからgdbにシグナルが渡り,gdbがダウン位置のソースコードを表示している.
...というのを期待していたのだが,よく見てほしい. シグナルハンドラ内部の setjmp() の位置で停止している. 考えてみたら,stubd の最初のブレークでもそうだった.
むむっっ!これは大問題だ!考えてみれば当り前のことなのだけれど, スレッドのコンテキスト保存は setjmp() によって行っているので, スタブ内部でレジスタ情報を参照したときに見えるのは,setjmp() した瞬間の レジスタ情報だ!ということは,ブレークした位置を表示させることができないと いうことだ!
これは実は大問題で,いろいろと回避策を考えたのだが,これ以上KOZOSのgdb対応を 進めるのも限界かと一時は思った程だった.この解決策は次回!