連載
» 2017年06月22日 05時00分 公開

Linuxカーネルのソースコードを読んで、システムコールを探るmain()関数の前には何があるのか(6)(2/3 ページ)

[坂井弘亮,著]

見るべきファイルを限定していく

 grepで検索した場合、このように大量のファイルがヒットしてしまい引いてしまうこともあるかもしれないが、こういうときにはコツがあって、あまり恐れることは無い。

 まず考えるべきことは、見なくていいファイルを間引くことだ。

 知りたいのは、割込み処理だ。つまりヘッダファイルは除いていいだろう。さらに割込み処理はアセンブラで書いてあるはずなので、*.sか*.Sというファイルを見るべきと言える。

[user@localhost x86]$ find . -name "*.s"
[user@localhost x86]$

 *.sというファイルは無いようだ。*.Sのみ検索すればいいだろう。

[user@localhost x86]$ find . -name "*.S" | xargs grep syscall
./vdso/vdso32/sysenter.S: * You can not use this vsyscall for the clone() syscall because the
./vdso/vdso32/sysenter.S:	.globl __kernel_vsyscall
./vdso/vdso32/sysenter.S:	.type __kernel_vsyscall,@function
...

 こちらはヒットしているファイルがいくつもあるようだ。ヒット数を調べてみよう。

[user@localhost x86]$ find . -name "*.S" | xargs grep syscall | wc -l
246
[user@localhost x86]$

 約250件なので、だいぶしぼり込むことができた。

 さらに、ディレクトリでもしぼり込むことができる。実際に見てみるとvdsoやxenといったディレクトリのファイルもヒットしているのだが、これらは名前からして、あまり関係が無さそうだ。そして関係があるのはkernelというディレクトリのみのように思える。

 ということでkernel以下のみで検索しなおしてみよう。

[user@localhost x86]$ find kernel -name "*.S" | xargs grep syscall | wc -l
127
[user@localhost x86]$

 半分くらいにすることができた。

 次はファイル名を見てみよう。

[user@localhost x86]$ find kernel -name "*.S" | xargs grep syscall | perl -pe 's/:.*//' | uniq
kernel/entry_64.S
kernel/head_64.S
kernel/entry_32.S
kernel/syscall_table_32.S
kernel/vmlinux.lds.S
[user@localhost x86]$

 5つのファイルがヒットしていることがわかる。

 しかしentry_64.Sとhead_64.Sは、名前からして64ビット対応のものだろう。

 またvmlinux.lds.Sは名前からしてリンクの際に参照されるリンカスクリプトのように思えるので、これも除外できる。

 残りはentry_32.Sとsyscall_table_32.Sという2つのファイルだ。ここまで来たら、あとはファイルをビューワで開いて中身を見てみてもいいだろう。

 syscall_table_32.Sは実際に見てみると以下のようになっており、名前の通りシステムコールのテーブルがあるだけということがわかる。ちなみにテーブルの名前はsys_call_tableとなっているようなのだが、これは後で言及するので覚えておこう。

ENTRY(sys_call_table)
	.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
	.long sys_exit
	.long ptregs_fork
	.long sys_read
	.long sys_write
	.long sys_open	/* 5 */
	.long sys_close
...

 ということで、entry_32.Sというファイルを見ればよさそうだ。

 ところでここまでにいろいろと説明をしてきているが、そろそろ「そもそも何をしたいんだっけ?」と思ってしまっている読者のかたも多いかもしれない。

 ここで本来の目的を振り返っておこう。そもそもの目的は、int $0x80が呼ばれたときのシステムコール例外の処理を探すことだ。そのために「syscall」をキーワードに検索をかけ、entry_32.Sというファイルがあやしい、というところまで来ている。

 巨大なソースコードを追いかけているときにはこのように、本来の目的を忘れてしまうことも多くある。定期的に本来の目的を振り返るようにするといいだろう。

割込みハンドラを見る

 entry_32.Sを見ていこう。entry_32.Sの内部でsyscallというキーワードで検索すると、以下のような部分がある。

