x86 Linux上で sysenter 命令を使ってシステムコールを呼び出す方法
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
は
- ESP ← ECX
- EIP ← EDX
- CS ← (MSR に書かれたセレクタ + 16) OR 3
- SS ← CS + 8
的なことをやって ring 3 に戻ります。 sysenter
で戻りアドレスを保存しないのにどうやって sysexit
で ESP
と EIP
を復元しているのでしょうか。
答えは、「sysenter
を呼び出すのは決められたサブルーチンに限る」です。少なくとも Linux ではユーザープログラムから直接 sysenter
を呼び出すことはありません。戻るときは、カーネルがその「決められたサブルーチン」の値を直接指定して戻ります。そこから改めてユーザープログラムに ret
します。
ここで、アセンブリで直接システムコールを呼び出すサンプルプログラムを見てみましょう。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main() {
unsigned int gs10;
pid_t pid, pid10;
FILE *fp;
char buf[256];
__asm__ volatile("movl %%gs:0x10, %%eax" : "=a"(gs10));
printf("syscall point = %p\n", gs10);
pid = getpid();
__asm__ volatile("\t"
"movl $20, %%eax\n\t"
"call *%%gs:0x10\n\t"
: "=a"(pid10));
printf("pid = %u, pid10 = %u\n", pid, pid10);
fp = fopen("/proc/self/maps", "r");
while (fgets(buf, sizeof(buf), fp) != NULL) {
fputs(buf, stdout);
}
fclose(fp);
}
実行例は次のようになります。
$ ./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]
に含まれているコードを見てみましょう。
$ gdb ./a.out
GNU gdb (GDB) Fedora 7.7.1-13.fc20
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from ./a.out...done.
(gdb) start
Temporary breakpoint 1 at 0x804856c: file gscall.c, line 12.
Starting program: /home/yuryu/src/a.out
Temporary breakpoint 1, main () at gscall.c:12
12 __asm__ volatile (
Missing separate debuginfos, use: debuginfo-install glibc-2.18-12.fc20.i686
(gdb) n
16 printf("syscall point = %p\n", gs10);
(gdb)
syscall point = 0xb7ffd414
17 pid = getpid();
(gdb) disas 0xb7ffd414
Dump of assembler code for function __kernel_vsyscall:
0xb7ffd414 <+0>: push %ecx
0xb7ffd415 <+1>: push %edx
0xb7ffd416 <+2>: push %ebp
0xb7ffd417 <+3>: mov %esp,%ebp
0xb7ffd419 <+5>: sysenter
0xb7ffd41b <+7>: nop
0xb7ffd41c <+8>: nop
0xb7ffd41d <+9>: nop
0xb7ffd41e <+10>: nop
0xb7ffd41f <+11>: nop
0xb7ffd420 <+12>: nop
0xb7ffd421 <+13>: nop
0xb7ffd422 <+14>: int $0x80
0xb7ffd424 <+16>: pop %ebp
0xb7ffd425 <+17>: pop %edx
0xb7ffd426 <+18>: pop %ecx
0xb7ffd427 <+19>: ret
End of assembler dump.
レジスタを保存して sysenter
しているコードが見つかりました。EDX
と ECX
を保存しているのは、上述の通り sysexit
で使用するからです。ebp
を保存しているのは、システムコールからユーザースタックにアクセスすることがあるからです。 nop
の後に int 80h
が見えますが、これはシステムコールをリスタートする時に使うもので、通常はここを通らずその後の pop ebp
から実行が再開されます。
この vdso のアドレスはカーネルが知っているので、カーネルは決め打ちで EDX
と ECX
をセットすることができます。どこに戻るかの情報は thread_info
構造体の sysenter_return
に入ってます。これがシステムコールハンドラで ECX
にセットされます。
struct thread_info {
struct task_struct *task; /* main task structure */
struct exec_domain *exec_domain; /* execution domain */
__u32 flags; /* low level flags */
__u32 status; /* thread synchronous flags */
__u32 cpu; /* current CPU */
int saved_preempt_count;
mm_segment_t addr_limit;
struct restart_block restart_block;
void __user *sysenter_return;
unsigned int sig_on_uaccess_error:1;
unsigned int uaccess_err:1; /* uaccess failed */
};
最後の謎は gs:[10h]
って何?というところですが、Linux の x86 では gs は thread local storage を指しているそうです。で、ここに glibc が起動時に vdso のこのアドレスを探してきて保存しているようでした。
というわけで sysenter
って int 80h
のようなノリで使うものじゃ無いっぽいですね。 Windows も同じような仕組みだったのでしょうか。 x64 で使用している syscall 命令は、RCX
に RIP
を保存するので、ユーザープログラムのどこからでも直接システムコールが呼べるみたいです。
参考文献
Discussion