いくつかのテキストとソースコードを調べた後、私はに気づきましたfork
。vfork
そしてclone
、3つすべてが異なるパラメータで実行さdo_fork
れます。fork.c
しかし、どのように正確にfork()
..do_fork()
fork()
どのすべての関数が呼び出されるかを呼び出すとき?
ステップバイステップのクラスdo_fork()
から何までfork()
ですか?
いくつかのテキストとソースコードを調べた後、私はに気づきましたfork
。vfork
そしてclone
、3つすべてが異なるパラメータで実行さdo_fork
れます。fork.c
しかし、どのように正確にfork()
..do_fork()
fork()
どのすべての関数が呼び出されるかを呼び出すとき?
ステップバイステップのクラスdo_fork()
から何までfork()
ですか?
libcの実装fork()
およびその他のシステムコールには、システムコールを呼び出す特別なプロセッサ命令が含まれています。システムコールの呼び出しはアーキテクチャ固有であり、非常に複雑なトピックになる可能性があります。
「単純な」例、MIPSから始めましょう。
MIPSでは、システムコールはSYSCALL命令を介して呼び出されます。したがって、libcの実装は、fork()
いくつかのレジスタにいくつかの引数を置き、regiterv0
にシステムコール番号を置き、命令を発行することになりsyscall
ます。
MIPSでは、これによりSYSCALL_EXCEPTION
(例外番号8)が発生します。起動時に、カーネルは例外8を次の処理ルーチンに関連付けますarch/mips/kernel/traps.c:trap_init()
。
set_except_vector(8, handle_sys);
したがって、プログラムが命令を発行したためにCPUが例外8を受け取ると、CPUはsyscall
カーネルモードに移行し、でハンドラーの実行を開始しますhandle_sys
(/usr/src/linux/arch/mips/kernel/scall*.S
32/64ビットのカーネルスペース/ユーザースペースの組み合わせごとにいくつかのファイルがあります)。このルーチンは、システムコールテーブルでシステムコール番号を検索し、適切なsys_...()
関数(この例では)にジャンプしsys_fork()
ます。
現在、x86はより複雑です。従来、Linuxは割り込み0x80を使用してシステムコールを呼び出していました。これは、次のx86ゲートに関連付けられていarch/x86/kernel/traps_*.c:trap_init()
ます。
set_system_gate(SYSCALL_VECTOR,&system_call);
x86プロセッサには、いくつかのレベル(リング)の特権があります(80286以降)。カーネルによって設定された特別な種類のセグメント記述子である事前定義されたゲートを介してのみ、下位リング(=より多くの特権)にアクセス(ジャンプ)することができます。したがって、int 0x80
が呼び出されると、割り込みが生成され、CPUはIDT(割り込み記述子テーブル)と呼ばれる特別なテーブルを検索し、ゲート(x86ではトラップゲート、x86-64では割り込みゲート)があることを確認します。リング0に遷移し、/で/ハンドラーの実行を開始します(system_call
それぞれia32_syscall
x86 / x86_64の場合)。arch/x86/kernel/entry_32.S
arch/x86/ia32/ia32entry.S
ただし、Pentium Pro以降、システムコールを呼び出す別の方法があります。SYSENTER
命令を使用することです(AMDにも独自のSYSCALL
命令があります)。これは、システムコールを呼び出すためのより効率的な方法です。この「新しい」メカニズムのハンドラーは次のように設定されarch/x86/vdso/vdso32-setup.c:syscall32_cpu_init()
ます。
#ifdef CONFIG_X86_64
[...]
void syscall32_cpu_init(void)
{
if (use_sysenter < 0)
use_sysenter = (boot_cpu_data.x86_vendor == X86_VENDOR_INTEL);
/* Load these always in case some future AMD CPU supports
SYSENTER from compat mode too. */
checking_wrmsrl(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
checking_wrmsrl(MSR_IA32_SYSENTER_ESP, 0ULL);
checking_wrmsrl(MSR_IA32_SYSENTER_EIP, (u64)ia32_sysenter_target);
wrmsrl(MSR_CSTAR, ia32_cstar_target);
}
[...]
#else
[...]
void enable_sep_cpu(void)
{
int cpu = get_cpu();
struct tss_struct *tss = &per_cpu(init_tss, cpu);
if (!boot_cpu_has(X86_FEATURE_SEP)) {
put_cpu();
return;
}
tss->x86_tss.ss1 = __KERNEL_CS;
tss->x86_tss.sp1 = sizeof(struct tss_struct) + (unsigned long) tss;
wrmsr(MSR_IA32_SYSENTER_CS, __KERNEL_CS, 0);
wrmsr(MSR_IA32_SYSENTER_ESP, tss->x86_tss.sp1, 0);
wrmsr(MSR_IA32_SYSENTER_EIP, (unsigned long) ia32_sysenter_target, 0);
put_cpu();
}
[...]
#endif /* CONFIG_X86_64 */
上記では、マシン固有のレジスタ(MSR)を使用してセットアップを行います。ハンドラールーチンはia32_sysenter_target
and ia32_cstar_target
(これはx86_64の場合のみ)(inarch/x86/kernel/entry_32.S
またはarch/x86/ia32/ia32entry.S
)です。
使用するシステムコールメカニズムの選択
Linuxカーネルとglibcには、システムコールを呼び出すさまざまな方法から選択するメカニズムがあります。
カーネルは、プロセスごとに仮想共有ライブラリをセットアップします。これはVDSO(仮想動的共有オブジェクト)と呼ばれ、次の出力で確認できますcat /proc/<pid>/maps
。
$ cat /proc/self/maps
08048000-0804c000 r-xp 00000000 03:04 1553592 /bin/cat
0804c000-0804d000 rw-p 00003000 03:04 1553592 /bin/cat
[...]
b7ee8000-b7ee9000 r-xp b7ee8000 00:00 0 [vdso]
[...]
このvdsoには、特に、使用中のCPUに適したシステムコール呼び出しシーケンスが含まれています。例:
ffffe414 <__kernel_vsyscall>:
ffffe414: 51 push %ecx ; \
ffffe415: 52 push %edx ; > save registers
ffffe416: 55 push %ebp ; /
ffffe417: 89 e5 mov %esp,%ebp ; save stack pointer
ffffe419: 0f 34 sysenter ; invoke system call
ffffe41b: 90 nop
ffffe41c: 90 nop ; the kernel will usually
ffffe41d: 90 nop ; return to the insn just
ffffe41e: 90 nop ; past the jmp, but if the
ffffe41f: 90 nop ; system call was interrupted
ffffe420: 90 nop ; and needs to be restarted
ffffe421: 90 nop ; it will return to this jmp
ffffe422: eb f3 jmp ffffe417 <__kernel_vsyscall+0x3>
ffffe424: 5d pop %ebp ; \
ffffe425: 5a pop %edx ; > restore registers
ffffe426: 59 pop %ecx ; /
ffffe427: c3 ret ; return to caller
、、を使用したarch/x86/vdso/vdso32/
実装があり、カーネルは適切なものを選択します。int 0x80
sysenter
syscall
ユーザースペースにvdsoがあり、それがどこにあるかを知らせるために、カーネルは補助ベクトル( 、カーネルから新しく開始されたプロセスに情報を渡すために使用される、の後の4番目の引数)にエントリを設定AT_SYSINFO
します。vdsoのELFヘッダーを指し、vsyscall実装を指します。AT_SYSINFO_EHDR
auxv
main()
argc, argv, envp
AT_SYSINFO_EHDR
AT_SYSINFO
$ LD_SHOW_AUXV=1 id # tell the dynamic linker ld.so to output auxv values
AT_SYSINFO: 0xb7fd4414
AT_SYSINFO_EHDR: 0xb7fd4000
[...]
glibcは、この情報を使用してを検索しますvsyscall
。ダイナミックローダーグローバルに保存します_dl_sysinfo
。例:
glibc-2.16.0/elf/dl-support.c:_dl_aux_init():
ifdef NEED_DL_SYSINFO
case AT_SYSINFO:
GL(dl_sysinfo) = av->a_un.a_val;
break;
#endif
#if defined NEED_DL_SYSINFO || defined NEED_DL_SYSINFO_DSO
case AT_SYSINFO_EHDR:
GL(dl_sysinfo_dso) = (void *) av->a_un.a_val;
break;
#endif
glibc-2.16.0/elf/dl-sysdep.c:_dl_sysdep_start()
glibc-2.16.0/elf/rtld.c:dl_main:
GLRO(dl_sysinfo) = GLRO(dl_sysinfo_dso)->e_entry + l->l_addr;
およびTCB(スレッド制御ブロック)のヘッダーのフィールド:
glibc-2.16.0/nptl/sysdeps/i386/tls.h
_head->sysinfo = GLRO(dl_sysinfo)
カーネルが古く、vdsoを提供しない場合、glibcは次のデフォルトの実装を提供します_dl_sysinfo
。
.hidden _dl_sysinfo_int80:
int $0x80
ret
プログラムがglibcに対してコンパイルされる場合、状況に応じて、システムコールを呼び出すさまざまな方法から選択が行われます。
glibc-2.16.0/sysdeps/unix/sysv/linux/i386/sysdep.h:
/* The original calling convention for system calls on Linux/i386 is
to use int $0x80. */
#ifdef I386_USE_SYSENTER
# ifdef SHARED
# define ENTER_KERNEL call *%gs:SYSINFO_OFFSET
# else
# define ENTER_KERNEL call *_dl_sysinfo
# endif
#else
# define ENTER_KERNEL int $0x80
#endif
int 0x80
←伝統的な方法call *%gs:offsetof(tcb_head_t, sysinfo)
←%gs
はTCBを指しているため、これはTCBに格納されているvsyscallへのポインタを介して間接的にジャンプします。これは、PICとしてコンパイルされたオブジェクトに適しています。これにはTLSの初期化が必要です。動的実行可能ファイルの場合、TLSはld.soによって初期化されます。静的PIE実行可能ファイルの場合、TLSは__libc_setup_tls()によって初期化されます。call *_dl_sysinfo
←これは、グローバル変数を間接的にジャンプします。これには_dl_sysinfoの再配置が必要であるため、PICとしてコンパイルされたオブジェクトでは回避されます。したがって、x86では:
fork()
↓
int 0x80 / call *%gs:0x10 / call *_dl_sysinfo
| ↓ ↓
| (in vdso) int 0x80 / sysenter / syscall
↓ ↓ ↓ ↓
system_call | ia32_sysenter_target | ia32_cstar_target
↓
sys_fork()