(2)で採取した「カーネルメモリダンプ(kernel.dump)」ファイルを直接参照して、対象のpageのアドレスを探すのは不可能ではありませんが、少し骨が折れます。そこで、このメモリダンプファイル内のデータ参照は、gdbの強力なメモリ参照機能を利用することにしました。
ただし、単純にこのダンプファイルをgdbの引数に与えても、gdbが認識できるフォーマット形式ではないのでアクセスできません。gdbが認識できるフォーマットは「コアダンプファイル(コアファイル)」という形式なのです。そこで、いろいろと思案した結果、あまりエレガントな手法ではありませんが、以下の方法で対処することにしました。
「自前で作ったユーザープロセスにkernel.dumpファイルをメモリ上に読み込みさせた後、故意に異常終了(SIGSEGV)させて、コアファイルを出力させる」。
そのイメージを図9に示します(自前のプロセスの名前は「createCore」とします(注9))。
こうすれば、カーネルのメモリダンプを含んだコアファイルが自動的に作成されることになり、gdbはこのコアファイルを参照し、カーネルメモリダンプのアクセスが可能になる、という具合です。それでは、実際にカーネルメモリダンプをcreateCoreプロセスに渡して、コアファイルを作成してみましょう。
$ ./createCore kernel.dump ……kernel.dumpをcreateCoreプロセスで読み込み、SIGSEGVを発生させる dump_ptr=0xb1d48008 ……kernel.dumpの先頭アドレスはユーザー空間の0xb1d48008からはじまる now dumping core... Segmentation fault(コアダンプ) $ ls -lh core -rw------- 1 tmori tmori 91M 2010-12-12 11:26 core ……コアファイルができていることを確認
ただし、このやり方には1つ問題があります。それは、カーネルメモリダンプ内に存在するアドレスは、すべてカーネルアドレス空間(0xc0000000以上のアドレス)のものであり、createCoreプロセスのアドレス空間はユーザー空間にあるという点です。つまり、カーネルメモリダンプ内のメモリにアクセスしようとしても、アクセス領域が実際のユーザープロセスのものとは異なるため参照できないのです(図10)。
そこで、追加対策として、gdbからのメモリアクセス時には、カーネル空間のアドレスをユーザー空間にマッピングすることにしました。
なお、メモリアクセスのたびに手動でマッピングを行うことは面倒なので、gdbのユーザー定義コマンドを利用することにしました(注10)。今回追加したコマンドで主要なものは表2のとおりです。
これでgdbを使って、シームレスにカーネルのメモリダンプ情報を参照できるようになったわけです。それでは、早速メモリ探索をはじめましょう。
$ gdb vmlinux core ……メモリダンプの解析開始
まず、最初にtarfsの起点となる「file_system_type」構造体のデータを参照します。
ここから、fs_supersのメンバをたどることで、マウントしている「super_block」構造体のデータを参照できます。ただし、このアドレスはsuper_block構造体の先頭アドレスではなく、「s_instances」というメンバのアドレスになりますので、以下のコマンドでsuper_block構造体のデータを参照します。
super_block構造体には「s_root」というメンバがあり、ここにtarfsのルートディレクトリの「dentry」のアドレスが格納されています。今回復旧するinit.rcというファイルは、ルートディレクトリ直下にありますので、「dentry_childs」コマンドを使用すれば、対象ファイルの候補を探すことができます。
この結果から、ルートディレクトリ直下には3つのファイルが存在することが分かります。それでは、先頭のdentryを参照し、ファイル名を見てみましょう。
運よく、先頭のdentryがinit.rcであることが分かりました。それでは、「d_inode」のアドレスを参照してみます。
次に、inodeのi_mappingから「address_space」構造体を参照します。
そして、rnodeのアドレスを参照すれば基数ツリーノードを見ることができるわけです。ここで、このアドレスが奇数になっていることに気付かれたでしょうか? 通常、奇数のアドレスなど許されません。実は、0ビット目に「1」が設定されている場合は、radix_tree_nodeのデータがあるという意味なのです(そうでない場合はpageが入っています)。とてもトリッキーなルールなのですが、とにかく、1を引いたアドレスをradix_tree_nodeでキャストして、データ参照してみましょう。
slots配列に4個のアドレスがあります。これがinit.rcに割り当てられているpageなのです。やっと見つかりました!
init.rcのpageのアドレスが分かりましたので、後はそこから対応するページフレーム番号を求めればよいわけです。
結果は以下のとおりです(表3)。
index番号 | pageアドレス | ページフレーム番号 | |
---|---|---|---|
0 | 0xc0556500 | 16296 | |
1 | 0xc0519f00 | 8568 | |
2 | 0xc055d2a0 | 17173 | |
3 | 0xc05603a0 | 17565 | |
表3 結果 |
ページフレーム番号は、kerne.dumpファイルのオフセット(4kbytes単位)と同じですから、「dd」コマンド(注13)を使って、ページフレーム番号のデータを抽出し、データを再構成します。
$ dd if=tarfs.dump bs=4096 skip=16296 count=1 > tmp.dump $ dd if=tarfs.dump bs=4096 skip=8568 count=1 >> tmp.dump $ dd if=tarfs.dump bs=4096 skip=17173 count=1 >> tmp.dump $ dd if=tarfs.dump bs=4096 skip=17565 count=1 >> tmp.dump
これでinit.rcのページフレームデータをすべて抽出できました。ただし、これだとファイルサイズが元のもの(12823bytes)より大きいので、以下のコマンドで、余分な領域を削ったファイルを作成すれば完了です!
$ dd if=tmp.dump of=init.rc bs=1 count=12823
では、このファイルを早速見てみましょう(図11)。
ちゃんとデータが見えますね〜。
最後に、Android端末上のinit.rc(ファイル名はandroid_init.rcとして変名)との差分がないか「diff」コマンドで検査してみましょう。
$ diff android_init.rc init.rc $ echo $? 0
ばっちり差分はありません!
以上、tarfsを実装するために必要となるファイルシステムの基礎的な内容は説明し終わりました。細かなことを書き出すともっと長くなってしまいますが、ここまで読み進められた読者の方ならLinuxカーネルの専門書を片手にLinuxカーネルソースをきっと読破できるものと信じております。
さて、次回は、tarfsの実装方法について解説します。お楽しみに! (次回に続く)
Copyright © ITmedia, Inc. All Rights Reserved.