2014年6月10日火曜日

x86 Linux上で sysenter 命令を使ってシステムコールを呼び出す方法

x86(32bit)環境で、どのようにシステムコールが呼ばれているのかを調べてみました。

int 80h

昔からあるシステムコールの呼び方で、eax レジスタにシステムコールの番号をセットした後、ソフトウェア割り込みの int 80h をコールするとシステムコールが呼ばれます。これはどのバージョンの Linux でも使えます。ただし、ソフトウェア割り込みは遅いのが欠点です。

sysenter

最近のIntelのプロセッサはすべて sysenter というシステムコール専用の命令が用意されています。この命令は int 80h に比べるとかなりシンプルで、予め設定されたシステムコールハンドラのアドレスをCS:EIPとSS:ESPにセットして、特権モードに切り替えて実行を再開するのみです。詳しくは「64ビットCPU(AMD64+EM64T)でアセンブラ int 2E/sysenter/syscall考察」が参考になります。

さてここでのポイントは、sysenter命令はユーザーモード空間での実行アドレスをスタックやレジスタに退避しないということです。通常の call 命令や int 80h 命令の場合は、スタックに CS:EIP を push してから jmp します。sysenter ではどこにも保存されません。ではsysexitはどのように戻っているのでしょうか。sysexitは

  1. ESP ← ECX
  2. EIP ← EDX
  3. CS ← (MSR に書かれたセレクタ + 16) OR 3
  4. SS ← CS + 8

的なことをやって ring 3 に戻ります。

さてここで疑問ですが、sysenterで保存しないのにどうやってsysexitでESPとEIPを復元しているのでしょうか。不思議ですよね。

答えは、「sysenterを呼び出すのは決められたサブルーチンに限る」です。少なくとも Linux ではユーザープログラムから直接 sysenter を呼び出すことはありません。戻るときは、カーネルがその「決められたサブルーチン」の値を直接指定して戻ります。そこから改めてユーザープログラムに ret します。

ここで、アセンブリで直接 syscall を呼び出すサンプルプログラムを見てみましょう。

実行例は次のようになります。

$ ./a.out 
syscall point = 0xb775a414
pid = 13734, pid10 = 13734
08048000-08049000 r-xp 00000000 fd:00 812670     /home/yuryu/src/a.out
08049000-0804a000 r--p 00000000 fd:00 812670     /home/yuryu/src/a.out
0804a000-0804b000 rw-p 00001000 fd:00 812670     /home/yuryu/src/a.out
09370000-09391000 rw-p 00000000 00:00 0          [heap]
b7568000-b7569000 rw-p 00000000 00:00 0 
b7569000-b7721000 r-xp 00000000 fd:00 391110     /usr/lib/libc-2.18.so
b7721000-b7723000 r--p 001b8000 fd:00 391110     /usr/lib/libc-2.18.so
b7723000-b7724000 rw-p 001ba000 fd:00 391110     /usr/lib/libc-2.18.so
b7724000-b7727000 rw-p 00000000 00:00 0 
b7738000-b773b000 rw-p 00000000 00:00 0 
b773b000-b775a000 r-xp 00000000 fd:00 397102     /usr/lib/ld-2.18.so
b775a000-b775b000 r-xp 00000000 00:00 0          [vdso]
b775b000-b775c000 r--p 0001f000 fd:00 397102     /usr/lib/ld-2.18.so
b775c000-b775d000 rw-p 00020000 fd:00 397102     /usr/lib/ld-2.18.so
bfed0000-bfef1000 rw-p 00000000 00:00 0          [stack]

gs:[10h]に保存されているアドレスに対して call すると、システムコールが実行されます。この例の場合、0xb775a414 が保存されていますが、これはちょうど "vdso" と呼ばれている領域が該当します。この vdso というのは、カーネルがユーザープログラムから使えるように、共有ライブラリとして自動的にリンクしている領域です。

gs:[10h] に含まれているコードを見てみましょう。

レジスタを保存して sysenter しているコードが見つかりました。EDXと ECXを保存しているのは、上述の通り sysexit で使用するからです。ebp を保存しているのは、システムコールからユーザースタックにアクセスすることがあるからです。 nop の後に int 80h が見えますが、これはシステムコールをリスタートする時に使うもので、通常はここを通らずその後の pop ebp から実行が再開されます。

このvdsoのアドレスはカーネルが知っているので、カーネルは決め打ちでEDXとECXをセットすることができます。どこに戻るかの情報は

thread_info 構造体の sysenter_return に入ってます。これがシステムコールハンドラでECXにセットされます。

最後の謎は gs:[10h] って何?というところですが、Linux の x86 では gs は thread local storage を指しているそうです。で、ここに glibc が起動時に vdso のこのアドレスを探してきて保存しているようでした。

というわけで sysenter って int 80h のようなノリで使うものじゃ無いっぽいですね。 Windows も同じような仕組みだったのでしょうか。 x64 で使用している syscall 命令は、RCX に RIP を保存するので、ユーザープログラムのどこからでも syscall できるみたいです。

参考文献