Haskellとインラインアセンブリ(導入編)

(以下では前提として実行するCPUをx86とします。SPARCとかの人はごめんなさい)


Haskellから任意の機械語のコードを実行するにはどうすればよいのだろう。
Foreign.PtrにFunPtrという型が定義してあり、これは機械語コードの入っているメモリへのポインタを示す。
さらに、

type IntFunction = CInt -> IO ()
foreign import ccall "dynamic" 
  mkFun :: FunPtr IntFunction -> IntFunction

などとすることによりFunPtrの指すコードを呼び出すためのラッパを生成できる。
型ごとに別個のラッパが必要になり、必要に応じて自動的に定義されるわけでもないので、必要なものは個別に書いてやる必要がある。


これらを用いれば、Haskellからmallocを用いてメモリを確保し、そこにデータを書き込み、それをラッパにかけてやることによって、Haskellから任意の機械語コードを実行することができるだろう。


まず、利便性のために、ラッパを自動的に選択できるよう、FunPtr a から a に変換できる型のクラスを作っておく。

class MkFun a where
  mkFun :: FunPtr a -> a

foreign import ccall "dynamic"
  mkFunInt :: FunPtr (IO Int) -> IO Int
foreign import ccall "dynamic"
  mkFunInt2Int :: FunPtr (Int -> IO Int) -> (Int -> IO Int)

instance MkFun (IO Int) where
  mkFun = mkFunInt

instance MkFun (Int -> IO Int) where
  mkFun = mkFunInt2Int

当面はIntを返す関数とIntを取ってIntを返す関数のみをサポートしておく。


機械語列からはその関数の型を推論することなどできないので、関数の型はこちらが与えてやる。その際に、コードに対して型が付与できるように型を作る。

newtype CodeBlock a = CodeBlock (Ptr Word8)

newtypeでなくtypeでやると、typeは単なるエイリアスになるため、うまく型が付かない。


メモリを確保してコードを書き込むのと機械語列を実行する関数を作る。

writeCode :: [Word8] -> CodeBlock a
writeCode code = unsafePerformIO $ do
  let len = length code
  q <- mallocArray len
  pokeArray q code
  return $ CodeBlock q

exec :: MkFun a => CodeBlock a -> a
exec (CodeBlock p) = mkFun $ castPtrToFunPtr p


これで次のようにして望みのコードを実行させることができる。

foo :: CodeBlock (IO Int)
foo = writeCode
  [ 0xb8, 0x09, 0x03, 0x00, 0x00 -- mov eax,777
  , 0xc3                         -- ret
  ]

main = do
  ret <- exec foo
  print ret


fooは単に777を返す関数で、mainはそれを呼び出してprintしている。

$ ghc --make AsmTest.hs
$ ./AsmTest
777


さて、これでインラインアセンブリの可能性が見えてきた。当然のことながら次は機械語列をHaskellから生成したいという要求が出てくるので、そちら方面に話を進めることにする。