529:syscall_call:
530:	call *sys_call_table(,%eax,4)
531:syscall_after_call:
532:	movl %eax,PT_EAX(%esp)		# store the return value
533:syscall_exit:
534:	LOCKDEP_SYS_EXIT
535:	DISABLE_INTERRUPTS(CLBR_ANY)	# make sure we don't miss an interrupt
536:					# setting need_resched or sigpending
537:					# between sampling and the iret
538:	TRACE_IRQS_OFF
539:	movl TI_flags(%ebp), %ecx
540:	testl $_TIF_ALLWORK_MASK, %ecx	# current->work
541:	jne syscall_exit_work
...

 syscall_callというラベルの位置で、sys_call_tableというアドレスに対してEAXレジスタを4倍した値を加算し、そこに対して関数呼び出しを行っている。

 さらにsys_call_tableはsyscall_table_32.Sでシステムコールのテーブルとして定義されていたことを思い出してほしい。これは関数のアドレス一覧になっており、言い替えると関数へのポインタの配列になっている。

 つまりこれはC言語風に書くと、以下のような呼び出しをしていることになる。

int (*sys_call_table[])(void) = {
	sys_restart_syscall,
	sys_exit,
	ptregs_fork,
	sys_read,
	sys_write,
	...
};
 
sys_call_table[EAX]();

 つまり、関数へのポインタの配列であるsys_call_table[]に対して、sys_call_table[EAX]に登録されている関数を呼び出しているということだ。

 これは言いかたを変えると、EAXレジスタをインデックスにしてシステムコール・テーブルに登録されている関数を呼び出している、ということになる。

 そしてそれらの関数が、システムコールの処理関数だ。つまりEAXをシステムコール番号として、それをインデックスにして配列から処理関数を得て呼び出しているということだ。

割込みハンドラの登録

 システムコールの処理関数の呼び出しは、entry_32.S内のsyscall_callというラベル位置で行われていた。

 このsyscall_callの処理はどこから呼ばれているのだろうか。

 ソースコードを読むときのコツとして、注目した箇所の付近の部分も見ておく、というものがある。結び付きの強いものは、ソースコード上でも近くに置かれていることが多いためだ。

 ということでsyscall_callの周りを見てみると、直上に以下のような部分があった。

517:	# system call handler stub
518:ENTRY(system_call)
519:	RING0_INT_FRAME			# can't unwind into user space anyway
520:	pushl %eax			# save orig_eax
521:	CFI_ADJUST_CFA_OFFSET 4
522:	SAVE_ALL
523:	GET_THREAD_INFO(%ebp)
524:					# system call tracing in operation / emulation
525:	testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
526:	jnz syscall_trace_entry
527:	cmpl $(nr_syscalls), %eax
528:	jae syscall_badsys
529:syscall_call:
530:	call *sys_call_table(,%eax,4)
...

 つまりsystem_callという処理が呼び出され、そのまま下にあるsyscall_callの処理に入っていっているようだ。なおentry_32.Sを見てみるとsyscall_trace_entryを経由したsyscall_callの呼び出しも他にあるようなのだが、トレース処理のようなのでとりあえず無視しよう。

 system_callというシンボルがどのように利用されているのか、調べてみよう。grepで検索してみると、以下のような箇所があった。

[user@localhost x86]$ grep -r system_call .
...
./kernel/traps.c:	set_system_trap_gate(SYSCALL_VECTOR, &system_call);
[user@localhost x86]$

 おそらくこれは割込みハンドラの登録処理だろう。つまり割込みハンドラとしてsystem_callが登録されているわけだ。なおこのような割込みハンドラのことを一般にISR(Interrupt Service Routine)と呼んだりする。

 ということは、system_callがint $0x80が実行されたときのソフトウェア割込み処理の入り口のようだ。

 システムコール命令を発行するとCPUに例外が発生する。これはソフトウェア割込みであり、CPUはLinuxカーネルの割込み処理の実行に移る。そしてハンドラとして登録されているsystem_callが呼ばれるようだ。

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。