Inline::x86でPerlの中に直接機械語を書くB!

前置き

こんにちは、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 さんです。