オブジェクトについて抽象から具象まで取り混ぜて説明していた時、最も具象なレベルで見れば、オブジェクトはメモリ上に確保した領域にすぎないと説明しました。
そんな説明をしていた時、メソッドの実体ってどこにどうあるのですかと質問を受けました。人の心はどこにどうあるのですかという質問に比べると緩い質問ですが、良い質問だと思いました。こういう疑問を持つのは大事だと思うからです。自分もかつてプログラムとは結局のところどう実行されるのかが気になりました。プログラマなら誰もが通る道だと思います。
そんなわけでJavaのような箱入り娘から離れて、デレのないツンデレ娘ことC言語で古のテクニックを見せることにしました。
とりあえず次の簡単なコードから始めます。C言語は知らなくても構いません。関数fnがあって、引数に2を加算して返すことだけを読み取ってください。
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <stdio.h> static int fn(int a) { return a + 2; } int main() { printf("%d\n", fn(3)); return 0; } |
念のため実行してみます。
1 2 3 |
$ gcc -Wall -g x.c -o x $ ./x 5 |
これ以降はコードを簡潔にするため警告を黙らせるキャストを省略します。良い習慣ではないのでマネはしないでください。
実行ファイルxをgdb(デバッガ)で起動してみます。
gdb上のdisasコマンドでディスアセンブルできます。関数fnの実体のアセンブリコードを見える化できます。
1 2 3 4 5 6 7 8 9 10 |
$ gdb x (gdb) disas fn Dump of assembler code for function fn: 0x080483a4 <fn+0>: push %ebp 0x080483a5 <fn+1>: mov %esp,%ebp 0x080483a7 <fn+3>: mov 0x8(%ebp),%eax 0x080483aa <fn+6>: add $0x2,%eax 0x080483ad <fn+9>: pop %ebp 0x080483ae <fn+10>: ret End of assembler dump. |
次のようにすると、関数fnの実体がメモリ上でたんなる数値の列(バイト列)にすぎないと分かります。後でこの数値と上記のアセンブリコードの対応をobjdumpコマンドで見ます。
1 2 3 |
(gdb) x/11xb fn 0x80483a4 <fn>: 0x55 0x89 0xe5 0x8b 0x45 0x08 0x83 0xc0 0x80483ac <fn+8>: 0x02 0x5d 0xc3 |
最初のコードを次のように変更してみます。
1 2 3 4 |
static int fn(int a) { return a + 3; // 2から3に書き換え } |
再コンパイル後、gdbで書き換わっていることを確認してみます。0x02があった部分が0x03になっているのを確認してください。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(gdb) disas fn Dump of assembler code for function fn: 0x080483a4 <fn+0>: push %ebp 0x080483a5 <fn+1>: mov %esp,%ebp 0x080483a7 <fn+3>: mov 0x8(%ebp),%eax 0x080483aa <fn+6>: add $0x3,%eax 0x080483ad <fn+9>: pop %ebp 0x080483ae <fn+10>: ret End of assembler dump. (gdb) x/11xb fn 0x80483a4 <fn>: 0x55 0x89 0xe5 0x8b 0x45 0x08 0x83 0xc0 0x80483ac <fn+8>: 0x03 0x5d 0xc3 |
関数fnのバイト列は実行ファイルxの中に見つかります(Javaで言えば、JVMのバイトコードのバイト列がクラスファイルの中に見つかるのと同じ話です)。
実行ファイルxを解析するにはobjdumpが便利です(nmコマンドでもほぼ同じようなことができますがobjdumpのほうがリッチです)。
objdumpに-dオプションを渡すとディスアセンブルできます。gdbで見たバイト列とアセンブリコードの対応を確認できます。
1 2 3 4 5 6 7 8 9 |
$ objdump -d x 出力を抜粋 080483d4 <fn>: 80483d4: 55 push %ebp 80483d5: 89 e5 mov %esp,%ebp 80483d7: 8b 45 08 mov 0x8(%ebp),%eax 80483da: 83 c0 03 add $0x3,%eax 80483dd: 5d pop %ebp 80483de: c3 ret |
このバイト列を実行ファイルxの中から探してバイナリパッチを当ててみます。バイナリエディタでバイト列を探してもいいですが、objdumpを使うとファイル中での正確なオフセットを計算できます。
objdumpを次のように-xオプションで起動します。出力の中で見るべき箇所は、.textセクションの位置情報と関数fnの位置情報です。以下は出力から抜粋した様子です。
1 2 3 4 5 6 7 8 9 |
$ objdump -x x 出力を抜粋 Sections: Idx Name Size VMA LMA File off Algn 12 .text 000001ac 08048320 08048320 00000320 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE SYMBOL TABLE: 080483d4 l F .text 0000000b fn |
上記objdump出力の読み方は次のようになります。
1 2 3 4 5 6 7 8 9 |
.textセクションのVMA(virtual memory address): 0x08048320 .textセクションのファイル内でのオフセット: 0x00000320 関数fnの実体は.textセクションにあり、VMAでのアドレスが 0x080483d4 ファイル内の関数fnのオフセット: 0x00000320 + (0x080483d4 - 0x08048320) 計算例 $ perl -e '{printf("%x\n", 0x00000320 + (0x080483d4 - 0x08048320))}' 3d4 |
答え合わせをしてみます。次のようにファイル内でのオフセット0x3d4の位置に、関数fnのバイト列が見つかります(55から始まる列)。
1 2 |
$ od -tx1 -Ax x|grep 3d0 0003d0 d0 c9 c3 90 55 89 e5 8b 45 08 83 c0 03 5d c3 8d |
バイナリエディタで上記の03の部分を書き換えてxを実行すると見事に動作が変わります。バイナリパッチが成功しました。
疑問: マシン語の世界に変数名や関数名なんて存在しないって聞きましたが。
回答:
不要です。objdumpで見たシンボル(関数名や変数名)はデバッグ用です。
stripコマンドでシンボル情報を削除できます。削除しても実行に支障はありません。
疑問: stripコマンドする意味はありますか?
回答:
実行ファイルのサイズが少し小さくなります。それだけです。
ディスク容量が少ない時代には意味がありました。
ここから古のテクニックです。
実験:
関数fnのバイト列をmallocで確保したメモリにコピーして、関数ポインタを使ってそこを関数呼び出ししてみます。
1 2 3 4 5 6 7 8 9 |
int main() { char* pt = fn; // 関数fnの先頭アドレス char* mem = malloc(11); // 11バイトのメモリ確保 memcpy(mem, pt, 11); // 関数fnのバイト列11バイトをコピー int (*fp)(int a) = mem; // 関数ポインタに型変換 printf("%d\n", fp(3)); // 関数fpの呼び出し(mallocした領域の呼び出し) return 0; } |
実行すると動きます(注意:動かない環境もあります)。
1 2 |
$ ./x 5 |
コピーした関数のバイト列を書き換えて、バイナリパッチ相当の動作を模倣してみます。
1 2 3 |
memcpy(mem, pt, 11); // 関数fnのバイト列11バイトをコピー // 以下の行を追加 mem[8] = 0x7; // mallocした領域にバイナリパッチ |
実行すると動きます(注意:動かない環境もあります)。
1 2 |
$ ./x 10 |
スタックでも同じこと(関数のバイト列をコピーして関数ポインタ経由で呼び出し)ができるかは、次のように確認できます。
1 2 3 4 |
char* pt = fn; // mallocの代わりに下記の行を使う char mem[11]; memcpy(mem, pt, 11); // 関数fnのバイト列11バイトをコピー |
ここまでのまとめ
- コードもデータもメモリ上では単なるバイト列
- コードと同じバイト列をメモリ領域に書き込んで、そこにジャンプすれば関数を実行できる
- ただし、セキュリティの観点から禁止する方向にある(e.g. StackGuard、ProPolice)
疑問: コピーしたものではなくfn本体を書き換えられるか?
実験:
1 2 3 4 5 6 7 |
int main() { char* pt = fn; pt[8] = 0x7; printf("%d\n", fn(3)); return 0; } |
実行してみます。
1 2 |
$ ./x segmentation fault (core dumped) |
回答:
できません。
OSがメモリ保護機能を活用してコードのメモリ領域をread-onlyにしているためです。
(あくまでOSの機能なので、できるOSも存在します)
疑問: 無茶苦茶な内容のメモリ領域を関数呼び出しすると何が起きるか?
実験:
1 2 3 4 5 6 7 |
int main() { char* mem = malloc(11); // 確保したメモリ(中身はゴミデータ) int (*fp)(int a) = mem; // 関数ポインタに型変換 printf("%d\n", fp(3)); return 0; } |
実行してみます。
1 2 3 4 |
$ ./x Trace/breakpoint trap (core dumped) or Segmentation fault (core dumped) |
回答:
何が起きても不思議はありませんが、ほぼ確実にプロセスが落ちます。
まともなOSがあれば暴走はしないので、マシンは壊れません。
疑問: 別プロセスのユーザ空間のメモリを読み書きする手段は存在するか?
回答:
あります。デバッガ(gdbなど)がやっています(システムコールptrace)。
どこまで許すかはOS次第です。伝統的には実行ユーザが同じプロセスの間でだけ許します(rootユーザは別)。
ただし、Ubuntu最新版は親子プロセスの間でしか許さないようになっています。以下のファイルで設定します。
1 2 3 4 5 6 7 |
/etc/sysctl.d/10-ptrace.conf # デフォルト(親子プロセスの間でのみptraceを許可) kernel.yama.ptrace_scope = 1 # 実行ユーザが同じプロセス間でptraceを許可 kernel.yama.ptrace_scope = 0 |
と、ここまでの話をするつもりでした。
しかし、会社のPCでやったら、mallocした領域にコードをコピーして関数呼び出しするとsegmentation faultで落ちてしまいます。
64bitマシンなので以下のNX bitが効いているようです。
http://en.wikipedia.org/wiki/NX_bit
それから、プロセスを実行するたびにmallocで確保したメモリのアドレスが変わるのなぜかなと思っていたら下記の機能のようでした。言われてみれば聞いたことがある気がしますがすぐに思い至りませんでした。
http://en.wikipedia.org/wiki/Address_space_layout_randomization
タイトルは「最近の技術」と書きましたが、自分が知らなかっただけで、たいして最近ではないかもしれません。
最近のコメント