付録 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 をキーワードにしてカーネルのソースを追っていけば よいでしょう.