調査対象版数
linux-4.2(mainline)
どこから始めるか
カーネルソースを読むための切り口として、システムコール呼び出し時のカーネルのハンドリング処理を調べる。(前の記事(システムコールエントリについて見てみた。(linuxソース解析))の継続)
何を切り口に調べれば良いか?
取り敢えず、前の記事で書いたシステムコールのエントリテーブル(sys_call_table)をキーにソース検索。当然だがアセンブラのソース等があり、切り分けしづらかったので、切り口になる情報をネットで検索。
(切り口として)下記のサイトを参考に調べることにした。
その他、次のサイトも参考。
以下、記事の中で、ソースの検索に、"search"とコマンドを打つところがあるが、がこれは、.bashrc
に作った関数。
定義は、次の通り。
function search() {
find . \( -name \*.c -o -name \*.h -o -name \*.S \) -exec grep -n $1 {} /dev/null \;
}
システムコールハンドリング関数の登録処理
まず最初に調べたのが、システムコールのハンドリング関数を登録する処理で、参考サイト
「Assembly Programming Linux (system call)」に、/usr/src/linux/arch/i386/kernel/traps.cのソースに、
void __init trap_init(void)
set_system_gate(SYSCALL_VECTOR,&system_call);
のようなコードがあるとのことなので、調査ソースを探してみた。が、サイトの記事のlinuxの版数( linux-2.2.16)が古いので
arch/i386のようなディレクトリ自体がない。
"int 0x80"の割り込みハンドリングの登録している処理はあるはずなので、SYSCALL_VECTORと言うキーワードで検索。
kou77@ubuntu:~/linux-4.2$ search SYSCALL_VECTOR
./arch/x86/include/asm/irq_vectors.h:49:#define IA32_SYSCALL_VECTOR 0x80
./arch/x86/kernel/traps.c:896: set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_compat);
./arch/x86/kernel/traps.c:897: set_bit(IA32_SYSCALL_VECTOR, used_vectors);
./arch/x86/kernel/traps.c:901: set_system_trap_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
./arch/x86/kernel/traps.c:902: set_bit(IA32_SYSCALL_VECTOR, used_vectors);
./arch/x86/kernel/irqinit.c:186: /* IA32_SYSCALL_VECTOR could be used in trap_init already. */
./arch/x86/lguest/boot.c:93: .syscall_vec = IA32_SYSCALL_VECTOR,
./arch/x86/lguest/boot.c:869: if (i != IA32_SYSCALL_VECTOR)
./arch/m32r/include/asm/syscall.h:5:#define SYSCALL_VECTOR "2"
./arch/m32r/include/asm/syscall.h:6:#define SYSCALL_VECTOR_ADDRESS "0xa0"
./drivers/lguest/interrupts_and_traps.c:23:static unsigned int syscall_vector = IA32_SYSCALL_VECTOR;
./drivers/lguest/interrupts_and_traps.c:336: /* Normal Linux IA32_SYSCALL_VECTOR or reserved vector? */
./drivers/lguest/interrupts_and_traps.c:337: return num == IA32_SYSCALL_VECTOR || num == syscall_vector;
./drivers/lguest/interrupts_and_traps.c:354: if (syscall_vector != IA32_SYSCALL_VECTOR) {
./drivers/lguest/interrupts_and_traps.c:369: if (syscall_vector != IA32_SYSCALL_VECTOR)
こちらも名称が少し変わっていた。IA32_SYSCALL_VECTOR。
上記検索結果の、./arch/x86/kernel/traps.cの中を確認。
trap_init関数が直ぐに見つかった。IA32_SYSCALL_VECTORで見ると、目的のシステムコールのハンドリング処理を登録している箇所らしきところがあった。
void __init trap_init(void)
{
int i;
#ifdef CONFIG_EISA
void __iomem *p = early_ioremap(0x0FFFD9, 4);
if (readl(p) == 'E' + ('I'<<8) + ('S'<<16) + ('A'<<24))
EISA_bus = 1;
early_iounmap(p, 4);
#endif
set_intr_gate(X86_TRAP_DE, divide_error);
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);
/* int4 can be called from all */
set_system_intr_gate(X86_TRAP_OF, &overflow);
set_intr_gate(X86_TRAP_BR, bounds);
set_intr_gate(X86_TRAP_UD, invalid_op);
set_intr_gate(X86_TRAP_NM, device_not_available);
#ifdef CONFIG_X86_32
set_task_gate(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS);
#else
set_intr_gate_ist(X86_TRAP_DF, &double_fault, DOUBLEFAULT_STACK);
#endif
set_intr_gate(X86_TRAP_OLD_MF, coprocessor_segment_overrun);
set_intr_gate(X86_TRAP_TS, invalid_TSS);
set_intr_gate(X86_TRAP_NP, segment_not_present);
set_intr_gate(X86_TRAP_SS, stack_segment);
set_intr_gate(X86_TRAP_GP, general_protection);
set_intr_gate(X86_TRAP_SPURIOUS, spurious_interrupt_bug);
set_intr_gate(X86_TRAP_MF, coprocessor_error);
set_intr_gate(X86_TRAP_AC, alignment_check);
#ifdef CONFIG_X86_MCE
set_intr_gate_ist(X86_TRAP_MC, &machine_check, MCE_STACK);
#endif
set_intr_gate(X86_TRAP_XF, simd_coprocessor_error);
/* Reserve all the builtin and the syscall vector: */
for (i = 0; i < FIRST_EXTERNAL_VECTOR; i++)
set_bit(i, used_vectors);
#ifdef CONFIG_IA32_EMULATION
set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_compat);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
#ifdef CONFIG_X86_32
set_system_trap_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
set_bit(IA32_SYSCALL_VECTOR, used_vectors);
#endif
//以下略・・・
entry_INT80_compatの内容は確認していないが、entry_INT80_32の方に当たりを付けて、周辺のソースを確認。
kou77@ubuntu:~/linux-4.2/arch/x86$ search entry_INT80_32
./include/asm/proto.h:12:void entry_INT80_32(void);
./kernel/traps.c:901: set_system_trap_gate(IA32_SYSCALL_VECTOR, entry_INT80_32);
./entry/entry_32.S:408:ENTRY(entry_INT80_32)
./entry/entry_32.S:505:ENDPROC(entry_INT80_32)
./entry/entry_32.Sに次のコードがあった。
ENDPROC(entry_SYSENTER_32)
# system call handler stub
ENTRY(entry_INT80_32)
ASM_CLAC
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation / emulation
testl $_TIF_WORK_SYSCALL_ENTRY, TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(NR_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(, %eax, 4)
syscall_after_call:
#以下略・・・
上記のハンドリング処理のコードを理解すると、システムコール呼び出し時にユーザが指定した引数をどのように受け取るかが分かると思う。
余りノウハウがないアセンブラのコードを読む前に、もう少しネットに割り込み関連の情報がないか探してみた。
で見つけたサイトが次。
次は、上記サイトからの引用。
この記事もカーネルの版数は古いようだけど、とても分かり易く書いてある。
2.11 システムコールがi386ではどのように実装されているか?
lcall7/lcall27 コールゲート
int 0x80 ソフトウエア割り込み
他のUNIX系OS(Solaris, Unixware 7 など)のバイナリではlcall7メカニズムを使っていますが、ネイティブな Linux プログラムは int 0x80 を使っています。'lcall7'の名前は、歴史的な過ちです。これは、lcall27(例えば Solaris/x86)も使用しているが、ハンドラ関数はlcall7_funcと呼ばれているからです。
システムがブートするとき、IDTを設定する関数 arch/i386/kernel/traps.c:trap_init() が呼ばれ、(type 15, dpl 3 の)ベクタ 0x80 を、arch/i386/kernel/entry.Sのシステムコールエントリアドレスを示すように設定します。
ユーザ空間のアプリケーションがシステムコールを行う時は、引数をレジスタに入れて、'int 0x80' 命令を実行します。これは、カーネルモードにトラップされ、プロセッサはentry.Sのシステムコールエントリポイントへとジャンプします。これは、次のようなことを行います。
1. レジスタを保存する。
2. %ds と %es を KERNEL_DS に設定する。そのため、参照する全てのデータ(と外部のセグメント)はカーネルアドレス空間になる。
3. もし %eax の値がNR_syscalls(現在 256) より大きかった場合には、ENOSYS エラーで失敗する。
4. もしタスクがptraced されていた(tsk->ptrace & PF_TRADESYS )時は、特別な処理を行う。これは strace (SVR4でいうtruss(1))のようなプログラムやデバッガをサポートするためである。
5. sys_call_table+4*(%eax の syscall_number)を呼ぶ。このテーブルは同じファイル(arch/i386/kernel/entry.S)で初期化されており、各々のシステムコールハンドラを指している。Linux においては、ハンドラには通常、例えば sys_openやsys_exitのように sys_というプレフィックスがついている。これらの C システムコールハンドラはSAVE_ALLが格納したスタックから引数を見つけ出す。
6. ``system call return path''に入る。int 0x80 だけでなく、lcall7, lcall27でも使うため、別のラベルになっている。これは、(ボトムハーフの)タスクレットの取り扱いや、schedule()が必要かどうか(tsk->need_resched != 0) を確認したり、シグナルが保留されていないか確認したり、そしてそれらのシグナルを処理したりすることに関連している。
上記記事に「ユーザ空間のアプリケーションがシステムコールを行う時は、引数をレジスタに入れて、'int 0x80' 命令を実行します。」のようにある。
「レジスタを保存する」の処理は、アセンブラのentry_SYSENTER_32のコードのSAVE_ALLで行われるようだ。
この記事の頭の方で書いた参考サイトAssembly Programming Linux (system call)のページの後ろの方に次の記述があった。
/usr/src/linux/arch/i386/kernel/entry.S<br />
83 #define SAVE_ALL \<br />
84 cld; \<br />
85 pushl %es; \<br />
86 pushl %ds; \<br />
87 pushl %eax; \<br />
88 pushl %ebp; \<br />
89 pushl %edi; \<br />
90 pushl %esi; \<br />
91 pushl %edx; \<br />
92 pushl %ecx; \<br />
93 pushl %ebx; \<br />
94 movl $(__KERNEL_DS),%edx; \<br />
95 movl %dx,%ds; \<br />
96 movl %dx,%es;
で、上記SAVE_ALLの内容とentry_INT80_32の内容を分かり易くなるようマージしたものとして、次が書かれている。
分かり難いので system_call を単純化して以下に示します. アセンブリでレジスタに設定した引数はスタックに積まれるため, システムコールとして呼び出される C で記述された sys_XXXX 関数の 引数となります.
ENTRY(system_call)
pushl %eax # save orig_eax
cld;
pushl %es;
pushl %ds;
pushl %eax;
pushl %ebp;
pushl %edi; # 第 5 引数
pushl %esi; # 第 4 引数
pushl %edx; # 第 3 引数
pushl %ecx; # 第 2 引数
pushl %ebx; # 第 1 引数
movl $(__KERNEL_DS),%edx;
movl %dx,%ds;
movl %dx,%es;
movl %esp, %ebx;
andl $-8192, %ebx;
cmpl $(NR_syscalls),%eax
jae badsys
testb $0x20,flags(%ebx) # PF_TRACESYS
jne tracesys
call *SYMBOL_NAME(sys_call_table)(,%eax,4)
# eax のシステムコールに対応する
# 関数を呼ぶ
movl %eax,EAX(%esp) # スタック上のeaxに返り値設定
popl %ebx;
popl %ecx;
popl %edx;
popl %esi;
popl %edi;
popl %ebp;
popl %eax; # 返り値設定済み
popl %ds;
popl %es;
addl $4,%esp; # 最初の pushl %eax の分を捨てる
iret; # システムコールから戻る
自分が調査しているソースのSAVE_ALLのコードは次の通り。
(arch/x86/entry/entry_32.Sからの抜粋)
.macro SAVE_ALL
cld
PUSH_GS
pushl %fs
pushl %es
pushl %ds
pushl %eax
pushl %ebp
pushl %edi
pushl %esi
pushl %edx
pushl %ecx
pushl %ebx
movl $(__USER_DS), %edx
movl %edx, %ds
movl %edx, %es
movl $(__KERNEL_PERCPU), %edx
movl %edx, %fs
SET_KERNEL_GS %edx
.endm
上で説明されているシステムコールの引数として指定されているレジスタのスタックへの退避は同じようだ。
ところで、前の記事で書いたように、システムコールのエントリの定義って、
asmlinkage long sys_fork(void)
な感じで、asmlinkageの定義は、
./arch/x86/include/asm/linkage.h:10:#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
だと思っていた。
regparm(0)
はレジスタ渡しかと思ったけど、上の説明を見ると、普通の関数呼び出しと同様にスタックで渡しているようだ。
regparm(0)
の"0"があるから、レジスタでは渡さないと言うことになるのか?
(前の記事でも書いたが、regparmについては別途確認)
システムコールの割り込みが起こって、ハンドリングするところの処理については、大体分かった感じ。
この後は、個々のシステムコールとスケジューラ、メモリー管理辺りを調べていく。(プロセス管理、スケジューラが先で、メモリー管理はそのあとで)