システムコールとは(前編)
「WSL」で頻出する用語であるシステムコールについての紹介です。「WSL」は、Windows NTカーネル上でLinuxカーネルインターフェースをエミュレートすることにより、LinuxのELF64バイナリーをそのまま実行することができます。
Linuxカーネルインターフェースが提供する機能の1つが、システムコール(syscall)です。
システムコールの概要
システムコールはカーネルにより提供されるサービスであり、デバイスへのアクセス要求や権限が必要な操作を行うユーザーモードのソフトウェアからシステムコールが呼ばれます。例えばnodejsを使用したウェブサーバーは、ディスク上のファイルにアクセスする時や、ネットワーク要求を処理する時、そしてプロセスやスレッドを作成する時など様々な場面でシステムコールを使用します。
システムコールはOS及びCPUのアーキテクチャーに依存した仕組みですが、ユーザーモードから呼ばれ、カーネルモードヘ向う処理の流れは変わりません。
この時、ユーザーモードとカーネルモード間のインターフェースのことを、ABI(Application Binary Interface)といいます。
よく利用されるシステムコールの流れは、以下のようになります。
1.パラメーターのマーシャリング
まずシステムコールを利用するユーザーモードのソフトウェアは、ABIで定義されているシステムコールのパラメーターを用意します。パラメーターは、関数で言うところの引数みたいなものです。
マーシャリングとは、システムコールが必要とするデータを準備することです。
2.特別なプロセッサー命令
ユーザーモードのソフトウェアはシステムコールを呼び出すにあたり、カーネルモードへ移行するためCPUの特別な命令を実行します。3.処理結果の取得
システムコールが呼ばれシステムコールが実行されます。その後カーネルはCPUの特別な命令を実行し、呼ばれたシステムコールの処理結果をユーザーモードのソフトウェアに返します。
ユーザーモードのソフトウェアは、システムコールで得られた結果を使用して必要な処理を行います。
LinuxカーネルもWindows NTカーネルも流れも同じ
LinuxカーネルもWindows NTカーネルもシステムコールの処理の流れは、上記と同じ流れです。異なる点はABIです。
LinuxカーネルとWindows NTカーネルのABIに互換性はありません。
もしLinuxカーネルとWindows NTカーネルが同じABIを提供していたとしても、それぞれのカーネルは異なるシステムコールを提供しており、それぞれのシステムコールに互換性はありません。
例えばLinuxカーネルは、「fork」「open」「kill」といったシステムコールを提供しています。
一方Windows NTカーネルが提供する相当の機能を持ったシステムコールは、「NtCreateProcess」「NtOpenFile」「NtTerminateProcess」です。
Linux x86_64におけるシステムコールの仕組み
Linux x86_64におけるシステムコールの呼び出し規約は、System V x86_64 ABIに従っています。例えばCプログラムから直接「getdents64」システムコールを呼び出す方法の1つは、以下のようなシステムコールラッパーを使用することです。
Result = syscall(__NR_getdents64, Fd, Buffer, sizeof(Buffer));
このコードをアセンブラで表現すると、以下のようになります。
- mov rax, __NR_getdents64
- mov rdi, Fd
- mov rsi, Buffer
- mov rdx, sizeof(Buffer)
- syscall
- cmp rax, 0xFFFFFFFFFFFFF001
1.パラメータのマーシャリング
まず初めに、ユーザーモードのソフトウェアが行うパラメーターのマーシャリング処理を見てみましょう。上記アセンブラの「1.」〜「4.」の処理がパラメーターのマーシャリング処理になります。
システムコールに必要なデータを、呼び出し規約に従いレジスターへ転送(コピー)しています。
2.システムコールの呼び出し
「5.」の処理にて特別なシステムコール命令を実行し、カーネルモードへ移行します。3.システムコールの処理結果
「6.」でユーザーモードのソフトウェアがシステムコールの処理結果をチェックします。システムコールはいつ実行されるのか
「5.」と「6.」の間で「getdents64」システムコールが呼ばれ、Linuxカーネルで処理が行われています。システムコール命令が実行されるとCPUは、モード(リング)をカーネルモードに移行し、ユーザーモードのソフトウェアが呼び出したシステムコールに対応したカーネルモード内の特別な関数を実行します。
Linuxカーネルのシステムコールの準備と実行
PC起動時にカーネルの初期化が行われます。システムコール命令が実行される時に、ある特定の環境でシステムコールの処理が行えるよう、Linuxカーネルの初期化時にCPUの設定を行います。
1.レジスターの保存
システムコール命令実行時にまず最初にLinuxカーネルが行うことは、ユーザーモードのスレッドのレジスターの内容をABIに従い保存することです。レジスターの保存は、システムコール実行後にユーザーモードのソフトウェアが処理を継続するために必要な処理です。
システムコールの実行によりレジスターの内容(コンテキスト)が切り替わるため、コンテキストを切り替える前の状態を一時的に保持しておく必要があります。
さもないと、ユーザーモードのソフトウェアはシステムコール実行後に正しい処理を続行できなくなります。
2.システムコールの準備
その後「rax」レジスターの内容を調べ、ユーザーモードのソフトウェアがどのシステムコールを呼ぼうとしているのかを判別します。またシステムコールのパラメーターをレジスターから取得します。
これでシステムコールの実行準備が整いました。
3.システムコールの実行
システムコールを実行します。4.システムコールの実行完了
システムコールの実行が完了したらLinuxカーネルは、「1.」で保存したレジスターの内容でコンテキストを元に戻します。そしてシステムコールの実行結果を「rax」レジスターに保存します。
5.ユーザーモードに戻す
システムコール命令の実行でカーネルモードへ移行しました。特別な命令(大抵は「sysret」か「iretq」)を実行し、ユーザーモードに戻します。
Windows NT x64におけるシステムコールの仕組み
Windows NT x64(amd64)におけるシステムコールの呼び出し規約は、x64呼び出し規約に従っています。例えばCプログラムから「NtQueryDirectoryFile」システムコールを呼び出すには、以下の記述を行います。
Status = NtQueryDirectoryFile(Foo, Bar, Baz);
このコードをアセンブラで表現すると、以下のようになります。
- mov rax, #NtQueryDirectoryFile
- mov rcx, Foo
- mov rdx, Bar
- mov r8, Baz
- syscall
- test eax, eax
補足
実際の「NtQueryDirectoryFile」は、以下のように11のパラメーターを要求しそれらのパラメーターをスタックに積む必要がありますが、ここでは簡略化しています。NTSYSAPI NTSTATUS NTAPI NtQueryDirectoryFile( IN HANDLE FileHandle, IN HANDLE Event OPTIONAL, IN PIO_APC_ROUTINE ApcRoutine OPTIONAL, IN PVOID ApcContext OPTIONAL, OUT PIO_STATUS_BLOCK IoStatusBlock, OUT PVOID FileInformation, IN ULONG Length, IN FILE_INFORMATION_CLASS FileInformationClass, IN BOOLEAN ReturnSingleEntry, IN PUNICODE_STRING FileMask OPTIONAL, IN BOOLEAN RestartScan);
Linuxカーネルとの比較
上記のLinuxカーネルでのアセンブラと比較して記述すると、以下のようになります。ステップ | Linuxカーネル getdents64 |
Windowsカーネル NtQueryDirectoryFile |
---|---|---|
1. | mov rax, __NR_getdents64 | mov rax, #NtQueryDirectoryFile |
2. | mov rdi, Fd | mov rcx, Foo |
3. | mov rsi, Buffer | mov rdx, Bar |
4. | mov rdx, sizeof(Buffer) | mov r8, Baz |
5. | syscall | syscall |
6. | cmp rax, 0xFFFFFFFFFFFFF001 | test eax, eax |
1.パラメータのマーシャリング
まず初めに、ユーザーモードのソフトウェアが行うパラメーターのマーシャリング処理を見てみましょう。Windows NTカーネルでも同様に「rax」をシステムコールを判別するために使用しますが、システムコールのパラメータを保持するレジスターは、Linuxカーネルと異なります。
これはABIが異なっているためです。(「2.」〜「4.」)
2.システムコールの呼び出し
「5.」の処理にて特別なシステムコール命令を実行し、カーネルモードへ移行します。「syscall」命令は、x86においてシステムコールを呼び出すための推奨される命令です。
ここはLinuxカーネルでもWindows NTカーネルでも同じです。
3.システムコールの処理結果
「6.」でユーザーモードのソフトウェアがシステムコールの処理結果をチェックします。この部分はLinuxカーネルでもWindows NTカーネルで少し異なります。
「NtQueryDirectoryFile」システムコールの戻り値の型は「NTSTATUS」であり、エラーコードはマイナス値で表現されます。
一方「getdents64」システムコールの戻り値の型はintであり、エラーコードはある特定の範囲で収まるためです。
システムコールはいつ実行されるのか
「5.」と「6.」の間で「NtQueryDirectoryFile」システムコールが呼ばれ、Windows NTカーネルで処理が行われています。ここはLinuxカーネルと同じです。
システムコール命令が実行されるとCPUは、モード(リング)をカーネルモードに移行し、ユーザーモードのソフトウェアが呼び出したシステムコールに対応したカーネルモード内の特別な関数を実行します。
Windows NTカーネルのシステムコールの準備と実行
いくつかのABI周りの例外を除いて、LinuxカーネルとWindows NTカーネルのシステムコールの実行方法は同じです。ABI周りの例外とは、ユーザーモードのスレッドのレジスターの内容を保存する際、保存対象のレジスターが異なります。
またシステムコールのパラメーターが保存されるレジスターが異なります。
volatileレジスターは保存しない
Windows NTのシステムコールはx64呼び出し規約に従うため、Windows NTカーネルはvolatileレジスターを保存する必要がありません。保持しておく必要があるすべてのvolatileレジスターはシステムコールを呼び出す直前に、コンパイラーによって生成された命令により必要な処理が行われるためです。