Linux kernel 从fork()到do_fork()的函数调用

Linux kernel 从fork()到do_fork()的函数调用,linux-kernel,Linux Kernel,在阅读了一些文本和源代码后,我意识到,fork、vfork和clone这三个都是通过fork.c中的do_fork以不同的参数执行的 但是fork()调用do\u fork()的确切方式是什么呢 调用fork()时调用哪些函数 从fork()到do\u fork()的逐步类是什么?libc对fork()和其他系统调用的实现包含调用系统调用的特殊处理器指令。系统调用是特定于体系结构的,可能是一个相当复杂的主题 让我们从一个“简单”的例子MIPS开始: 在MIPS上,通过SYSCALL指令调用系统调

在阅读了一些文本和源代码后,我意识到,
fork
vfork
clone
这三个都是通过
fork.c
中的
do_fork
以不同的参数执行的

但是
fork()
调用
do\u fork()
的确切方式是什么呢

调用
fork()
时调用哪些函数


fork()
do\u fork()
的逐步类是什么?

libc
fork()
和其他系统调用的实现包含调用系统调用的特殊处理器指令。系统调用是特定于体系结构的,可能是一个相当复杂的主题

让我们从一个“简单”的例子MIPS开始:

在MIPS上,通过SYSCALL指令调用系统调用。因此,libc对
fork()
的实现最终在一些寄存器上放置了一些参数,regiter
v0
中的系统调用号,并发出了一条
syscall
指令

在MIPS上,这会导致系统调用异常(异常编号8)。引导时,内核将异常8关联到
arch/mips/kernel/traps.c:trap_init()
中的处理例程:

因此,当CPU收到异常8,因为程序发出了
syscall
指令时,CPU将转换到内核模式,并在
/usr/src/linux/arch/mips/kernel/scall*.S
处开始执行处理程序(不同的32/64位内核空间/用户空间组合有多个文件)。该例程在系统调用表中查找系统调用号,并跳转到相应的
sys_…()
函数,在本例中为
sys_fork()

现在,x86更加复杂。传统上,Linux使用中断0x80来调用系统调用。这与
arch/x86/kernel/traps.*.c:trap_init()
中的x86门相关联:

x86处理器具有多个级别(环)的权限(从80286开始)。只能访问(跳转到)较低的环(=更多权限)通过预定义的门,这是内核设置的特殊类型的段描述符。因此,当调用
int0x80
时,会生成一个中断,CPU会查找一个称为IDT(中断描述符表)的特殊表,看到它有一个门(x86中的陷阱门,x86-64中的中断门),并转换到环0,开始执行
arch/x86/kernel/entry\u 32.S
/
arch/x86/ia32/ia32entry.S处的
system\u调用
/
系统调用
(分别适用于x86/x86\u 64)

但是,自从奔腾Pro以来,还有一种调用系统调用的替代方法:使用
SYSENTER
指令(AMD也有自己的
SYSCALL
指令)。这是调用系统调用的更有效的方法。此“较新”机制的处理程序设置为
arch/x86/vdso/vdso32 setup.c:syscall32_cpu_init()

除其他外,此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
glibc使用此信息定位
vsyscall
。它将其存储到动态加载程序全局
\u dl\u 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(线程控制块)标题中的字段中:

如果内核很旧并且没有提供vdso,glibc将为
\u dl\u 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\u head\u t,sysinfo)
    ← <代码>%gs
指向TCB,因此这会通过TCB中存储的vsyscall指针间接跳转。对于编译为PIC的对象,这是首选。这需要TLS初始化。对于动态可执行文件,TLS由ld.so初始化。对于静态饼图可执行文件,TLS由_libc_setup_TLS()初始化
  • call*\u dl\u 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()
    

    fork()->system call->transition to kernel mode->syscall table lookup->sys\u fork()->do\u fork()我知道..fork()到底是如何调用NR\u fork的?那么NR\u fork是如何调用sys\u fork()???fork()->程序使用依赖于arch的方式执行系统调用->处理器转换到内核模式,转换到内核特定于arch的初始化中指定的地址->特定于arch的系统调用处理程序参考特定于arch的系统调用表->sys_fork()系统如何理解fork()时您提到的步骤,即进程转换、转换到内核模式等,都发生在ENTRY.S中。
    $ LD_SHOW_AUXV=1 id    # tell the dynamic linker ld.so to output auxv values
    AT_SYSINFO:      0xb7fd4414
    AT_SYSINFO_EHDR: 0xb7fd4000
    [...]
    
    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;
    
    glibc-2.16.0/nptl/sysdeps/i386/tls.h
    
    _head->sysinfo = GLRO(dl_sysinfo)
    
    .hidden _dl_sysinfo_int80:
    int $0x80
    ret
    
    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
    
                           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()