概要
- システムコールをいろいろ調べてみたので、自分の言葉で用語をまとめてみた。
- 修正しました(2017/4/25)
システムコール
種類
- i386
- AMD
- INT $0x80/iret
- syscall/sysret(古いと使えないものもある)
- Intel
- INT $0x80/iret
- sysenter/sysexit(古いと使えないものもある)
- AMD
- x86_64
- AMD
- INT $0x80/iret
- syscall/sysret
- Intel
- INT $0x80/iret
- sysenter/sysexit
- syscall/sysret
- AMD
特徴
- INT $0x80/iret
- ソフトウェア割り込み発生させる。システムコールに特化していないのでsysenter、syscallよりオーバーヘッドがある
- 昔からある方法なのでほとんどのCPUは対応している??
- sysenter/sysexitとsyscall/sysret
- システムコール専用に特化していてint命令より負荷は軽い。
x86_64の場合、カーネルはsyscall命令を使うだけでよい。i386の場合、カーネルはCPUの種別によってINT $0x80/iret
、sysenter/sysexit
、syscall/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は、の一部。
リンカスクリプト。
/*
* 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内に配置しようとすることが確認できる。
/*
* 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_EHDR にcurrent->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拡張のようです。
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の場合)
カーネルが定義した実行コードをユーザー空間から参照できるようにして、ライブラリから実行させるものです。システムコールを発行するために、ライブラリはカーネルから指定されたアドレスを呼び出し、カーネルが定義したコードを実行します。
- ユーザー空間にあるCPU種別によって適切なシステムコールが配置された番地のシンボルみたいなもののはず
- そのシンボル名は32ビットLinuxでは
__kernel_vsyscall
で、64ビットLinuxではvsyscall
があるようだ -
int \$0x80
、sysenter/syscall
はVDSO(共有ライブラリ)の枠組みの中にあり、(VDSOを初期化する中のsysenter_setup()でvsyscallの設定もしている)__kernel_vsyscallというシンボルの番地に配置されている。 - glibcからは
__kernelvsyscall
を呼ぶだけでその環境にあったint $0x80
、sysenter/syscall
が呼ばれると思われる。透過的に扱えるのでVirtualSystemCall
と呼ばれている。
vsyscall(x86_64の場合)
環境によるシステムコールの差異を考慮しなくてよいのでglibcから直接システムコール命令を使う。vsyscall領域は使用されなくなったが、32bitアプリケーションなどのためにまだ残っている様子。
glibcからの呼び出し
動的にリンクする場合は**__vdso_gettimeofday**。静的にリンクする場合はVSYSCALL_ADDR_vgettimeofday。64bitの場合でも静的な場合はvsyscallの領域を使用するようでした。
/* 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月号