kledgeb Ubuntuの使い方や日本語化、アプリの使い方を紹介しています。

システムコールとは(前編)

「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));

このコードをアセンブラで表現すると、以下のようになります。

  1. mov rax, __NR_getdents64
  2. mov rdi, Fd
  3. mov rsi, Buffer
  4. mov rdx, sizeof(Buffer)
  5. syscall
  6. 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);

このコードをアセンブラで表現すると、以下のようになります。

  1. mov rax, #NtQueryDirectoryFile
  2. mov rcx, Foo
  3. mov rdx, Bar
  4. mov r8, Baz
  5. syscall
  6. 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レジスターはシステムコールを呼び出す直前に、コンパイラーによって生成された命令により必要な処理が行われるためです。

参考



関連記事一覧
オプション