付録 B. システムコールの仕組み
すでに説明した(4. Linux カーネルとシステムコール) ように Linux の システムコールはレジスタに引数を設定して int 0x80 によるソフトウェア 割り込みで呼び出します.
ここでは linux-2.2.16 のソースから実際にシステムコール呼び出しの 仕組みを次の3つの部分にわけて解説します.
- 【注】
- ソースリスト中で行頭の数字は行番号を示しています.
カーネルのバージョンによって差がありますが目安にはなるでしょう.
1. 割込みによるシステムコールをカーネルが初期化する部分
最初にカーネルの起動部分で割り込みテーブルの設定をしています.
/usr/src/linux/init/main.c:
1350 asmlinkage void __init start_kernel(void)
1351 {
1352 char * command_line;
1353
1354 #ifdef __SMP__
1355 static int boot_cpu = 1;
1356 /* "current" has been set up, we need to load it now */
1357 if (!boot_cpu)
1358 initialize_secondary();
1359 boot_cpu = 0;
1360 #endif
1361
1362 /*
1363 * Interrupts are still disabled. Do necessary setups, then
1364 * enable them
1365 */
1366 printk(linux_banner);
1367 setup_arch(&command_line, &memory_start, &memory_end);
1368 memory_start = paging_init(memory_start,memory_end);
1369 trap_init();
1370 memory_start = init_IRQ( memory_start );
1371 sched_init();
1372 time_init();
1373 parse_options(command_line);
1369行目の trap_init() でシステムコールの入り口(int 80h) が設定されます. 実際に設定している部分は次のコードです.
/usr/src/linux/arch/i386/kernel/traps.c:
679 void __init trap_init(void)
:
702 set_system_gate(SYSCALL_VECTOR,&system_call);
SYSCALL_VECTOR は次のファイルで 0x80 に設定されています.
/usr/src/linux/arch/i386/kernel/irq.h
52 #define SYSCALL_VECTOR 0x80
8086 では割込みで実行されるコードのセグメント:オフセットを設定しますが, 80386以降の CPU の保護モードでは複雑な仕組みになっています.
/usr/src/linux/arch/i386/kernel/traps.c:
518 #define _set_gate(gate_addr,type,dpl,addr) \
519 do { \
520 int __d0, __d1; \
521 __asm__ __volatile__ ("movw %%dx,%%ax\n\t" \
522 "movw %4,%%dx\n\t" \
523 "movl %%eax,%0\n\t" \
524 "movl %%edx,%1" \
525 :"=m" (*((long *) (gate_addr))), \
526 "=m" (*(1+(long *) (gate_addr))), "=&a" (__d0), "=&d" (__d1) \
527 :"i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
528 "3" ((char *) (addr)),"2" (__KERNEL_CS << 16)); \
529 } while (0)
:
548 static void __init set_system_gate(unsigned int n, void *addr)
549 {
550 _set_gate(idt_table+n,15,3,addr);
551 }
_set_gate がどのように展開されるかよくわかりません.と書いていたら教えて頂きました(2003/09/14).
_set_gate の展開で UNIX MAGAZINE 2003.2 月号のLinuxのブートプロセスをみる
の P86下部に擬似コードが掲載されていたので紹介します。
_set_gate(gate_addr, type,dpl,addr) {
movl addr, %edx
movl $0x100000, %eax # __KERNEL_CS << 16
movw %dx, %ax
movw $0x8000+(dpl << 13) + (type << 8).%dx
movl %eax, (gate_addr)
movl %edx, (gate_addr+4)
}
とにかく set_system_gate は割込みテーブルの 0x80 番目のエントリに 386 トラップゲートとして特権レベル3(ユーザモード), カーネル用の セグメントセレクタと entry.S の system_call のアドレスを設定します.
これで int 80h のソフトウェア割り込みで system_call が実行されるように 設定されました.
また int 0x80 のソフトウェア割り込みで特権レベルが一時的にユーザモード からカーネルモードに移行します.
2. システムコールが呼び出された場合に実行される部分
カーネルの起動時に割込み番号 0x80 でシステムコールが呼び出されるように 設定されることを解説しましたが,ここでは実際にシステムコールで実行される コードを見てみます.
カーネルが初期化時に設定した system_call は entry.S の中にあります. システムコールを使う場合に個別のシステムコールを実装したCの関数に制御が移る前に 必ず通過する部分です.
gas ではオペランドの順序が NASM とは逆になっていることに注意して下さい.
/usr/src/linux/arch/i386/kernel/entry.S 171 ENTRY(system_call) 172 pushl %eax # save orig_eax 173 SAVE_ALL 174 GET_CURRENT(%ebx) 175 cmpl $(NR_syscalls),%eax 176 jae badsys 177 testb $0x20,flags(%ebx) # PF_TRACESYS 178 jne tracesys 179 call *SYMBOL_NAME(sys_call_table)(,%eax,4) 180 movl %eax,EAX(%esp) # save the return value 181 ALIGN 182 .globl ret_from_sys_call 183 .globl ret_from_intr 184 ret_from_sys_call: 185 movl SYMBOL_NAME(bh_mask),%eax 186 andl SYMBOL_NAME(bh_active),%eax 187 jne handle_bottom_half 188 ret_with_reschedule: 189 cmpl $0,need_resched(%ebx) 190 jne reschedule 191 cmpl $0,sigpending(%ebx) 192 jne signal_return 193 restore_all: 194 RESTORE_ALL
179行目の call *SYMBOL_NAME(sys_call_table)(,%eax,4) で eax に指定された システムコール番号の処理を呼び出します.
SAVE_ALL, RESTORE_ALL といったマクロの定義を示します.
/usr/src/linux/arch/i386/kernel/entry.S
83 #define SAVE_ALL \
84 cld; \
85 pushl %es; \
86 pushl %ds; \
87 pushl %eax; \
88 pushl %ebp; \
89 pushl %edi; \
90 pushl %esi; \
91 pushl %edx; \
92 pushl %ecx; \
93 pushl %ebx; \
94 movl $(__KERNEL_DS),%edx; \
95 movl %dx,%ds; \
96 movl %dx,%es;
97
98 #define RESTORE_ALL \
99 popl %ebx; \
100 popl %ecx; \
101 popl %edx; \
102 popl %esi; \
103 popl %edi; \
104 popl %ebp; \
105 popl %eax; \
106 1: popl %ds; \
107 2: popl %es; \
108 addl $4,%esp; \
109 3: iret; \
110 .section .fixup,"ax"; \
111 4: movl $0,(%esp); \
112 jmp 1b; \
113 5: movl $0,(%esp); \
114 jmp 2b; \
115 6: pushl %ss; \
116 popl %ds; \
117 pushl %ss; \
118 popl %es; \
119 pushl $11; \
120 call do_exit; \
121 .previous; \
122 .section __ex_table,"a";\
123 .align 4; \
124 .long 1b,4b; \
125 .long 2b,5b; \
126 .long 3b,6b; \
127 .previous
128
129 #define GET_CURRENT(reg) \
130 movl %esp, reg; \
131 andl $-8192, reg;
132
分かり難いので 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; # システムコールから戻る
eax のシステムコール番号と実際にコールされる sys_XXXX 関数との 対応表(ジャンプテーブル)は /usr/src/linux/arch/i386/kernel/entry.S の以下の部分にあります.
375 ENTRY(sys_call_table)
376 .long SYMBOL_NAME(sys_ni_syscall) /* 0 - old "setup()" system call*/
377 .long SYMBOL_NAME(sys_exit)
:
566 .long SYMBOL_NAME(sys_vfork) /* 190 */
システムコール番号とsys_XXXX 関数との対応のカーネルの各バージョン(2.0, 2.2, 2.4)での差は 付録 A.システムコールとカーネルの関数 を参照してください.
3. システムコールの実装
実際のシステムコールで呼び出される関数名は entry.S の ENTRY(sys_call_table) に書かれています. 例えば exit システムコールは単に do_exit を呼び出しています. このように sys_xxx が do_xxx を呼び出しているシステムコールは多くあります.
/usr/src/linux/kernel/exit.c
429 asmlinkage int sys_exit(int error_code)
430 {
431 do_exit((error_code&0xff)<<8);
432 }
さて asmlinkage とはなんでしょうか?
定義しているのは /usr/src/linux/include/linux/linkage.h の 以下の部分です.
4 #ifdef __cplusplus
5 #define CPP_ASMLINKAGE extern "C"
6 #else
7 #define CPP_ASMLINKAGE
8 #endif
9
10 #if defined __i386__ && (__GNUC__ > 2 || __GNUC__ == 2 &&__GNUC_MINOR__ > 7)
11 #define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
12 #else
13 #define asmlinkage CPP_ASMLINKAGE
14 #endif
つまり,asmlinkage は __attribute__((regparm(0))) に展開されます.
gcc-info では:
In GNU C, you declare certain things about functions called in your
program which help the compiler optimize function calls and check your
code more carefully.
The keyword `__attribute__' allows you to specify special attributes
when making a declaration.
: 略
`regparm (NUMBER)'
On the Intel 386, the `regparm' attribute causes the compiler to
pass up to NUMBER integer arguments in registers EAX, EDX, and ECX
instead of on the stack. Functions that take a variable number of
arguments will continue to be passed all of their arguments on the
stack.
結局 regparam(0) は レジスタ経由で渡す引数の数は 0 と言うことになり, asmlinkage は 「引数が確実にスタックで渡されるように gcc に指示」 しているだけです.
したがって entry.S の system_call はレジスタに設定された引数を スタックに積み,個別のシステムコールを実装した C の関数 (sys_xxx) はすべての引数をスタックから受け取ります.
システムコールの 「レジスタに設定して int 0x80 を実行する仕組み」 をカーネルソースをもとに見てきました.あとは個別のシステムコール を asmlinkage をキーワードにしてカーネルのソースを追っていけば よいでしょう.