298
252

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

システムコールとは何なのか

Last updated at Posted at 2019-06-13

なぜシステムコールについて気にするのか

システムコールとはOSが提供する機能をアプリケーションが利用する仕組みのことですが、システムコールについて知ることはアプリケーションの働きを理解する上で重要です。

なぜならアプリケーションの動作の中で、重要なもののほぼ全てはシステムコールを利用して実現されているからです。
例えば、ネットワークを利用した通信、ファイルへの入出力、新しいプロセスの生成、プロセス間通信、コンテナの生成などは、システムコールを使用することで実現されています。
逆にシステムコールを使わずにアプリケーションが出来ることと言えば、CPU上での計算とメモリへの入出力くらいでしょう。

この記事で解説する内容

この記事で解説する内容は、システムコールの一般的な性質や仕組みについてです。
そもそもシステムコールとは何なのか、どのような仕組みで実現されているのかというところを解説します。
さらには、システムコールをライブラリを介さず直接使用したり、システムコールの実装をカーネル内にまで踏み込んで調べる方法も紹介します。

内容は以下のようになります。

システムコールの目的と役割

Linux Kernel Developmentなどによるとシステムコールという仕組みを導入するメリットとして以下の2つがあります。

  1. ハードウェアを操作するシンプルなインターフェイスの提供
  2. アプリケーションが安全かつセキュアにOSのリソースを利用できる

1. ハードウェアを操作するシンプルなインターフェイスの提供

システムコールはアプリケーションに対して、ハードウェアを操作するための抽象化されたシンプルなインターフェイスを提供しています。これによってアプリケーションのコードは、背後にあるハードウェアの詳細に関して意識する必要がなくなります。
例えばシステムコールwriteをバイト列を何らかの対象に書き込むための共通化されたインターフェイスです。
書き込み対象として指定できる、つまりファイルとして扱えるものは非常に多岐にわたっており、例えば、様々な種類のファイルシステムはもちろん、プロセス間通信に使われるパイプ、ネットワーク通信に使われるソケット、モニターなどの各種出力デバイスなどが含まれます。
LinuxやUnixの基本哲学にすべてのものはファイルであるというものがありますが、writeを含め入出力に使われる各種のシステムコールはこれを具現しているものと言えるでしょう。

2. アプリケーションが安全かつセキュアにOSのリソースを利用できる

システムコールはアプリケーションとOSの管理するリソースとの間を仲介しており、アプリケーションがリソースを間違った方法で利用することを防いだり、セキュリティ上問題のあるような利用を防ぐ役割をもっています。これによってアプリケーションはリソースの利用を安全かつセキュアに行うことができます。

リソースの安全な利用の例としては、例えばプロセスによるメモリ領域の確保(mmapを使用した)があるでしょう。メモリというハードウェアリソースは、他のプロセスやOSと共有しており、過った方法で利用すると他のプロセスやOSを破壊してしまう恐れがあります。プロセスがメモリ領域を確保したいときにこのシステムコールを介することで、OSが各プロセスへのメモリ領域の割り当てを安全な方法で行うことができます。
また、リソースのセキュアな利用の例としては、権限情報に基づくファイルへのアクセス制御などがあるでしょう。

アプリケーションがシステムコールを呼び出す仕組み

アプリケーションはシステムコールを呼び出すときに何をやっているのでしょうか。普通の関数呼び出しと何が違うのでしょうか。ここでは、アプリケーションがシステムコールを呼び出す仕組みについて解説していきます。

アプリケーションがシステムコールを呼び出す際に行う手順は、以下の3ステップです。

  1. 実行するシステムコールを指定する番号をレジスタ(CPUに内蔵された極小メモリ)にセットする
  2. システムコールに渡す引数をレジスタにセットする
  3. システムコールを発動するインストラクション/命令文を実行する

以下では、画面(標準出力)へ文字を出力するコードを例にして、上記の手順について詳しく解説していきます。

文字出力を例にしたサンプル

システムコール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ステップで行われます。

  1. raxレジスタにsystem call numberをセットする
  2. システムコールに対して渡したい引数(もしあれば)をレジスタにセットする
  3. 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引数から順番にレジスタrdirsirdxr10r9r8を使用することになっています。

システムコール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上での機械語の実行は以下のステップを繰り返すことで行われます。

  1. メモリから一つの命令文/インストラクションを読み出す
  2. インストラクションを実行し、レジスタの値の更新などを行う
  3. 現在実行中のインストラクションのアドレスが格納されているレジスタの値を更新し、次のインストラクションの読み出しに備える

