vsyscall
Rev.3を表示中。最新版はこちら。
86ではシステムコールとしてint80とペンティアムで実装されているsysenterとがあるのですが、glibcでは直接int80ないしsysenterとしてコード展開していません。glibcは__kernel_vsyscallとしてリンクしています。カーネルは立ち上がり時、ラベルを__kernel_vsyscallとして、int80ないしsysenterで、システムコールするモジュールをカーネルのページに読み込み、ユーザプロセスを起動する時に、このページのモジュールをプロセスにリンクすることで、int80ないしsysenterに透過的な動作を実現しています。
start_kernel()関数からcheck_bugs()関数が呼ばれ、そこからidentify_boot_cpu()そして、sysenter_setup()とコールされます。ここでシステムコールのモジュールがページに読み込まれます。
get_zeroed_page関数で取得したページのリニアアドレスを、vdso32_pagesにセットします。syscallのCPUなら、vsyscallにsyscallにそのモジュールのアドレスを、vsyscall_lenにそのサイズを設定します。同様にsysenterならsysenterのモジュールを、どちらでもないなら、int80のモジュールを設定しています。
そして、memcpy関数でvdso32_pagesに複写し、relocate_vdso関数でモジュールをリロケーションしています。
int __init sysenter_setup(void) { void *syscall_page = (void *)get_zeroed_page(GFP_ATOMIC); const void *vsyscall; size_t vsyscall_len; vdso32_pages[0] = virt_to_page(syscall_page); #ifdef CONFIG_X86_32 gate_vma_init(); #endif if (vdso32_syscall()) { vsyscall = &vdso32_syscall_start; vsyscall_len = &vdso32_syscall_end - &vdso32_syscall_start; } else if (vdso32_sysenter()){ vsyscall = &vdso32_sysenter_start; vsyscall_len = &vdso32_sysenter_end - &vdso32_sysenter_start; } else { vsyscall = &vdso32_int80_start; vsyscall_len = &vdso32_int80_end - &vdso32_int80_start; } memcpy(syscall_page, vsyscall, vsyscall_len); relocate_vdso(syscall_page); return 0; }root/arch/x86/vdso/vdso32.S
__INITDATA .globl vdso32_int80_start, vdso32_int80_end vdso32_int80_start: .incbin "arch/x86/vdso/vdso32-int80.so" vdso32_int80_end: .globl vdso32_syscall_start, vdso32_syscall_end vdso32_syscall_start: #ifdef CONFIG_COMPAT .incbin "arch/x86/vdso/vdso32-syscall.so" #endif vdso32_syscall_end: .globl vdso32_sysenter_start, vdso32_sysenter_end vdso32_sysenter_start: .incbin "arch/x86/vdso/vdso32-sysenter.so" vdso32_sysenter_end: __FINITsyscallはAMDプロセッサーで実装されているsysenter相当のコマンドだそうです。
root/arch/x86/vdso/vdso32/syscall.S
.globl __kernel_vsyscall .type __kernel_vsyscall,@function ALIGN __kernel_vsyscall: .LSTART_vsyscall: push %ebp .Lpush_ebp: movl %ecx, %ebp syscall movl $__USER32_DS, %ecx movl %ecx, %ss movl %ebp, %ecx popl %ebp .Lpop_ebp: retroot/arch/x86/vdso/vdso32/sysenter.S
.text .globl __kernel_vsyscall .type __kernel_vsyscall,@function ALIGN __kernel_vsyscall: .LSTART_vsyscall: push %ecx .Lpush_ecx: push %edx .Lpush_edx: push %ebp .Lenter_kernel: movl %esp,%ebp sysenter /* 7: align return point with nop's to make disassembly easier */ .space 7,0x90 /* 14: System call restart point is here! (SYSENTER_RETURN-2) */ jmp .Lenter_kernel /* 16: System call normal return point is here! */ VDSO32_SYSENTER_RETURN: /* Symbol used by sysenter.c via vdso32-syms.h */ pop %ebp .Lpop_ebp: pop %edx .Lpop_edx: pop %ecx .Lpop_ecx: retroot/arch/x86/vdso/vdso32/int80.S
.text .globl __kernel_vsyscall .type __kernel_vsyscall,@function ALIGN __kernel_vsyscall: .LSTART_vsyscall: int $0x80 retプロセスがelfフォーマットで起動される時、load_elf_binary関数で、そのプロセスの動作環境が整えられます。そして、そこからarch_setup_additional_pages関数がコールされ、VDSO(vertual dinamic share object)としての、vsyscallの処理がなされます。
追記
arch_setup_additional_pages()でVDSOのマップが行われる。vdso_enabled = VDSO_DISABLED(0)なら、マップされない。vdso_enabled = VDSO_ENABLED(1)なら、ユーザ空間の先頭から、1ページ分開いている空間を取得する。従って毎回アドレスは異なる。vdso_enabled = VDSO_COMPAT (2)なら、VDSO_HIGH_BASE(0xffffe000)とする。install_special_mapping()で、VDSO空間をcurrent->mm下に登録する。VDSO_ENABLEDならそもそもユーザ空間ということもあり、必ず登録されるが、VDSO_COMPATの場合、compat_uses_vmaは、以下のように定義されていて、CONFIG_X86_64の時、current->mm下に登録される。・
#ifdef CONFIG_X86_64 : #define compat_uses_vma 1 : #else /* CONFIG_X86_32 */ : #define compat_uses_vma 0 : #endif enum { VDSO_DISABLED = 0, VDSO_ENABLED = 1, VDSO_COMPAT = 2, }; int arch_setup_additional_pages(struct linux_binprm *bprm, int exstack) { struct mm_struct *mm = current->mm; unsigned long addr; int ret = 0; bool compat; if (vdso_enabled == VDSO_DISABLED) return 0; down_write(&mm->mmap_sem); compat = (vdso_enabled == VDSO_COMPAT); map_compat_vdso(compat); if (compat) addr = VDSO_HIGH_BASE; else { addr = get_unmapped_area_prot(NULL, 0, PAGE_SIZE, 0, 0, 1); if (IS_ERR_VALUE(addr)) { ret = addr; goto up_fail; } } if (compat_uses_vma || !compat) { ret = install_special_mapping(mm, addr, PAGE_SIZE, VM_READ|VM_EXEC| VM_MAYREAD|VM_MAYWRITE|VM_MAYEXEC| VM_ALWAYSDUMP, vdso32_pages); if (ret) goto up_fail; } current->mm->context.vdso = (void *)addr; current_thread_info()->sysenter_return = VDSO32_SYMBOL(addr, SYSENTER_RETURN); up_fail: up_write(&mm->mmap_sem); return ret; }従ってシステムコールの実装は、以下の3通りの方法で実行される。
glibから直接コールする。(たぶん)
[root@localhost kitamura]# echo 0 > /proc/sys/vm/vdso_enabled [root@localhost kitamura]# ldd a.out libc.so.6 => /lib/libc.so.6 (0x4ea51000) /lib/ld-linux.so.2 (0x4ea28000)
0xb7738000のlinux-gate.so.1からコールする。これはユーザ空間に割り当てられたVDSOである。
[root@localhost kitamura]# echo 1 > /proc/sys/vm/vdso_enabled [root@localhost kitamura]# ldd a.out linux-gate.so.1 => (0xb7738000) libc.so.6 => /lib/libc.so.6 (0x4ea51000) /lib/ld-linux.so.2 (0x4ea28000)
0xffffe000のlinux-gate.so.1からコールする。これはカーネル空間に割り当てられたVDSOである。
[root@localhost kitamura]# echo 2 > /proc/sys/vm/vdso_enabled [root@localhost kitamura]# ldd a.out linux-gate.so.1 => (0xffffe000) libc.so.6 => /lib/libc.so.6 (0x4ea51000) /lib/ld-linux.so.2 (0x4ea28000)VDSOがユーザ空間に割り当てられる場合、その仮想アドレスは毎回異なるが、カーネル空間の場合、そのアドレスは0xffffe000固定である。なお、linux-gate.so.1内に、__kernel_vsyscallでシンボル定義されている。
検証したカーネルは、CONFIG_X86_32。従ってinstall_special_mappingの処理はスキップされているはず。しかし結果はvdsoと表示されている。/proc/pid/mapの処理は、プロセスのvm_structをリストをたどって、メモリ空間を表示しているはず。ちょっとこのあたりを調べてみると、最初にvdsoを領域をvm_structのtailとしてセットしていた。
32ビットアーキテクチャ下で、CONFIG_X86_64でのカーネルを実行した場合とかで、いまひとつわかりませんです。