Posted by & filed under 開発.


オブジェクトについて抽象から具象まで取り混ぜて説明していた時、最も具象なレベルで見れば、オブジェクトはメモリ上に確保した領域にすぎないと説明しました。

そんな説明をしていた時、メソッドの実体ってどこにどうあるのですかと質問を受けました。人の心はどこにどうあるのですかという質問に比べると緩い質問ですが、良い質問だと思いました。こういう疑問を持つのは大事だと思うからです。自分もかつてプログラムとは結局のところどう実行されるのかが気になりました。プログラマなら誰もが通る道だと思います。

そんなわけでJavaのような箱入り娘から離れて、デレのないツンデレ娘ことC言語で古のテクニックを見せることにしました。

とりあえず次の簡単なコードから始めます。C言語は知らなくても構いません。関数fnがあって、引数に2を加算して返すことだけを読み取ってください。

念のため実行してみます。

これ以降はコードを簡潔にするため警告を黙らせるキャストを省略します。良い習慣ではないのでマネはしないでください。

実行ファイルxをgdb(デバッガ)で起動してみます。

gdb上のdisasコマンドでディスアセンブルできます。関数fnの実体のアセンブリコードを見える化できます。

次のようにすると、関数fnの実体がメモリ上でたんなる数値の列(バイト列)にすぎないと分かります。後でこの数値と上記のアセンブリコードの対応をobjdumpコマンドで見ます。

最初のコードを次のように変更してみます。

再コンパイル後、gdbで書き換わっていることを確認してみます。0x02があった部分が0x03になっているのを確認してください。

関数fnのバイト列は実行ファイルxの中に見つかります(Javaで言えば、JVMのバイトコードのバイト列がクラスファイルの中に見つかるのと同じ話です)。

実行ファイルxを解析するにはobjdumpが便利です(nmコマンドでもほぼ同じようなことができますがobjdumpのほうがリッチです)。

objdumpに-dオプションを渡すとディスアセンブルできます。gdbで見たバイト列とアセンブリコードの対応を確認できます。

このバイト列を実行ファイルxの中から探してバイナリパッチを当ててみます。バイナリエディタでバイト列を探してもいいですが、objdumpを使うとファイル中での正確なオフセットを計算できます。

objdumpを次のように-xオプションで起動します。出力の中で見るべき箇所は、.textセクションの位置情報と関数fnの位置情報です。以下は出力から抜粋した様子です。

上記objdump出力の読み方は次のようになります。

答え合わせをしてみます。次のようにファイル内でのオフセット0x3d4の位置に、関数fnのバイト列が見つかります(55から始まる列)。

バイナリエディタで上記の03の部分を書き換えてxを実行すると見事に動作が変わります。バイナリパッチが成功しました。

疑問: マシン語の世界に変数名や関数名なんて存在しないって聞きましたが。
回答:
不要です。objdumpで見たシンボル(関数名や変数名)はデバッグ用です。
stripコマンドでシンボル情報を削除できます。削除しても実行に支障はありません。

疑問: stripコマンドする意味はありますか?
回答:
実行ファイルのサイズが少し小さくなります。それだけです。
ディスク容量が少ない時代には意味がありました。

ここから古のテクニックです。

実験:
関数fnのバイト列をmallocで確保したメモリにコピーして、関数ポインタを使ってそこを関数呼び出ししてみます。

実行すると動きます(注意:動かない環境もあります)。

コピーした関数のバイト列を書き換えて、バイナリパッチ相当の動作を模倣してみます。

実行すると動きます(注意:動かない環境もあります)。

スタックでも同じこと(関数のバイト列をコピーして関数ポインタ経由で呼び出し)ができるかは、次のように確認できます。

ここまでのまとめ

  • コードもデータもメモリ上では単なるバイト列
  • コードと同じバイト列をメモリ領域に書き込んで、そこにジャンプすれば関数を実行できる
  • ただし、セキュリティの観点から禁止する方向にある(e.g. StackGuard、ProPolice)

疑問: コピーしたものではなくfn本体を書き換えられるか?
実験:

実行してみます。

回答:
できません。
OSがメモリ保護機能を活用してコードのメモリ領域をread-onlyにしているためです。
(あくまでOSの機能なので、できるOSも存在します)

疑問: 無茶苦茶な内容のメモリ領域を関数呼び出しすると何が起きるか?
実験:

実行してみます。

回答:
何が起きても不思議はありませんが、ほぼ確実にプロセスが落ちます。
まともなOSがあれば暴走はしないので、マシンは壊れません。

疑問: 別プロセスのユーザ空間のメモリを読み書きする手段は存在するか?
回答:
あります。デバッガ(gdbなど)がやっています(システムコールptrace)。
どこまで許すかはOS次第です。伝統的には実行ユーザが同じプロセスの間でだけ許します(rootユーザは別)。
ただし、Ubuntu最新版は親子プロセスの間でしか許さないようになっています。以下のファイルで設定します。

と、ここまでの話をするつもりでした。

しかし、会社のPCでやったら、mallocした領域にコードをコピーして関数呼び出しするとsegmentation faultで落ちてしまいます。

64bitマシンなので以下のNX bitが効いているようです。

http://en.wikipedia.org/wiki/NX_bit

それから、プロセスを実行するたびにmallocで確保したメモリのアドレスが変わるのなぜかなと思っていたら下記の機能のようでした。言われてみれば聞いたことがある気がしますがすぐに思い至りませんでした。

http://en.wikipedia.org/wiki/Address_space_layout_randomization

タイトルは「最近の技術」と書きましたが、自分が知らなかっただけで、たいして最近ではないかもしれません。


関連文書:

  • 関連文書は見つからんがな

Comments are closed.