(注意)このブログは本家のほうの文章部分のみの転載です.ソースコードの配布,画像などについては本家のほうを参照してください.文章中のリンク先は面倒なのですべて本家のほうに変換してしまっているのでご注意ください.

前回は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)[+]

上のコマンド群は,こーいうふうに読みます.
  • 1行目... mコマンドで,gdbが0x8048088のアドレスから1バイトのデータを読むように要求した.
    → スタブが応答し,0x55という値を返した.
  • 2行目... Xコマンドで,gdbが0x8048088のアドレスにゼロバイトのデータを書き込むように要求した(これはgdbが,Xコマンドが使えるかのテストを行っているようだ).
    →スタブでXコマンドが実装されていないので,できませんと返した.
  • 3行目... Mコマンドで,gdbが0x8048088のアドレスに0xccという1バイトのデータを書き込むように要求した.
    → ここでとつぜんスタブ側でシグナル発生し,Tコマンドによってシグナル番号0x0b(SIGSEGV)を返している.
Xコマンドが使えるかどうかテストしたあとに,ダメだとわかってMコマンドでの書き込みに切替えている.Xコマンドはデータをバイナリで送るので効率が良いのだが,ダメだったのでMコマンドでの動作に切替えているようだ.こーいうふうにgdbは,スタブ側で実装されているコマンドを調べて,実装されていないならばもっと基本的な(しかし効率はあまりよくない)コマンドでの指示に切替える,ということをよく行う.なので,コマンドはなんでもかんでもがんばって対応させる必要は無かったりする.

で,問題は 0x8048088 に何を書き込もうとしているのかなのだけど,これを調べるには,実行形式 koz のメモリマッピングを知る必要がある.これは readelf コマンドで知ることができる.

% readelf -a ./koz

で,ELF形式とか readelf とかについて説明しようかなーとも思ったのだけど,実はこのへんはこの記事ですっげー詳しく説明されていて,まあぜひそっちを読んでください.ちなみに以下が,readelf の出力結果.結論から言ってしまうと,0x8048088 はテキスト領域だ.うーんわかりやすくいうと,実行形式の機械語命令がマッピングされている領域だ.

で,gdbが機械語命令部分になにを書こうとしているかというと,0xccという命令を書きにいっているわけなのだが,i386のアセンブラがよくわからんのでこれがなんの命令なのかがちょっとわからない.まあ調べてもいいのだけど,ここでもっと気をつけなければならないのは,

「gdbの実機制御の都合上,命令書き換えなどをgdbが勝手に行うことがある」

という事実だ.実際にリモートデバッグをいろいろやってみるとわかるのだけど,gdbはまあいろいろなことを裏でやっていて,命令書き換えなどはあたりまえのようにやっている.

たとえばブレークポイントで停止して continue するような動作について説明しよう.ブレークポイントの設定からcontinueまでの一連の動作を説明すると,なんと以下のようなことをやることになる.
  • ブレークしたい位置の命令を,(Mコマンドなどにより)トラップ命令に置き換える.
  • トラップ命令が実行されると割り込みが上がり,スタブに処理が渡る.
  • gdbでcontinueを実行する.
  • gdbはトラップ命令に置き換えた部分を元に戻し(でないと実行開始直後にまたブレークしてしまうから),CPUのステップ実行フラグ(たいていのCPUが持っていて,1命令実行するごとに割り込みが入る)を立てて実行を再開する.
  • ステップ実行フラグが立っているので,1命令実行ごとに毎回gdbに処理が渡る.gdbは処理がどこまで進んだかを見ながら実行再開を繰り返し,じりじりと実行を進める.
  • 実行がC言語のソース上での次の行(アセンブラ上の次の命令,だったかもしれない)に渡ったら,ブレークポイントを再度トラップ命令に置き換える(でないと次回ブレークできなくなってしまうので).
  • ステップ実行フラグを落とし,処理を再開する.
どうでしょう? 単なる continue でも,これだけ複雑なことをやっているのだ.まあよく考えれば当り前なのだけど,これを初めて知ったときは,gdbって頭良いなーと感心したものだ.

というわけで何が言いたいかというと,上のようにgdbが制御の都合上,裏でいろんなことをやるので,命令書き換えなどができるようになっていないとダメだということだ.ところがFreeBSD上,ていうか普通の汎用OS上では,仮想メモリが動作して機械語コード(テキスト領域)には書き込み不可のガードがかかるのが普通だ.で,書き込もうとすると segmentation fault で落ちることになる.これをなんとかしなければならない.

これをどうするかなのだけど,ここでもこの記事が出てくる.この連載中に,ELF形式を解釈してテキスト領域を書き込み可にして命令書き換えを行うという,まんまそのままの記事があるのだな.まあ自分で書いた記事なのだが,ぜひ読んでください.

で,詳しい説明は上記記事に譲るとして,ELF形式のプログラムヘッダを読んでテキスト領域を書き込み可にするツールをちょちょっと作ってみた.使いかたは実行形式を指定するだけ.で,t2w を使うように Makefile を修正したのが以下のソースコード.前回からの差分は 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 2007

koz 側で時刻表示が始まった.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対応を進めるのも限界かと一時は思った程だった.この解決策は次回!