付録 B. システムコールの仕組み


すでに説明した(4. Linux カーネルとシステムコール) ように Linux の システムコールはレジスタに引数を設定して int 0x80 によるソフトウェア 割り込みで呼び出します.

ここでは linux-2.2.16 のソースから実際にシステムコール呼び出しの 仕組みを次の3つの部分にわけて解説します.

  1. 割込みによるシステムコールをカーネルが初期化する部分
  2. システムコールが呼び出された場合に実行される部分
  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 をキーワードにしてカーネルのソースを追っていけば よいでしょう.