インストラクションとは、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)を発行することで行なえます。これによって実現できるものとしては、システムコールでのカーネル内へのコードのジャンプだけではなく、さらに一般には条件分岐やループ処理、関数呼び出しなどが含まれます。

システムコールの実現方法と内部実装

システムコールの大まかな処理の流れは以下のようになります。

  1. システムコールを呼び出すインストラクションを実行し、カーネル内のコードにジャンプする
  2. カーネル内でsystem call numberで指定されたシステムコールの実装を呼び出す
  3. カーネル内でシステムコールの呼び出し元へ復帰するインストラクションを実行し、元のコードに復帰する

ステップ1とステップ3は、カーネル内へのジャンプとカーネル内からの復帰を行っていますが、これはx86-64の場合は専用のインストラクション、syscallsysretqによって実現されてます。
言い換えると、この部分はハードウェア的に実装されている、つまりx86-64の仕様を満たすように組まれたCPU内の論理回路によって実装されているとも言えるでしょう。

ステップ2ですが、この処理はカーネル内のシステムコールハンドラーというコードで行われます。
システムコールハンドラー内でsystem call numberに基づいた各システムコールの実装へのディスパッチが行われます。

もう少し詳細に、システムコールの処理を分解すると以下のようになります。

  1. syscallインストラクションを実行し、システムコールハンドラーのアドレスが入ったレジスタの値をプログラムカウンターレジスタに読み込む
  2. システムコールハンドラーが処理を受け付ける
  3. システムコールハンドラー内でsystem call numberに基づいて各システムコールの実装へとディスパッチする
  4. sysretqインストラクションによってユーザープロセス上の元のコードに復帰する
  5. システムコールハンドラーのアドレスを起動時にレジスタに登録する

図にするとこんな感じです。

syscall (4).png

以下で(1)~(5)の各要素について解説していきます。

(1) syscallインストラクションの実行

syscallのインストラクションをCPUが実行すると、大まかには以下のようなことが行われます。

  1. CPUの現在の実行モードを表すFLAGSレジスタの値をR11レジスタに退避させる
  2. FLAGSレジスタの値をIA32_FMASK MSRレジスタの値でマスクし、CPUがカーネルのコードを実行できるモードへと切り替わる
  3. プログラムカウンターレジスタRIPの値をRCXレジスタに退避させる
  4. プログラムカウンターレジスタ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呼び出し時に行われます。

x86/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. プログラムカウンターレジスタRIPIA32_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に、rcxr11レジスタ(syscallインストラクション呼び出し時に退避用使われていた)やraxレジスタの値を代入しています。またシステムコールの引数として使われているrdirsiなどの値の構造体への代入は最後のマクロ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引数から順番に、rdirsirdxrcxr8r9のレジスタを使うことになってます。
なので以下のようなシグネチャを持った関数do_syscall_64に対して引数を渡したい場合、

linux/arch/x86/entry/common.c

__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)

2つのレジスタrdirsiに渡したい値をセットすることで行なえます。

  • 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にシステムコール呼び出し時のレジスタの値で構成された構造体が渡されています。

linux/arch/x86/entry/common.c

__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を渡すことで、各システムコールの処理へとディスパッチしています。

linux/arch/x86/entry/common.c

	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の処理は以下で実装されています。

linux/fs/read_write.c

SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
		size_t, count)
{
	return ksys_write(fd, buf, count);
}

一般に、各システムコールの実装は、SYSCALL_DEFINE*マクロで定義されているので、これを目印に各実装を見つけることができます。

sys_call_tableSYSCALL_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_LSTARentry_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ではシステムコール用に専用のインストラクション(syscallsysretq)が用意されていますが、一世代前の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にまつわる基本的に重要な概念として、プロセス、仮想メモリ、マルチタスキングは抑えておいて良いと思います。オススメのキーワードは以下です。

  • プロセス

    • プログラムカウンター
    • スタックポインタ
    • プロセス制御ブロック
  • 仮想メモリ

    • 仮想アドレス
    • 物理アドレス
    • メモリ管理ユニット
    • ページング
  • マルチタスキング

    • プロセスの状態遷移
    • コンテキストスイッチ
    • プロセススケジューリング

その他参考文献

298
252
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
298
252

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?