LoginSignup
33
26

More than 5 years have passed since last update.

システムコールめもめも

Last updated at Posted at 2016-01-02

概要

  • システムコールをいろいろ調べてみたので、自分の言葉で用語をまとめてみた。
  • 修正しました(2017/4/25)

システムコール

種類

  • i386
    • AMD
      • INT $0x80/iret
      • syscall/sysret(古いと使えないものもある)
    • Intel
      • INT $0x80/iret
      • sysenter/sysexit(古いと使えないものもある)
  • x86_64
    • AMD
      • INT $0x80/iret
      • syscall/sysret
    • Intel
      • INT $0x80/iret
      • sysenter/sysexit
      • syscall/sysret

特徴

  • INT $0x80/iret
    • ソフトウェア割り込み発生させる。システムコールに特化していないのでsysenter、syscallよりオーバーヘッドがある
    • 昔からある方法なのでほとんどのCPUは対応している??
  • sysenter/sysexitとsyscall/sysret
    • システムコール専用に特化していてint命令より負荷は軽い。

x86_64の場合、カーネルはsyscall命令を使うだけでよい。i386の場合、カーネルはCPUの種別によってINT $0x80/iretsysenter/sysexitsyscall/sysretを選択しなければいけない

システムコール呼び出し詳細

i386

  • ユーザプログラム → glibc(__kernel_vsyscall) → VDSO(vsyscallの番地にCPU種別にあったシステムコール命令が配置されている) → カーネル
  • ユーザプログラム → glibc → VDSO(gettimeofday、time、getcpu、clock_gettime)

ユーザプログラムは__kernel_vsyscallという関数を使うようになっています。

VDSOの初期化中にvsyscallの中身を設定しているので、vsyscallもVDSOの仕組みの一つとして解釈しています。すみません、よくわかっていません。。以降もその体で説明しています。

vsyscallは固定アドレスに配置してあり、vdsoは毎回ランダムな番地に配置されるようです。

# cat /proc/2227/maps | tail
7f3f48570000-7f3f4859e000 rw-p 003a1000 fd:00 2361898                    /usr/libexec/qemu-kvm
7f3f4859e000-7f3f48985000 rw-p 00000000 00:00 0 
7f3f49dac000-7f3f4da0f000 rw-p 00000000 00:00 0 
7f3f4da0f000-7f3f4da1f000 rw-p 00000000 00:00 0 
7f3f4da1f000-7f3f4da39000 rw-p 00000000 00:00 0 
7f3f4da39000-7f3f4da49000 rw-p 00000000 00:00 0 
7f3f4da49000-7f3f5e3d4000 rw-p 00000000 00:00 0 
7fffd139f000-7fffd13b4000 rw-p 00000000 00:00 0                          [stack]
7fffd13c0000-7fffd13c1000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

結局、__kernel_vsyscallは、プログラムがユーザ空間でカーネルのコードを実行できるようにするために存在している仮想動的共有オブジェクト(vDSO)と呼ばれるものの一部なのです。

__kernel_vsyscallは、の一部。

リンカスクリプト。

arch/x86/vdso/vdso32/vdso32.lds.S
/*
 * Linker script for 32-bit vDSO.
 * We #include the file to define the layout details.
 * Here we only choose the prelinked virtual address.
 *
 * This file defines the version script giving the user-exported symbols in
 * the DSO.  We can define local symbols here called VDSO* to make their
 * values visible using the asm-x86/vdso.h macros from the kernel proper.
 */

#define VDSO_PRELINK 0
#include "../vdso-layout.lds.S"

/* The ELF entry point can be used to set the AT_SYSINFO value.  */
ENTRY(__kernel_vsyscall);

/*
 * This controls what userland symbols we export from the vDSO.
 */
VERSION
{
        LINUX_2.5 {
        global:
                __kernel_vsyscall;
                __kernel_sigreturn;
                __kernel_rt_sigreturn;
        local: *;
        };
}

/*
 * Symbols we define here called VDSO* get their values into vdso32-syms.h.
 */
VDSO32_PRELINK          = VDSO_PRELINK;
VDSO32_vsyscall         = __kernel_vsyscall;
VDSO32_sigreturn        = __kernel_sigreturn;
VDSO32_rt_sigreturn     = __kernel_rt_sigreturn;

glibcは__kernel_vsyscallの番地を知る必要がある。x86_64と同様にユーザースタックから取ってきている様子。glibcはその番地を取得してcallしている。

x86_64

  • ユーザプログラム → glibc(直接SYSCALL命令) → カーネル
  • ユーザプログラム → glibc → VDSO(gettimeofday、time、getcpu、clock_gettime)

vdso(ELF)のリンカスクリプト。やはりgettimeofday、time、getcpu、clock_gettimeがvdso内に配置しようとすることが確認できる。

