人生は勉強ブログ

https://github.com/dooooooooinggggg

Linux(2.6.11)において,int 0x80命令によってシステムコールが呼ばれる流れを追ってみる

Linux(2.6.11)において,int 0x80命令によってシステムコールが呼ばれる流れを追ってみた

この記事では,以下の本を参考に,実際にコードを見ていく記事.

参考文献: Linuxカーネル2.6解読室

Linuxカーネル2.6解読室

Linuxカーネル2.6解読室

そもそもシステムコールとは

ウィキペディアにはこのように書いてある.

オペレーティングシステム (OS)(より明確に言えばOSのカーネル)の機能を呼び出すために使用される機構のこと。 実際のプログラミングにおいては、OSの機能は関数 (API) 呼び出しによって実現されるので、OSの備える関数 (API) のことを指すこともある。

例えば,プログラムから見て,外界に繋がる操作をする際は,システムコールを使用する. システムコールでは,デバイスを扱う処理などをOSが引き受けることで,より安全に,確実に処理を行えるようになる利点がある.

Linux 2.6におけるシステムコールの機構

i386 / x86_64 アーキテクチャでは,システムコールを実行する方法として,4つの方法を提供している.そのうち,Linuxで使用しているのは,以下のチェックが入った二つの方法を使用している.

  • [x] ソフトウェア割り込み(int n命令)
  • [ ] コールゲート
  • [ ] タスクゲート
  • [x] sysenter命令を使用

システムコールは,基本的にはglibcライブラリが,定められた手順によって実際に発行されるため,プログラマはそのAPIがライブラリルーチンなのか,システムコールなのかを知る必要はない. 例えばopenシステムコールでは,一旦は,glibcのライブラリを呼び出し,その中の処理でシステムコールを発行している.

システムコールはそれぞれに番号が振られているが,カーネル内では,この番号に応じた関数が定義されている.内部的にシステムコールの実態のなる関数は配列に定義されており, 前述のシステムコールの番号というのは,この配列のインデックスとなっている.

Linuxカーネル2.6/i386アーキテクチャでは,約270のシステムコールが定義されている.

/include/asm-i386/unistd.h

#ifndef _ASM_I386_UNISTD_H_
#define _ASM_I386_UNISTD_H_

/*
 * This file contains the system call numbers.
 */

#define __NR_restart_syscall      0
#define __NR_exit        1
#define __NR_fork        2
#define __NR_read        3
#define __NR_write       4
#define __NR_open        5
#define __NR_close       6
#define __NR_waitpid         7
// 省略....
#define __NR_sys_kexec_load    283
#define __NR_waitid        284
/* #define __NR_sys_setaltroot 285 */
#define __NR_add_key       286
#define __NR_request_key   287
#define __NR_keyctl        288

ちなみに,Linux2.6時点でのx86_64のシステムコールここにある.

C言語においては,システムコールの呼び出しとの変換は,glibcが行なっており,呼び出し方法としては,int 0x80/iret命令か,sysente/sysexit命令が用いられる.

システムコールを呼び出す際は,特定のレジスタにそれぞれ対応した値を格納したのちに,int 0x80命令なりsysenter命令を実行することで,処理が行われる.

int 0x80を使ったシステムコール

32bit(i386)の場合

  • eaxに呼び出すシステムコールの番号
  • ebxに0番目の引数
  • ecxに1番目の引数
  • 以降,edx,esi,edi,ebpと続いていく.

64bit(x86_64)の場合

  • EAX:呼び出すシステムコールの番号
  • RDI:0番目の引数
  • RSI:1番目の引数
  • 以降,RDX,R10,R8,R9と続いていく

例えば,x86_64 Linuxwriteシステムコールを使いたい場合は,

mov    eax,0x1 # 1番のシステムコールを呼びたい.
mov    edi,0x1 # 0番目の引数
movabs rsi,0x601001 # 1番目の引数
mov    edx,0x1 # 2番目の引数
syscall # まだ出てきていないが,x86_64においてシステムコールを呼び出すもの.

のようにすると呼び出すことができる.

では実際に処理を見ていく.

通常のC言語を用いたプログラミング(インラインアセンブラなどを用いない)では,先ほども書いたように,glibcが実際のシステムコールの呼び出しを行う. ライブラリが,レジスタに引数を設定したのち,int 0x80命令を実行する.すると,CPUは特権モードに移り,system_callから実行が始まる.

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L241

ENTRY(system_call)                                                  # 1
    pushl %eax            # save orig_eax                             # 2
    SAVE_ALL                                                        # 3
    GET_THREAD_INFO(%ebp)                                           # 4
                    # system call tracing in operation
    testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
    jnz syscall_trace_entry
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys                                              # 6
syscall_call:
    call *sys_call_table(,%eax,4)                                   # 7
    movl %eax,EAX(%esp)     # store the return value
syscall_exit:
    cli              # make sure we don't miss an interrupt
                    # setting need_resched or sigpending
                    # between sampling and the iret
    movl TI_flags(%ebp), %ecx
    testw $_TIF_ALLWORK_MASK, %cx  # current->work
    jne syscall_exit_work
restore_all:
    RESTORE_ALL

    # perform work that needs to be done immediately before resumption
    ALIGN

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L84

#define SAVE_ALL \
    cld; \
    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;
    cmpl $(nr_syscalls), %eax
    jae syscall_badsys
  • 7: システムコール関数を実際に呼び出しているところ.
    • ここで先ほど指定されていた番号のインデックスを辿り,関数を実行している.

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L575

ENTRY(sys_call_table)
    .long sys_restart_syscall  /* 0 - old "setup()" system call, used for restarting */
    .long sys_exit
    .long sys_fork
    .long sys_read
    .long sys_write
    .long sys_open     /* 5 */
# 省略
    .long sys_ni_syscall       /* reserved for kexec */
    .long sys_waitid
    .long sys_ni_syscall       /* 285 */ /* available */
    .long sys_add_key
    .long sys_request_key
    .long sys_keyctl

上で例にあげたwriteシステムコールの実装を探してみると.

https://elixir.bootlin.com/linux/v2.6.11/source/arch/i386/kernel/entry.S#L580

https://elixir.bootlin.com/linux/v2.6.11/source/fs/read_write.c#L330

システムコールは,カーネルの中に定義された関数であるということがわかった.

これ以降の処理は,ユーザーモードに戻る準備をしている.

sysenterを使ったシステムコール

sysenter, 32bit(i386)の場合

  • eaxに呼び出すシステムコールの番号
  • ebxに0番目の引数
  • ecxに1番目の引数
  • 以降,edx,esi,ediと続いていく.
  • ebp,5番目の引数へのポインタで,ユーザープロセスのスタックを指す.

最後だけ少し違う.

今回は省略.

次回以降

この記事では,i386(32bit)Linux 2.6.11のコードをみるに止まった.今後は,最新版Linuxx86_64の処理を追ってみたいと思う.

参考