なぜシステムコールについて気にするのか
システムコールとはOSが提供する機能をアプリケーションが利用する仕組みのことですが、システムコールについて知ることはアプリケーションの働きを理解する上で重要です。
なぜならアプリケーションの動作の中で、重要なもののほぼ全てはシステムコールを利用して実現されているからです。
例えば、ネットワークを利用した通信、ファイルへの入出力、新しいプロセスの生成、プロセス間通信、コンテナの生成などは、システムコールを使用することで実現されています。
逆にシステムコールを使わずにアプリケーションが出来ることと言えば、CPU上での計算とメモリへの入出力くらいでしょう。
この記事で解説する内容
この記事で解説する内容は、システムコールの一般的な性質や仕組みについてです。
そもそもシステムコールとは何なのか、どのような仕組みで実現されているのかというところを解説します。
さらには、システムコールをライブラリを介さず直接使用したり、システムコールの実装をカーネル内にまで踏み込んで調べる方法も紹介します。
内容は以下のようになります。
システムコールの目的と役割
Linux Kernel Developmentなどによるとシステムコールという仕組みを導入するメリットとして以下の2つがあります。
- ハードウェアを操作するシンプルなインターフェイスの提供
- アプリケーションが安全かつセキュアにOSのリソースを利用できる
1. ハードウェアを操作するシンプルなインターフェイスの提供
システムコールはアプリケーションに対して、ハードウェアを操作するための抽象化されたシンプルなインターフェイスを提供しています。これによってアプリケーションのコードは、背後にあるハードウェアの詳細に関して意識する必要がなくなります。
例えばシステムコールwrite
をバイト列を何らかの対象に書き込むための共通化されたインターフェイスです。
書き込み対象として指定できる、つまりファイルとして扱えるものは非常に多岐にわたっており、例えば、様々な種類のファイルシステムはもちろん、プロセス間通信に使われるパイプ、ネットワーク通信に使われるソケット、モニターなどの各種出力デバイスなどが含まれます。
LinuxやUnixの基本哲学にすべてのものはファイルである
というものがありますが、write
を含め入出力に使われる各種のシステムコールはこれを具現しているものと言えるでしょう。
2. アプリケーションが安全かつセキュアにOSのリソースを利用できる
システムコールはアプリケーションとOSの管理するリソースとの間を仲介しており、アプリケーションがリソースを間違った方法で利用することを防いだり、セキュリティ上問題のあるような利用を防ぐ役割をもっています。これによってアプリケーションはリソースの利用を安全かつセキュアに行うことができます。
リソースの安全な利用の例としては、例えばプロセスによるメモリ領域の確保(mmap
を使用した)があるでしょう。メモリというハードウェアリソースは、他のプロセスやOSと共有しており、過った方法で利用すると他のプロセスやOSを破壊してしまう恐れがあります。プロセスがメモリ領域を確保したいときにこのシステムコールを介することで、OSが各プロセスへのメモリ領域の割り当てを安全な方法で行うことができます。
また、リソースのセキュアな利用の例としては、権限情報に基づくファイルへのアクセス制御などがあるでしょう。
アプリケーションがシステムコールを呼び出す仕組み
アプリケーションはシステムコールを呼び出すときに何をやっているのでしょうか。普通の関数呼び出しと何が違うのでしょうか。ここでは、アプリケーションがシステムコールを呼び出す仕組みについて解説していきます。
アプリケーションがシステムコールを呼び出す際に行う手順は、以下の3ステップです。
- 実行するシステムコールを指定する番号をレジスタ(CPUに内蔵された極小メモリ)にセットする
- システムコールに渡す引数をレジスタにセットする
- システムコールを発動するインストラクション/命令文を実行する
以下では、画面(標準出力)へ文字を出力するコードを例にして、上記の手順について詳しく解説していきます。
文字出力を例にしたサンプル
システムコールwrite
は標準出力やファイルへの書き出しに使われますが、このシステムコールを呼び出すサンプルコードを使って、アプリケーションがシステムコールを呼び出す手順を見ていきましょう。
まず、サンプルコードを動かすのに使うgccのimageをコンテナとして立ち上げます。
$ docker container run -it --rm gcc /bin/bash
立ち上がったコンテナ内に以下のようなアセンブリコードが書かれたファイルhi.s
を作っていきます。コードの解説は後の方でやります。
# cat <<EOF > hi.s
.intel_syntax noprefix
.global main
main:
push rbp
mov rbp, rsp
push 0xA216948
mov rax, 1
mov rdi, 1
mov rsi, rsp
mov rdx, 4
syscall
mov rsp, rbp
pop rbp
ret
EOF
コンパイルして実行してみると以下のように文字が画面に出力されます。
# gcc -o hi.o hi.s; ./hi.o
Hi!
システムコールの呼び出し方はCPUの仕様/アーキテクチャごとに微妙に異なっているのですが、上記のコードはx86-64という最もポピュラーなアーキテクチャでのwrite
システムコールの呼び出しの例となっています。
上の紹介したサンプルコードについて解説する前に、まずはx86-64でのシステムコールの呼び出し方について説明していきます。なお他のアーキテクチャでもやってることはそんなに変わりません。
x86-64でのシステムコールの呼び出し方
x86-64でのシステムコールの呼び出しは以下の3ステップで行われます。
-
rax
レジスタにsystem call numberをセットする - システムコールに対して渡したい引数(もしあれば)をレジスタにセットする
-
syscall
のインストラクション/命令を呼び出す
ステップ1
まず、ステップ1ですが、ここではrax
というレジスタ(=CPUに内蔵された16~64bitサイズほどのメモリで、CPUから極めて高速にアクセス出来る)に、system call numberという、呼び出すシステムコールを指定する番号を格納します。この番号によってカーネルがどのシステムコールを実行したら良いのか識別する事ができます。
system call numberと対応するシステムコールは例えば以下のように指定されています。
system call number | システムコール名 | 内容 |
---|---|---|
0 | read | 読み込み |
1 | write | 書き出し |
2 | open | ファイルを開く |
57 | fork | 新しいプロセスを立ち上げる |
上記の例以外も含む網羅的な表はこちらで確認できます。
サンプルコード内だと、このステップに対応する部分は以下です。
mov rax, 1
ステップ2
次にステップ2ですが、ここではシステムコールに渡す引数をレジスタに格納していきます。
渡せる引数は6個までで、第1引数から順番にレジスタrdi
、rsi
、rdx
、r10
、r9
、r8
を使用することになっています。
システムコールwrite
の場合、渡す引数とレジスタは以下のように指定されています。
レジスタ名 | rdi | rsi | rdx |
---|---|---|---|
引数 | ファイルディスクリプタ | 書き出すバイト列の先頭アドレス | 書き出すバイト列のサイズ |
他のシステムコールでどのような引数を使えば良いかはここで確認できます。
ちなみにファイルディスクリプタ(rdi
レジスタにセットする)とは何なのかというと、これは各プロセスに紐付いた、プロセスからの入出力を制御するポート番号のようなものです。ファイルディスクリプタはデフォルトでは0~2の3つの値が使え、それぞれ 0 : 標準入力、1 : 標準出力、2 : 標準エラーに対応します。
ファイルやソケットを開くと、新しいファイルディスクリプタがさらに続けて3、4、5という様に割り当てられていき、それらに対して書き出しwrite
や読み込みread
などのシステムコールを実行する(システムコールに引数として渡す)ことができます。
サンプルコード内だと、このステップに対応する部分は以下です。
mov rdi, 1
mov rsi, rsp
mov rdx, 4
ステップ3
ステップ3ではsyscall
というインストラクション/命令文を発行します。
これによって、CPUはアプリケーションのコードを実行するのを中断し、カーネルのコードを実行するモードに切り替わった上で、カーネル内にあるシステムコールを実行するコード(システムコールハンドラー)へとジャンプします。
syscall
インストラクションを抜けたときには、戻り値がrax
レジスタにセットされていて、必要に応じて参照できます。
この部分については記事後半の[システムコールの実現方法と内部実装]の中で詳しく解説します。
サンプルコード内だと、このステップに対応する部分は以下です。
syscall
サンプルコードの解説
サンプルコードの意味が分かるようにコメントを付けてみました。ポイントはsyscall
と書かれた行とその直前です。
.intel_syntax noprefix # コードの書式
.global main # 実行開始地点のラベル
main:
# 前処理
push rbp
mov rbp, rsp
# 文字列'Hi!'をスタックに格納する
push 0xA216948
# ステップ1 system call number で'1'(=writeに対応)を指定する
mov rax, 1
# ステップ2 writeのシステムコールに渡す引数をレジスタにセットする
mov rdi, 1 # 書き出し対象のファイルディスクリプタで1(標準出力)をセットする
mov rsi, rsp # スタックの先頭アドレス(文字`H`が入っている)をセットする
mov rdx, 4 # 出力する文字列のサイズをセットする
# ステップ3 システムコールを発行する
syscall
# 後処理
mov rsp, rbp
pop rbp
ret
ちなみに上記のコードの中のステップ2で文字列の先頭アドレスを渡している部分について少し補足すると
mov rsi, rsp # スタックの先頭アドレス(文字`H`が入っている)をセットする
文字列はスタック上に格納されていますが、rsp
というレジスタはスタックの先頭アドレスを指している特殊なレジスタです。なのでrsp
の値をrsi
にセットすることで、文字列の先頭アドレスをシステムコールに渡しているということになります。
[補足] システムコールの提供方法について
通常システムコールはC言語のライブラリとして提供されています。システムコールを利用する際は基本はこのラッパーライブラリを使えばよく、アセンブリコードを直接書いていく必要はありません。
例えば、write(2)
は以下のようなシグネチャを持ったCの関数として定義されています。
ssize_t write(int fd, const void *buf, size_t count);
一方で、Cのライブラリを使いたくない場合は、上の例でやったように自前でアセンブリコードとしてシステムコールの呼び出しを実装する必要があります。
例えばGo言語はそのようなアプローチをとっています。
golang/sys/unix/asm_linux_amd64.s
TEXT ·SyscallNoError(SB),NOSPLIT,$0-48
CALL runtime·entersyscall(SB)
MOVQ a1+8(FP), DI
MOVQ a2+16(FP), SI
MOVQ a3+24(FP), DX
MOVQ $0, R10
MOVQ $0, R8
MOVQ $0, R9
MOVQ trap+0(FP), AX // syscall entry
SYSCALL
MOVQ AX, r1+32(FP)
MOVQ DX, r2+40(FP)
CALL runtime·exitsyscall(SB)
RET
上記のコードについて補足すると、AX
のような見覚えのないレジスタはrax
レジスタなどの16bit部分を指す別名になります。
[補足] CPUが機械語を実行していく仕組み
システムコールが発行されると、CPUは現在実行中のコードから、カーネル内のコードへとジャンプします。このような、実行するコードを途中でジャンプさせる機能はCPU上でどのようにして実現されているのでしょうか。これを理解するには、そもそもCPUがどのようにコード(機械語)を実行しているのかを理解する必要があります。
CPU上での機械語の実行は以下のステップを繰り返すことで行われます。
- メモリから一つの命令文/インストラクションを読み出す
- インストラクションを実行し、レジスタの値の更新などを行う
- 現在実行中のインストラクションのアドレスが格納されているレジスタの値を更新し、次のインストラクションの読み出しに備える
インストラクションとは、CPUが一つの命令として解釈できるバイト列の事で、アセンブリコードの一行のコードとほぼ一対一対応します。例えば上で例として挙げたアセンブリコードだと、機械語とインストラクションの対応は以下のようになります。
# objdump -d -M intel ./hi.o | grep syscall -B 4
66c: 48 c7 c0 01 00 00 00 mov rax,0x1
673: 48 c7 c7 01 00 00 00 mov rdi,0x1
67a: 48 89 e6 mov rsi,rsp
67d: 48 c7 c2 20 00 00 00 mov rdx,0x20
684: 0f 05 syscall
また、現在実行中のインストラクションのアドレスが格納されているレジスタは、プログラムカウンターレジスタやインストラクションポインターレジスタと呼ばれ、一つのインストラクションを実行すると加算され直後のインストラクションのアドレスの値に更新されます。これによってコードが逐次的に実行されていきます。
CPUが実行するコードを途中でジャンプさせる機能ですが、これはプログラムカウンターレジスタを直接書き換えるようなインストラクション(例えばjmp
)を発行することで行なえます。これによって実現できるものとしては、システムコールでのカーネル内へのコードのジャンプだけではなく、さらに一般には条件分岐やループ処理、関数呼び出しなどが含まれます。
システムコールの実現方法と内部実装
システムコールの大まかな処理の流れは以下のようになります。
- システムコールを呼び出すインストラクションを実行し、カーネル内のコードにジャンプする
- カーネル内でsystem call numberで指定されたシステムコールの実装を呼び出す
- カーネル内でシステムコールの呼び出し元へ復帰するインストラクションを実行し、元のコードに復帰する
ステップ1とステップ3は、カーネル内へのジャンプとカーネル内からの復帰を行っていますが、これはx86-64の場合は専用のインストラクション、syscall
とsysretq
によって実現されてます。
言い換えると、この部分はハードウェア的に実装されている、つまりx86-64の仕様を満たすように組まれたCPU内の論理回路によって実装されているとも言えるでしょう。
ステップ2ですが、この処理はカーネル内のシステムコールハンドラーというコードで行われます。
システムコールハンドラー内でsystem call numberに基づいた各システムコールの実装へのディスパッチが行われます。
もう少し詳細に、システムコールの処理を分解すると以下のようになります。
-
syscall
インストラクションを実行し、システムコールハンドラーのアドレスが入ったレジスタの値をプログラムカウンターレジスタに読み込む - システムコールハンドラーが処理を受け付ける
- システムコールハンドラー内でsystem call numberに基づいて各システムコールの実装へとディスパッチする
-
sysretq
インストラクションによってユーザープロセス上の元のコードに復帰する - システムコールハンドラーのアドレスを起動時にレジスタに登録する
図にするとこんな感じです。
以下で(1)~(5)の各要素について解説していきます。
(1) syscallインストラクションの実行
syscall
のインストラクションをCPUが実行すると、大まかには以下のようなことが行われます。
- CPUの現在の実行モードを表す
FLAGS
レジスタの値をR11
レジスタに退避させる -
FLAGS
レジスタの値をIA32_FMASK MSR
レジスタの値でマスクし、CPUがカーネルのコードを実行できるモードへと切り替わる - プログラムカウンターレジスタ
RIP
の値をRCX
レジスタに退避させる - プログラムカウンターレジスタ
RIP
に、システムコールハンドラーのアドレスをIA32_LSTAR MSR
レジスタから読み込み、システムコールハンドラーへジャンプする
1. FLAGSレジスタはCPUの現在のCPUの状態/実行モードを表すレジスタで、例えばCPUがプロテクションリング上のどこにいるかなどを表しています。システムコールからユーザープロセスに戻るときに元の状態に復元したいので、R11レジスタに現在の値を退避させます。
2. FLAGS
レジスタの値をIA32_FMASK MSR
レジスタの値でマスクすることによってCPUのモードが切り替わり、Privilege Level 0、つまりカーネルのコードを実行可能なモードへと遷移します。
Privilege LevelはFLAGSレジスタの12~13bitで表現されていますが、これらを00
(Privilege Level 0)とするために以下のようなオペレーションがsyscall
呼び出し時に行われます。
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
マスクの役割(NOT
を取ってることに注意)を果たすIA32_FMASK
レジスタへの値のセットはカーネル内だと以下のコードで行われています。
linux/arch/x86/kernel/cpu/common.c
/* Flags to clear on syscall */
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_TF|X86_EFLAGS_DF|X86_EFLAGS_IF|
X86_EFLAGS_IOPL|X86_EFLAGS_AC|X86_EFLAGS_NT);
上記のX86_EFLAGS_IOPL
は12~13bitを00
にする役割のマスクですが実際以下のように、12~13bitのみが1になるような値として定義されていることが分かります。
linux/arch/x86/include/uapi/asm/processor-flags.h
#define X86_EFLAGS_IOPL_BIT 12 /* I/O Privilege Level (2 bits) */
#define X86_EFLAGS_IOPL (_AC(3,UL) << X86_EFLAGS_IOPL_BIT)
3. プログラムカウンターレジスタRIP
の値をRCX
レジスタに退避させます。これをしないと、システムコールハンドラーからユーザープロセスへ復帰するときに、もといたインストラクションの場所(=プログラムカウンターの値)が分からなくなってしまいます。
4. プログラムカウンターレジスタRIP
へIA32_LSTAR MSR
にセットされたシステムコールハンドラーのアドレスを読み込み、システムコールハンドラーへとジャンプします。IA32_LSTAR MSR
へのハンドラーのアドレスの読み込みがどのように行われているかは(5)で紹介します。
その他syscall
が呼ばれたときの詳しいCPUの挙動に関してはここを参照してください。
(2) システムコールハンドラー
システムコールハンドラーとは、syscall
インストラクションが呼ばれた後に実行されるカーネル内のコードで、この中でシステムコールの処理が行われます。
ここでは、システムコールハンドラーの前処理の部分、引数としてレジスタに渡された値を構造体に詰めて後続の処理に渡す部分を紹介します。
まず、システムコールハンドラーの入り口/エントリポイントは以下のコードになります。
linux/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
このentry_SYSCALL_64
の中では様々な処理が行われるのですが、その中の一つとして、以下のようにレジスタに引数として渡された値フィールドに持つような構造体をスタック上に構築しています。
linux/arch/x86/entry/entry_64.S
/* Construct struct pt_regs on stack */
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
GLOBAL(entry_SYSCALL_64_after_hwframe)
pushq %rax /* pt_regs->orig_ax */
PUSH_AND_CLEAR_REGS rax=$-ENOSYS
上記のコードでは、スタック上の構造体pt_regs
に、rcx
やr11
レジスタ(syscall
インストラクション呼び出し時に退避用使われていた)やrax
レジスタの値を代入しています。またシステムコールの引数として使われているrdi
やrsi
などの値の構造体への代入は最後のマクロPUSH_AND_CLEAR_REGS
の中で定義されています。
上で作った構造体とsystem call numberを、システムコールの処理を行う関数do_syscall_64
に以下で渡しています。
linux/arch/x86/entry/entry_64.S#L173-L175
movq %rax, %rdi
movq %rsp, %rsi
call do_syscall_64 /* returns with IRQs disabled */
なお、do_syscall_64
への引数の渡し方ですがレジスタを使用して行っています。x86-64で関数に引数を渡すときのルールとして、第1引数から順番に、rdi
、 rsi
、 rdx
、 rcx
、 r8
、 r9
のレジスタを使うことになってます。
なので以下のようなシグネチャを持った関数do_syscall_64
に対して引数を渡したい場合、
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
2つのレジスタrdi
とrsi
に渡したい値をセットすることで行なえます。
-
movq %rax, %rdi
で、第1引数に対応するrdi
レジスタに対して、rax
にセットされたsystem call numberを渡す -
movq %rsp, %rsi
で、第2引数に対応するrsi
レジスタに対して、スタックの先頭アドレス(つまりスタックに積まれた構造体の先頭アドレス)を指すrsp
レジスタの値を渡す
(3) 各システムコールへディスパッチ
do_syscall_64
内で各システムコールの実装の呼び出しが行われます。具体的には、各システムコールを実装する関数が入った配列sys_call_table
対して、要素をsystem call numberで指定することで、対応するシステムコールの実装へとディスパッチが行われます。
また、各システムコールの実装はSYSCALL_DEFINE*
マクロで定義されていて、これを目印に見つけることができます。
以下、該当するカーネルのコードを見ていきましょう。
do_syscall_64
関数の引数として、第1引数nr
にsystem call numberが、第2引数regs
にシステムコール呼び出し時のレジスタの値で構成された構造体が渡されています。
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
struct thread_info *ti;
enter_from_user_mode();
local_irq_enable();
sys_call_table
はここで定義されている、各システムコールの処理を実装した関数の配列ですが、その配列の要素をsystem call numbernr
で指定して、構造体regs
を渡すことで、各システムコールの処理へとディスパッチしています。
if (likely(nr < NR_syscalls)) {
nr = array_index_nospec(nr, NR_syscalls);
regs->ax = sys_call_table[nr](regs);
}
regs->ax = sys_call_table[nr](regs);
がディスパッチを行っている部分です。また、関数の戻り値がAX
レジスタ(=raxレジスタ)に対応する構造体のフィールドにセットされていますが、この値がsyscall
インストラクションを抜けた後に、戻り値として実際にrax
レジスタにセットされることになります。
sys_call_table
配列の要素となる、実際にシステムコールを処理する関数ですが、例えばwrite
の処理は以下で実装されています。
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)
{
return ksys_write(fd, buf, count);
}
一般に、各システムコールの実装は、SYSCALL_DEFINE*
マクロで定義されているので、これを目印に各実装を見つけることができます。
sys_call_table
とSYSCALL_DEFINE*
マクロに関する詳細に関しては、こちらの記事が参考になります。
(4) sysretqインストラクションで復帰
システムコールハンドラーから、ユーザープロセス内の元のコードへの復帰はsysretq
インストラクションによって行われます。sysetq
インストラクションで行われる処理はほぼsyscall
の逆の操作です。
-
RFLAGS
レジスタに対して、R11
レジスタ(RFLAGS
の元の値を退避させていた)の値を読み込むことで元の値に戻し、ユーザープロセスを実行するCPUのモード(Privilege level 3)へ復帰 - プログラムカウンターレジスタ
RIP
に対して、RCX
レジスタ(RIP
の元の値を退避させていた)の値を読み込むことで元の値に戻し、syscall
を呼び出した元のインストラクションの地点に復帰する
この他にも色々な処理が行われているのですが、詳細はこちらを参照してください。
カーネル内でsysretq
を呼び出すコードですが、まずシステムコールハンドラーで最後に到達するコードは以下で、
linux/arch/x86/entry/entry_64.S
popq %rdi
popq %rsp
USERGS_SYSRET64
END(entry_SYSCALL_64)
この中のUSERGS_SYSRET64
にてsysretq
が呼び出されています。
linux/arch/x86/include/asm/irqflags.h
#define USERGS_SYSRET64 \
swapgs; \
sysretq;
(5) 起動時にシステムコールハンドラーのアドレスをレジスタへ読み込む
syscall
インストラクションが呼ばれたときに、プログラムカウンターレジスタRIP
に対して、システムコールハンドラーのアドレスをIA32_LSTAR MSR
レジスタから読み込むことで、システムコールハンドラーへのジャンプが行われるのでした。では、IA32_LSTAR MSR
レジスタはどのようにして、システムコールハンドラーのアドレスを知っているのでしょうか。
システムコールハンドラーのアドレスは、CPUの初期化時にIA32_LSTAR MSR
レジスタへ読み込まれます。
以下ではこの初期化処理に対応するカーネルのコードを紹介していきます。
CPUの初期化は以下で行われていますが、
linux/arch/x86/kernel/cpu/common.c
/*
* cpu_init() initializes state that is per-CPU. Some data is already
* initialized (naturally) in the bootstrap process, such as the GDT
* and IDT. We reload them nevertheless, this function acts as a
* 'CPU state barrier', nothing should get across.
*/
#ifdef CONFIG_X86_64
void cpu_init(void)
{
この中で呼ばれているsyscall_init();
によってアドレスのセットが行われます。
linux/arch/x86/kernel/cpu/common.c
void syscall_init(void)
{
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
上のコードの中の
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
によって、システムコールハンドラーのアドレスがIA32_LSTAR MSR
レジスタ対して書き込まれています。
定数MSR_LSTAR
とentry_SYSCALL_64
ですが、定数MSR_LSTAR
は書き込みターゲットのレジスタとしてIA32_LSTAR MSR
レジスタを指定しています。
linux/arch/x86/include/asm/msr-index.h
#define MSR_LSTAR 0xc0000082 /* long mode SYSCALL target */
entry_SYSCALL_64
は、ここでプロトタイプ宣言されている値で、システムコールハンドラーのエントリポイントのアドレスを指しています。
[補足] システムコールと割り込みの関係
システムコールについて調べると、割り込み(インターラプト)という概念によく遭遇すると思います。
例えばカーネル内だと以下のコードのコメントでInterrupts are off
と書かれていますが、interrupts
(割り込み)とは一体なんなのでしょうか。システムコールとどんな関わりがあるのでしょうか。
linux/arch/x86/entry/entry_64.S
ENTRY(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
まず割り込みとシステムコールの関係ですが、システムコールは割り込みの一種とみなせます。
割り込みが発生すると、CPUは現在実行中のコード(ユーザープロセス内/カーネル内問わず)を中断し、後で再開可能なように実行状態を退避させた上でカーネル内の特定のコード(=インターラプトハンドラー)にジャンプします。これまで見てきたシステムコールとやってることは大体同じです。
割り込みの発生のさせかたには2種類あり、ソフトウェア起因のものをソフトウェア割り込み、ハードウェア起因のものをハードウェア割り込みと言います。
ソフトウェア割り込みの例
- システムコールの呼び出し
- 例外発生時の処理
ハードウェア割り込みの例
- キーボード入力への応答
- ネットワークカードにパケットが到達したときの処理
割り込みの仕組みのおかげで、「何かが起きたときにCPUを特定のコードに問答無用でジャンプさせて処理させる」という機能を実装することができ、これによって例えばハードウェアからの入力に対して迅速に応答することが可能になります。
なお、x86-64ではシステムコール用に専用のインストラクション(syscall
とsysretq
)が用意されていますが、一世代前のx86-32や他のアーキテクチャではシステムコールはインターラプトの仕組みの中で実現されています。
例えばx86-32でのシステムコールの実装はint
インストラクションにベクタ0x80
を指定したint 0x80
というインストラクションを呼び出すことで行われていました。
割り込み/irqをオフにするとはどういうことなのか
システムコールハンドラー内は一部は割り込みがオフ、つまりirq
がオフの状態で実行されます。
irq
とはインターラプトリクエストのことで、これをCPUが受け取ることで割り込みが発生します。
一般に、インターラプトハンドラーが割り込みを処理している時には、irq
の一部はまたは全てを無効化することがあります。これによって割り込みの処理をしている最中に割り込みが発生するという状況が起きないようにできます。
irq
がオフになっている状態だと割り込みを受け付けられないので、例えばキーボードの入力に応答することができません。なので、irq
がオフの状態で実行するコードは、処理の時間がかからないように充分短い必要があります。
割り込みに関してさらに詳細が知りたい人はこちらのページが参考になります。
次のステップに進むには
システムコールやカーネルについてさらに進んで調べていくための資料やキーワードを紹介します。
この記事を書くのに参考にした文献の紹介も兼ねています。
システムコールにはどのような種類があるのか
x86-64やアセンブリコードについて
インストラクションについて調べる時はこちらを参考にしました。
x86-64のアセンブリコードの入門としては低レイヤを知りたい人のためのCコンパイラ作成入門もオススメです。
Linux Kernelについて
Linux Kernel Developmentは非常に評価が高く、カーネルの内部実装の解説本としてはおそらく最良でしょう。
内容的には決して簡単ではない(少なくとも自分にとっては)のですが、語り口がたまに面白く、説明の仕方も非常に丁寧なのでオススメです。
カーネルに興味がない人にも、カーネル内の並行プログラミンのテクニックを解説した10章は色々と参考になるかもしれません。
またLKDの内容をまとめたこのノートもオススメです。
カーネルのソースコードの読み方
実はこの記事を書くにあたって初めてカーネルのコードをちゃんと読みました。
感想としては、もちろんコードに書かれていることの全てを理解できるわけではないのですが、処理の大体の流れを追っていく分にはそこまで難しくない印象です。コメントが丁寧に書かれている部分も結構ありますので。
読み方としては、githubでキーワード検索(/
キーを押して)したり、こちらの定義元ジャンプ機能を使ったりして読んでいました。
自分もカーネルに関しては素人なので、もっと良い読み方があるかも知れませんが 、、、
OSの仕組みや働きについて
OSにまつわる基本的に重要な概念として、プロセス、仮想メモリ、マルチタスキングは抑えておいて良いと思います。オススメのキーワードは以下です。
-
プロセス
- プログラムカウンター
- スタックポインタ
- プロセス制御ブロック
-
仮想メモリ
- 仮想アドレス
- 物理アドレス
- メモリ管理ユニット
- ページング
-
マルチタスキング
- プロセスの状態遷移
- コンテキストスイッチ
- プロセススケジューリング