Inline::x86でPerlの中に直接機械語を書く
前置き
こんにちは、id:TAKESAKOです。最近コロプラにハマるようになってきて近郊遠方の国内出張が楽しくなりました。JPerl Advent Calendar 2009 Hacker Trackも9日目ですね。そろそろPerlのソースを読むだけでは飽き足らずx86の機械語を書きたくなってきたんじゃないでしょうか。そんなわけで、今日は稚拙のInline::x86を紹介します。
本題
皆さんの中にはよくx86を書く方もいらっしゃると思いますが、x86機械語を含むコードインジェクションは実行環境の設定が結構大変ですよね。Linuxのシステムコールmprotectを呼び出して実行ビットを追加で指定したり、Windows XP SP2以降のDEP(データ実行防止機能)を無効にしたりする必要がありますし、64bit命令は使えるか、nasmのバージョンは十分に新しいか、などなど考えなければいけないことが多々あります。CPUの中にコアが複数あると、また一層大変です。
これらの面倒なことは Inline::x86 にやらせましょう!
まず一番短くて簡単な Inline/x86.pm を自分で作成してみます。
package Inline::x86; use DynaLoader(); use Exporter; our @ISA = qw(Exporter); our @EXPORT = qw(x86); sub x86 { my ($x86) = @_; if ($^O eq "linux") { require 'syscall.ph';my $size = int(2+length($x86)/4096)*4096; syscall(&SYS;_mprotect,(unpack"L",pack"P",$x86)&~4095,$size,7); } DynaLoader::dl_install_xsub("X86",unpack"L",pack"P",$x86); &X86; } 1;
use DynaLoaderは、共有ライブラリの関数をダイナミックに呼び出すことができる標準モジュールです。通常はDynaLoader::dl_install_xsub の第二引数にCで書かれた共有ライブラリの関数ポインタを指定するのですが、ここに変数 $x86 に代入されている文字列のポインタを渡して、直接$x86に記述した機械語を呼び出すハックをしています。
※ Inline/x86.pm は 100% Pure Perl で書かれているため、別途CコンパイラやXSモジュールなどをインストールする必要がありません!これは便利です!
32bit OS か 64bit OS か判別する
これを使って、今実行しているOSが32bit互換モードか、64bitロングモードかを調べるPerlスクリプトを書いてみましょう。
use Inline::x86; sub bit { my $long_mode = "?" ; # "0" => 32bit, "1" => 64bit x86 do { "\xb8\x31\x00\x00\x00". # mov eax, 0x31 "\x48". # dec eax // 64bit REX PREFIX "\xa2".pack("P",$long_mode). # mov [$long_mode], al "\xc3"; # ret }; $long_mode ? "64bit" : "32bit"; } print bit(), "\n";
Inline::x86 を使用すると、x86 do { "機械語"; } という構文で、x86の機械語を直接実行することができます。
とてもわかりやすいですね。
実際の機械語の中身ですが、64bitロングモード特有のREX PREFIXを解釈するかどうかで判別するコードを入れています。32bitのx86互換モードでは dec eax が実行され $long_mode="0" となりますが、64bitロングモードではこのような1byte decは解釈されず、直後のmov命令のREX PREFIXを指定することになるので、$long_mode="1" となります。
Perlで64bit整数対応かどうか調べる
ちなみに、Config.pmを使用せずに、Perlが64bit整数対応でコンパイルされているかどうかを確認するコードは以下のように書けます。
#!/usr/bin/perl print((~0>>31==1)?"32bit":"64bit");
これは機械語を使っていませんが。64bit整数が扱える場合は、~0>>31==8589934591となるなのですが、32bit整数しか扱えない場合は、上位31bitがゼロクリアされて~0>>31==1となるためです。簡単ですね。
CPUのプロセッサ名を取得する
現在実行しているCPUのプロセッサ名を取得するPerlスクリプトを書いてみます。
use Inline::x86; sub ProcessorBrandString { my $cpu = "\0" x 48; x86 do { "S\xbf" . pack("P", $cpu). "\xb8\x02\x00\x00\x80". "P\x0f\xa2\x89\a\x89_\x04\x89O\b\x89W\x0c\x8d\x7f\x10X\x8d\@\x01". "P\x0f\xa2\x89\a\x89_\x04\x89O\b\x89W\x0c\x8d\x7f\x10X\x8d\@\x01". "P\x0f\xa2\x89\a\x89_\x04\x89O\b\x89W\x0c\x8d\x7f\x10X\x8d\@\x01". "[\xc3"; }; $cpu =~ s/\0+//g; $cpu =~ s/^ +//; $cpu; } print ProcessorBrandString(), "\n";
このプログラムを実際にいくつかのマシン上で動かしてみましょう。
Intel(R) Pentium(R) 4 CPU 3.40GHzIntel(R) Pentium(R) 4 CPU 3.40GHz
AMD Athlon(tm) Processor 1640B
搭載しているマシンのCPU毎に様々な結果が得られることがわかりました。
これで、Perlで機械語プログラミングしていて、途中でCPUを判別してプログラムの処理を分岐したいときがでてきても安心です。
ちなみに、実際に実行している機械語をディスアセンブルすると以下になります。
----------------------------------------------- 00000000 53 push ebx 00000001 BFxxxxxxxx mov edi,0xXXXXXXXX 00000006 B802000080 mov eax,0x80000002 ----------------------------------------------- 0000000B 50 push eax 0000000C 0FA2 cpuid 0000000E 8907 mov [edi],eax 00000010 895F04 mov [edi+0x4],ebx 00000013 894F08 mov [edi+0x8],ecx 00000016 89570C mov [edi+0xc],edx 00000019 8D7F10 lea edi,[edi+0x10] 0000001C 58 pop eax 0000001D 8D4001 lea eax,[eax+0x1] ----------------- 3回繰り返し ----------------- 0000004A 5B pop ebx 0000004B C3 ret -----------------------------------------------
eaxレジスタに0x80000002を代入して値を1増やしながらCPUID命令を実行して、ediレジスタの指す文字列の先頭から32bit×4=16byteの書き込みを3回繰り返しています。
ループアンローリングできる形に機械語を変形しているので多い日も安心ですね。
まとめ
今回は簡単な Inline::x86 の作り方について解説しました。
このモジュールは、同様の機械語を何度も書いたり見かけたりしているうちに、「もうめんどくさいからモジュールにしちゃおう」とおもって作りました。普段から「定型的なコードがないだろうか」と気をつけていることが重要ですね。
というわけで今回はここまで。明日は id:cho45 さんです。