[Linux][Debian][C#] binfmtという魔法 - Linux上でC#とmonoが出会うまで

Linux上でC#を触り始めたのだが、少し不思議に思うことがあったので書いてみた。

Linux上でC#を実行

C#でHelloWorldプログラムを書く

// Hello.cs
namespace Hello{
  using System;

  class HelloWorld{
    public static void Main(){
      Console.WriteLine("Hello World!");
    }
  }
}

Linux上でmcsを使用してコンパイル
exeができあがるのでそのまま実行。

$ mcs Hello.cs
$ ./Hello.exe
Hello World!

上出来。それにしても拡張子exeなんだ。。。
ものは試しと、できたHello.exeをWindows上にコピーしてコンソールで実行してみる。

> Hello.exe
Hello World!

なんとそのまま実行されたではないか。これには驚いた。
Hello.exeをfileで調べてみると

$ file Hello.exe
Hello.exe: MS-DOS executable PE  for MS Windows (console) Intel 80386 32-bit Mono/.Net assembly

Windowsの実行形式であることがわかる。どうりで拡張子がexeなわけだ。
驚くべきはLinux上で素で実行されたことの方だったのだ。

binfmt_miscで遊ぶ

調べてみるとこれはbinfmt_miscというカーネルモジュールによって実現されているとことがわかった。
説明は後にしていきなりbinfmt_miscで遊んでみる。(以下コマンドの実行は自己責任でおねがいします。あとこれ以降の話はDebian固有かもしれないので他のディストリの方はご注意を。)

まずはフォーマットの登録

$ sudo /usr/sbin/update-binfmts --install test_fmt /bin/cat --magic 123


次にテキストファイルに実行属性を付けて実行してみる。

$ echo -e "123\nhoge" > test.txt
$ chmod 755 test.txt
$ ./test.txt
123
hoge

test.txtに対して/bin/catが実行されている。


後始末

$ sudo /usr/sbin/update-binfmts --remove test_fmt /bin/cat --magic 123

上でやったことの説明とか

Linuxではプログラムファイルのロードはbinfmtモジュールが担当している。elfならbinfmt_elf、a.outならbinfmt_aoutといった具合に主要なフォーマットは専用のモジュールが担当するのだが、ユーザがプログラムの扱いを指定したいときに使用するのがbinfmt_misc。
やっていることは単純でユーザの指定したフォーマットに合致するファイルであればユーザが指定したコマンドにファイルのパスを渡して実行するというもの。

上の例だとファイルの先頭が"123"であるファイルがきたら/bin/catにそのパスを渡すという意味になる。

binfmt_miscに登録されているフォーマットは/proc/sys/fs/binfmt_miscで参照できる。
上の登録を実行した後であればtest_fmtというファイルができていることが確認できるはずだ。

exeがmonoに出会うまで

/proc/sys/fs/binfmt_miscをみてみるとcliというファイルがある。

enabled
interpreter /usr/share/binfmt-support/run-detectors
flags:
offset 0
magic 4d5a

4d5aはASCIIコードで"MZ"であり、こいつはexeの先頭に必ずあるmagicナンバーだ。
つまり先頭にMZがあるファイルがくるとrun-detectorsが実行されることになる。

run-detectorsはperlスクリプトで/var/lib/binfmtsの内容を参照しつつ最終的にファイルの実行をまかせるプログラムの決定をおこなう。ここでは/usr/bin/cliに実行がまかされることになる。/usr/bin/cliは/usr/bin/monoへのシンボリックリンクであり、これでめでたくexeがMono JIT compilerにより実行されることになるわけだ。