Haskellとインラインアセンブリ(構築編)
前回の内容を踏まえて、今回は、Haskellで記述されたアセンブリプログラムをアセンブルして、それを前回述べた方法を用いて実行することを目標とする。
本項ではHaskell上でアセンブリプログラムを記述するための言語を構築し、それに対するアセンブラを作成する。
記述の利便性のため、Haskell上で構築する言語はモナドで構成する。普通のアセンブラで扱えるような形で記述できればうれしい。
foo :: CodeBlock (IO Int) foo = assemble $ do movl eax 777 movl eax ecx movl eax (mem(eax)) movl eax (mem(eax,ebx)) movl eax (mem(eax,(ebx,4))) movl eax (mem(eax,4)) movl eax (mem(eax,ebx,777)) movl eax (mem(eax,(ebx,8),777)) ret
サイズ指定を容易にするため、gasのようにニモニックでオペランドサイズを指定することにする。そのほかの部分はintel形式を模倣する。このようにコードを記述できてそれを実行できるようにすべく以降で定義をしてゆく。
まず、アセンブリプログラムを表すAsmモナドを定義する。Asmモナドは
type Asm a = RWS () [Mnemonic] AsmState a
このように定義した。RWSモナドは、ReaderとWriterとStateの能力を持つモナドで、今回のアセンブラではアセンブリの中間コード(上でのMnemonic)を出力するためのWriterと状態を扱うためにStateを利用する。Asmモナドは一塊のコードを示し、これをassembleにかけることによって関数というまとまった単位のコードにアセンブルされるものとする。
Mnemonicは次のように定義した。
data Mnemonic = Imp Mne | Uni Mne Size Operand | Bin Mne Size Operand Operand | Tri Mne Size Operand Operand Operand | Rel Mne LabelID | Abs Mne CodeBlock | Label LabelID
Impはオペランドなしの命令。Uni,Bin,Triはそれぞれオペランドが1,2,3個の命令。RelとAbsは後述。Labelはラベルの定義。
サイズは、1,2,4,8バイトのいずれか。
data Size = SByte | SWord | SDWord | SQWord deriving (Show)
x86のオペランドは、レジスタと即値とメモリアドレスがあり、アドレッシングモードは、(ベースレジスタ)+(インデックスレジスタ)*(スケール(1,2,4 or 8))+(ディスプレースメント)の形および、このうち1つ以上が欠けた形を取る。
data Operand = Imm Int | Reg RegName | Mem Address data Address = Address (Maybe RegName) (Maybe (RegName,Int)) Displacement
実際にアセンブリを記述する際はmovlなどのヘルパ関数を用いる。この際、利便性のために引数でオーバーロードしてほしい(数値が書かれていれば即値など)。アドレスを指すためにはmemという関数を用いる。これを用いなくても(eax,(ebx,2),1)のような形であればmovlなどで直接オーバーロードできるが、(eax)というような形は(つまり1-tupleは)eaxと区別できないのでそれらを区別するために用いる。引数をオーバーロードするために次のようなクラスを定義する。
class AddrMode a where toArgument :: a -> Operand instance Integral a => AddrMode a where toArgument n = Imm $ fromIntegral n instance AddrMode RegName where toArgument r = Reg r instance AddrMode Address where toArgument a = Mem a instance AddrMode Operand where toArgument o = o
これでmovlが定義可能になる。
movl :: (AddrMode a, AddrMode b) => a -> b -> Asm () movl = tell [ Bin Mov SDWord (toArgument a) (toArgument b) ]
他の命令も同様に定義できる。
ラベルおよびジャンプ命令は次のように記述できるようにする。
hoge = assemble $ do l <- genLabel label l ... jmp l
genLabelでラベルIDを生成して、それを用いてラベル定義やジャンプ、条件ジャンプを記述する。ラベルIDを文字列にしてユーザが指定の文字列でラベルを記述できるようにしてもよかったが、後のことを考えるとこちらのほうが有利と判断した。
call命令に関してはローカルなラベルではなくて、他の関数を呼び出せなければいけない。他の関数はCodeBlock型で示される同じくアセンブラで生成された機械語列であるとする。
foo = assemble $ do ... bar = assemble $ do ... call foo ...
このように定義すると、例えば上の場合だと、barのアセンブルの際にfooの値が必要になる、つまり、fooのアセンブルが終わっている必要がある。あるいは、fooの値はメモリの先頭なのであるから、ポインタの値だけでも決まっている必要がある。これは再帰的に参照している場合に問題になる。それを許す実装も不可能ではないが、必要以上に複雑になるので、ここでは再帰的な参照は許さないものとする。ただし、直接的な再帰は利用される機会も多いのでselfという特別な名前を用いて行えるようにしておく。相互再帰に関しては、今のところおいておく。
このようにAsmモナドを定義すると、これを実行することによってMnemonicの列が得られる。あとは得られたMnemonicをバイト列にしてやればよい(と一言で言っているが、ここが一番大変)。
そのようにしてできたのがこちらである。命令はごく少数しかサポートしていないのであしからず。
http://fxp.infoseek.ne.jp/tmp/asm/AsmX86.zip
コード例をいくつか示す。
前回手書きしたコードと同じもの。
import AsmX86 bar :: CodeBlock (IO Int) bar = assemble $ do movl eax 777 ret
階乗
fact :: CodeBlock (Int -> IO Int) fact = assemble $ do l <- genLabel movl eax (mem(esp,4)) subl eax 1 jnz l movl eax 1 ret label l pushl eax call self popl ecx addl ecx 1 imull eax ecx ret
呼び出し例
main = do ret <- exec fact 10 print ret
実行例
$ ghc --make Main.hs
[1 of 2] Compiling AsmX86 ( AsmX86.hs, AsmX86.o )
[2 of 2] Compiling Main ( Main.hs, Main.o )
Linking Main.exe ...
$ ./Main
3628800
このようにごく自然に呼び出せる(くれぐれもx86以外で実行しないように)。
以上のようにHaskell用インラインアセンブリを定義することができたが、AsmモナドはHaskell上で構築するものであり、その際にはフルスペックのHaskellの能力が利用可能である。つまり、これを用いると一般的なマクロアセンブラの能力あるいはそれ以上に便利な機能が実装できる可能性がある。AsmはモナドでありHaskellで普通に扱えるデータであるので、これを用いてより抽象的なブロックが作れるだろうし、AsmモナドのStateを加えればより便利な機能が実装できるかもしれない。そこで、次回はそちらの方向に話をすすめて、アセンブラをより使いやすいものへと変化させたい。