arch/x86/vdso/vdso.lds.S
/*
 * Linker script for 64-bit vDSO.
 * We #include the file to define the layout details.
 * Here we only choose the prelinked virtual address.
 *
 * This file defines the version script giving the user-exported symbols in
 * the DSO.  We can define local symbols here called VDSO* to make their
 * values visible using the asm-x86/vdso.h macros from the kernel proper.
 */

#define VDSO_PRELINK 0xffffffffff700000
#include "vdso-layout.lds.S"

/*
 * This controls what userland symbols we export from the vDSO.
 */
VERSION {
        LINUX_2.6 {
        global:
                clock_gettime;
                __vdso_clock_gettime;
                gettimeofday;
                __vdso_gettimeofday;
                getcpu;
                __vdso_getcpu;
                time;
                __vdso_time;
        local: *;
        };
}

VDSO64_PRELINK = VDSO_PRELINK;

カーネルがvdsoの配置を決めていて、アプリケーション側はその上記システムコールのシンボルの番地を知る必要がある。カーネルがプログラムを実行する際に、そのプロセスのユーザースタック上のインタープリタテーブルのAT_SYSINFO_EHDRcurrent->mm->context.vdsoの値を代入するので、glibc側でこれを調べるようになっている。

AT_SYSINFO_EHDRの確認


$ gdb ./hello -q
Reading symbols from /root/hello...done.
(gdb) b main
Breakpoint 1 at 0x4004d3: file hello.c, line 5.
(gdb) run
Starting program: /root/hello 

Breakpoint 1, main (argc=1, args=0x7fffffffe658) at hello.c:5
5           printf("Hello, world!\n");
(gdb) info auxv
33   AT_SYSINFO_EHDR      System-supplied DSO's ELF header 0x7ffff7ffa000
16   AT_HWCAP             Machine-dependent CPU capability hints 0xbfebfbff
6    AT_PAGESZ            System page size               4096
17   AT_CLKTCK            Frequency of times()           100
3    AT_PHDR              Program headers for program    0x400040
4    AT_PHENT             Size of program header entry   56
5    AT_PHNUM             Number of program headers      8
7    AT_BASE              Base address of interpreter    0x7ffff7ddc000
8    AT_FLAGS             Flags                          0x0
9    AT_ENTRY             Entry point of program         0x4003e0
11   AT_UID               Real user ID                   0
12   AT_EUID              Effective user ID              0
13   AT_GID               Real group ID                  0
14   AT_EGID              Effective group ID             0
23   AT_SECURE            Boolean, was exec setuid-like? 0
25   AT_RANDOM            Address of 16 random bytes     0x7fffffffe879
31   AT_EXECFN            File name of executable        0x7fffffffefec "/root/hello"
15   AT_PLATFORM          String identifying platform    0x7fffffffe889 "x86_64"
0    AT_NULL              End of vector                  0x0

_dl_vdso_vsym で調べている。この条件演算子はGNU拡張のようです

sysdeps/unix/sysv/linux/x86_64/gettimeofday.cの抜粋
void *gettimeofday_ifunc (void) __asm__ ("__gettimeofday");

void *
gettimeofday_ifunc (void)
{
  PREPARE_VERSION (linux26, "LINUX_2.6", 61765110);

  /* If the vDSO is not available we fall back on the old vsyscall.  */
  return (_dl_vdso_vsym ("gettimeofday", &linux26)
          ?: (void *) VSYSCALL_ADDR_vgettimeofday);
}
__asm (".type __gettimeofday, %gnu_indirect_function");

Virtual Dynamic Shared Object(VDSO)とは

  • ユーザープロセスのメモリ空間の一部にカーネルのメモリを読み取り専用でマップしている
  • ここにgettimeofday、time、getcpu、clock_gettimeを実現するコードが読み込み専用でマップされている、ここを利用することでシステムコールがユーザーモード内だけで完結して実行速度が向上。
  • カーネルモードに切り替えることなくアクセスできるようにするしくみ。
  • 通常の共有ライブラリと同じELF形式。よって、呼び方は共有ライブラリと同じ。関数を普通に呼び出し、動的リンカがアドレスを見つけ出す。
  • VDSOを初期化する中のsysenter_setup()でvsyscallの設定もしている
  • vsyscallもvdsoの枠組の一つの仕組みで、固定の番地に配置される

VDSO(i386の場合)

全てのシステムコールがVDSO or vsyscallを利用する

VDSO(x86_64の場合)

  • gettimeofday、time、getcpu、clock_gettimeだけが利用する。
  • それ以外のシステムコールはglibcからsyscall命令が実行される

VirtualSystemCall(vsyscall)

vsyscall(i386の場合)

Linuxカーネル2.6解読室

