Darwin(xnu)のシステムコールを直で呼び出してHello, world

背景

#lowhacks @ irc.freenode.netを作ったが、低レベルプログラミングはインラインアセンブラでちょっとした事を書いた事ぐらいしかないことに気づいた。とりあえず純asmでHello, worldぐらい書けなきゃなーということで、手元の環境で書いてみる事に。

Linuxは山ほど資料があふれているのと、既に大筋の方法を知っていることもあり、某氏が挫折したらしいMac OSX環境でやってみることに…

システムコール規約を調べる

まず、予備知識としてxnuでは基本的にシステムコールにはsysenter/sysexitシステムコールを使うということは知っていた。

Intelの命令セットリファレンスによると、sysenter命令はcall命令等とはちがい、自前でリターンアドレス等を管理しなければいけない。リターンアドレスはEDX, スタックポインタ(ESP)はECXにおけばいいらしい。
http://www.intel.com/design/intarch/manuals/243191.htm

とりあえず、とにかく資料がないので、xnuのソースを読むところから。
http://www.opensource.apple.com/darwinsource/tarballs/apsl/xnu-792.13.8.tar.gz

osfmk以下にmach関連のソースコードがあるらしいので、その下をひたすらsysenterでgrepしていく。すると、osfmk/i386/idt.sというファイルが見つかる。後で調べたところ、IDTInterrupt Descriptor Tableの略らしい。ソースコードの上の方には、ソフトウェア例外のテーブルが書かれている。

その606行目あたり、他のソフトウェア割り込みハンドラ(おそらくINT命令用)の下に、sysenter entry pointと書かれたものが見つかる。読んでいくと、EAXがsyscall codeとしてpushされているのが分かる。また、プロシージャの終わりにはjmp *%EBXがある。*1sysenter経由の場合、%EBXにはlo_sysenterのアドレスが入るらしい。

lo_sysenterの実装はlocore.sにある。まず、%eaxの正負(つまりsyscall codeの正負)でlo_mach_scallかlo_unix_scallに分岐しているのが分かる(locore.s: 723行目あたり)。lo_unix_scallでは統計情報を更新後、unix_syscallというC関数を呼んでいる。引数はPCB(Process control block)内にあるsyscall呼び出し時のレジスタダンプ。

unix_syscall関数の実装は、そのまんますぎる名前のsystemcalls.c内にある。sysent構造体を適当にディスパッチしている模様。sysent構造体はbsd/init_sysent.cで大量に初期化されている。今回使いたいのは標準出力に"Hello, world"をwriteと、終了する為にexit。それぞれ、4番と1番。これをあとでEAXにいれてやれば良さそうだ。

続いて、引数がどのように扱われているか見ていく。肝は117行目のこれ

	params = (vm_offset_t) ((caddr_t)regs->uesp + sizeof (int));

と、133行目のこれ

		error = copyin((user_addr_t) params, (char *) vt, nargs);

と、156行目のこれ

			(*mungerp)(NULL, vt);

mungerpにはmunge.sで実装されているmungeプロシージャへのアドレスが入る。ざっと見た感じ、64-bitで実装されているシステムコール実装向けに32bitの引数が拡張されている模様。

あとは、204行目あたりで実装が呼ばれるようになっている。

	error = (*(callp->sy_call))((void *) p, (void *) vt, &(uthread->uu_rval[0]));

とりあえず、使う側としては、regs->uesp、つまりECXにはESPからsizeof(int)引いた値を入れればいいことが分かる。なぜこのような実装になっているかを推測すると、おそらくsyscallのstub関数を想定していて、その際stub関数でのret命令を介さずに、直接sysexitからユーザコードに戻るように高速化できるようにしているのだろう。

実際、後でlibcをgdbで追ってみた結果、以下の様なstub関数実装が見られた:

   1  Dump of assembler code for function _sysenter_trap:
   2  0x95954234 <_sysenter_trap+0>:	pop    %edx
   3  0x95954235 <_sysenter_trap+1>:	mov    %esp,%ecx
   4  0x95954237 <_sysenter_trap+3>:	sysenter 
   5  0x95954239 <_sysenter_trap+5>:	nopl   (%eax)

やはりret命令はみられない、noplとはなんじゃらほい、と思ったがこれはおそらくxchg %eax, %eaxではないかと邪推してみる。

ここまでのまとめ

xnuのシステムコールをsysenterで呼び出す場合、その前に

  • ESPに引数を適当にpush
  • ECXにESPマイナスsizeof(int)
  • EDXにsysexit先アドレス
  • そしてsysenter

すればいいことが分かる。次は実際にコードを書いてみる。

*1:TODO:なんで*がついているのか調べる