カーネルが定義した実行コードをユーザー空間から参照できるようにして、ライブラリから実行させるものです。システムコールを発行するために、ライブラリはカーネルから指定されたアドレスを呼び出し、カーネルが定義したコードを実行します。

  • ユーザー空間にあるCPU種別によって適切なシステムコールが配置された番地のシンボルみたいなもののはず
  • そのシンボル名は32ビットLinuxでは__kernel_vsyscallで、64ビットLinuxではvsyscallがあるようだ
  • int \$0x80sysenter/syscallはVDSO(共有ライブラリ)の枠組みの中にあり、(VDSOを初期化する中のsysenter_setup()でvsyscallの設定もしている)__kernel_vsyscallというシンボルの番地に配置されている。
  • glibcからは__kernelvsyscallを呼ぶだけでその環境にあったint $0x80sysenter/syscallが呼ばれると思われる。透過的に扱えるのでVirtualSystemCallと呼ばれている。

vsyscall(x86_64の場合)

環境によるシステムコールの差異を考慮しなくてよいのでglibcから直接システムコール命令を使う。vsyscall領域は使用されなくなったが、32bitアプリケーションなどのためにまだ残っている様子。

glibcからの呼び出し

動的にリンクする場合は__vdso_gettimeofday。静的にリンクする場合はVSYSCALL_ADDR_vgettimeofday。64bitの場合でも静的な場合はvsyscallの領域を使用するようでした。

glibc-2.12-2-gc4ccff1/sysdeps/unix/sysv/linux/x86_64/gettimeofday.S
/* For the calculation see asm/vsyscall.h.  */
#define VSYSCALL_ADDR_vgettimeofday     0xffffffffff600000


ENTRY (__gettimeofday)
        /* Align stack.  */
        sub     $0x8, %rsp
        cfi_adjust_cfa_offset(8)
#ifdef SHARED
        movq    __vdso_gettimeofday(%rip), %rax
        PTR_DEMANGLE (%rax)
#else
        movq    $VSYSCALL_ADDR_vgettimeofday, %rax
#endif
        callq   *%rax
        /* Check error return.  */
        cmpl    $-4095, %eax
        jae     SYSCALL_ERROR_LABEL

その他メモ

GNU C Library(glibc)

  • GNUプロジェクトによる標準Cライブラリ実装。ベル研が作ったlibcと互換がある。
  • カーネルのシステムコールを呼び出す処理が実際に書かれている。
  • ユーザープログラムではここで定義されている関数を呼ぶだけ。write()、read()などなど。

Application Binary Interface (ABI)

  • システムコールの呼び出しかた(カーネルとCPUからみた視点)
  • OSとCPUの種類分ABIはある
  • Linux/x86のABI
    • 引数にレジスタ、int $0x80命令を使うなど。

Application Programming Interface(API)

  • システムコールを呼び出しかた(ユーザからみた視点)
  • 例)ssize_t write(int fd , const void * buf , size_t count );
  • POSIX準拠だとAPIが同じ。OSが異なっていてもPOSIX準拠であればABIが異なった場合でもそのユーザープログラムはコンパイルし直せば実行できる。

具体的なシステムコールの呼び出しかた

  • INT 0x80命令の場合
    • CPUに対して例外(ソフトウェア割り込み)を発生させてCPUがIDTRレジスタからカーネルのIDTをみにいってそこに登録されているカーネル処理(ハンドラ)に飛びその処理を実行すること。
    • 処理の最初に現在のレジスタをスタックに入れる。最後にスタックからその値をレジスタに戻す。
    • スタックの使い方
  • sysenterの場合

sysenter_setup()

カーネル起動時に、CPUを判別してVDSO内にCPUに適したシステムコールを最初から埋め込んでいる。
埋め込んでいる箇所

http://lxr.free-electrons.com/source/arch/x86/vdso/vdso32-setup.c?v=3.13#L277

システムコール x86_64

通常のシステムコール

  • glibc
    • レジスタにシステムコール番号
    • syscall命令

  • system_call (入り口) (arch/x86/kernel/entry_64.S)

  • システムコールテーブル (/arch/x86/syscalls/syscall_64.tbl)
    • /arch/x86/kernel/syscall_64.cで配列に各システムコールの関数ポインタを設定。

  • システムコールテーブルの各関数(sys_システムコール名)に飛ぶ。

カーネルでのシステムコール定義

  • SYSCALL_DEFINE0~6マクロで定義されている
  • fork()はなかった

システムコールを実装している関数だからと言って、これを必ず使ってるわけでも無いみたいです。sys_fork()とか・・

  • 例えば、3.3.4にはなかった。4.3にはSYSCALL_DEFINEでforkが定義されていた。なんでだろ。。

カーネル内でシステムコールの定義箇所を探すには

  • SYSCALLとシステムコール名でgrep すると良い。
  • だいたいSYSCALL_DEFINE0~6マクロで検索ヒットするっぽい。

参考

http://softwaretechnique.jp/Linux/SystemCall/sc_01.html
Software Design 2014年8月号

33
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
26