我是靠谱客的博主 自信冰棍,最近开发中收集的这篇文章主要介绍系统调用内部数据结构以及执行过程的初步分析,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

主要是找到一篇不错的博客进行学习

系统调用概述

​ 计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段。

​ 一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。不过在linux系统编程和之前的实验中是直接采用系统调用来进行编程。

​ linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于内核态,而普通的函数调用由函数库或用户自己提供,运行于用户态。一般情况进程是不能直接访问内核的。它不能访问内核所占内存空间也不能调用内核函数。

​ 但是实际情况用户程序是需要使用系统的硬件资源的,为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,由内核帮助应用程序访问硬件设备和其他操作系统资源。

系统调用原理

要想实现系统调用,要满足以下几个方面:

  1. 通知内核调用一个哪个系统调用
  2. 用户程序把系统调用的参数传递给内核
  3. 用户程序获取内核返回的系统调用返回值

内核内部定义了6个宏。分别可以调用参数个数为0~6的系统调用。

_syscall0(type,name)
_syscall1(type,name,type1,arg1)
_syscall2(type,name,type1,arg1,type2,arg2)
_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)
_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)
_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)
_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)

超过6个参数的系统调用很罕见,所以这里只定义了6个。

系统调用过程

以下的内核版本为:Linux kernel 4.9.76

历史上,x86 的系统调用实现经历了 int / iretsysenter / sysexit 再到 syscall / sysret 的演变。

本次学习的系统调用过程是int 0x80的系统调用过程

最开始进行系统调用是通过int 0x80进行系统调用。

这次学习的是通过open的系统调用进行查看的。

mov 0x05,$eax
int 0x80

而在arch/x86/kernel/traps.c中的trap_init中,定义了各种 set_intr_gate / set_intr_gate_ist / set_system_intr_gate 。其中 set_system_intr_gate 用于在中断描述符表(IDT)上设置系统调用门:

image-20211214161405784

image-20211214161421246

#ifdef CONFIG_IA32_EMULATION     
    set_system_intr_gate(IA32_SYSCALL_VECTOR, entry_INT80_compat);     
    set_bit(IA32_SYSCALL_VECTOR, used_vectors);     
#endif    

根据/home/ycb/linux-4.9.76/arch/x86/include/asm/irq_vectors.h可得:

IA32_SYSCALL_VECTOR 值为 0x80。

image-20211214161619464

调用 int 0x80 后,硬件根据向量号在 IDT 中找到对应的表项,即中断描述符,进行特权级检查,发现 DPL = CPL = 3 ,允许调用。然后硬件将切换到内核栈 (tss.ss0 : tss.esp0)。接着根据中断描述符的 segment selector 在 GDT / LDT 中找到对应的段描述符,从段描述符拿到段的基址,加载到 cs 。将 offset 加载到 eip。最后硬件将 ss / sp / eflags / cs / ip / error code 依次压到内核栈。

总而言之,这部分是CPU的中断,在收到中断时,CPU 会把当前那条指令执行完,然后转移到中断处理程序,返回后执行的是当前的下一条指令。

然后此时从 entry_INT80_32 开始执行,其定义在 arch/x86/entry/entry_32.S

image-20211214162613869

image-20211214162703492

ENTRY(entry_INT80_32)
    ASM_CLAC
    pushl %eax      /* pt_regs->orig_ax */
    SAVE_ALL pt_regs_ax=$-ENOSYS  /* save rest */                                                                                                 
  
    /*
     * User mode is traced as though IRQs are on, and the interrupt gate
     * turned them off.
     */
    TRACE_IRQS_OFF
  
    movl  %esp, %eax
    call  do_int80_syscall_32
  .Lsyscall_32_done:
  
  restore_all:
    TRACE_IRQS_IRET
  restore_all_notrace:
  #ifdef CONFIG_X86_ESPFIX32
    ALTERNATIVE "jmp restore_nocheck", "", X86_BUG_ESPFIX
  
    movl  PT_EFLAGS(%esp), %eax   # mix EFLAGS, SS and CS
  /*     
   * Warning: PT_OLDSS(%esp) contains the wrong/random values if we     
   * are returning to the kernel.     
   * See comments in process.c:copy_thread() for details.     
   */     
  movb  PT_OLDSS(%esp), %ah     
  movb  PT_CS(%esp), %al     
  andl  $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax     
  cmpl  $((SEGMENT_LDT << 8) | USER_RPL), %eax     
  je ldt_ss       # returning to user-space with LDT SS     
#endif     
restore_nocheck:     
  RESTORE_REGS 4        # skip orig_eax/error_code     
irq_return:     
  INTERRUPT_RETURN                                                                                                                                   
.section .fixup, "ax"  

将存在 eax 中的系统调用号压入栈中,然后调用 SAVE_ALL 将其他寄存器的值压入栈中进行保存:

image-20211214163345735

SAVE_ALL宏的实现

.macro SAVE_ALL pt_regs_ax=%eax     
  cld     
  PUSH_GS     
  pushl %fs                                                                                                                                          
  pushl %es     
  pushl %ds     
  pushl pt_regs_ax     
  pushl %ebp     
  pushl %edi     
  pushl %esi     
  pushl %edx     
  pushl %ecx     
  pushl %ebx     
  movl  $(__USER_DS), %edx     
  movl  %edx, %ds     
  movl  %edx, %es     
  movl  $(__KERNEL_PERCPU), %edx     
  movl  %edx, %fs     
  SET_KERNEL_GS %edx     
.endm     

保存完毕后,关闭中断,将当前栈指针保存到 eax ,调用 do_int80_syscall_32 => do_syscall_32_irqs_on

其中do_syscall_32_irqs_onarch/x86/entry/common.c中定义。

image-20211214163825824

image-20211214163850179

static __always_inline void do_syscall_32_irqs_on(struct pt_regs *regs)
{
    struct thread_info *ti = current_thread_info();
    unsigned int nr = (unsigned int)regs->orig_ax; /*系统调用号*/

#ifdef CONFIG_IA32_EMULATION
    current->thread.status |= TS_COMPAT;
#endif

    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY) {
        /*
         * Subtlety here: if ptrace pokes something larger than
         * 2^32-1 into orig_ax, this truncates it.  This may or
         * may not be necessary, but it matches the old asm
         * behavior.
         */
        nr = syscall_trace_enter(regs);
    }

    if (likely(nr < IA32_NR_syscalls)) {
        /*
         * It's possible that a 32-bit syscall implementation
         * takes a 64-bit parameter but nonetheless assumes that
         * the high bits are zero.  Make sure we zero-extend all
         * of the args.
         */
        regs->ax = ia32_sys_call_table[nr](
            (unsigned int)regs->bx, (unsigned int)regs->cx,
            (unsigned int)regs->dx, (unsigned int)regs->si,
            (unsigned int)regs->di, (unsigned int)regs->bp);
    }

    syscall_return_slowpath(regs);
}

这个函数的参数 regs(struct pt_regs 定义见 arch/x86/include/asm/ptrace.h )就是先前在 entry_INT80_32 依次被压入栈的寄存器值。这里先取出系统调用号,从系统调用表(ia32_sys_call_table) 中取出对应的处理函数,然后通过先前寄存器中的参数调用之。

而系统调用表 ia32_sys_call_table 在 arch/x86/entry/syscall_32.c 中定义

  /* System call table for i386. */    
      
#include <linux/linkage.h>    
#include <linux/sys.h>    
#include <linux/cache.h>    
#include <asm/asm-offsets.h>    
include <asm/syscall.h>    
      
  #define __SYSCALL_I386(nr, sym, qual) extern asmlinkage long sym(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigne  d long) ;        
#include <asm/syscalls_32.h>    
  #undef __SYSCALL_I386    
      
  #define __SYSCALL_I386(nr, sym, qual) [nr] = sym,    
      
extern asmlinkage long sys_ni_syscall(unsigned long, unsigned long, unsigned long, unsigned long, unsigned long, unsigned long);    
      
__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {    
    /*    
     * Smells like a compiler bug -- it doesn't work    
     * when the & below is removed.    
     */    
    [0 ... __NR_syscall_compat_max] = &sys_ni_syscall,    
#include <asm/syscalls_32.h>    
  };  

这里会发现系统调用表的内容是 include 进来的。而且syscalls_32.h 是在编译过程中动态生成的。

编译内核后在arch/x86/include/generated/asm

image-20211214172129347

image-20211214173708187

这里就是增加系统调用的时候自己添加的系统调用

#ifdef CONFIG_X87_32
__SYSCALL_I386(0, sys_restart_syscall, )
#else
__SYSCALL_I386(0, __ia32_sys_restart_syscall, )
#endif
#ifdef CONFIG_X86_32
__SYSCALL_I386(1, sys_exit, )
#else
__SYSCALL_I386(1, __ia32_sys_exit, )
#endif
#ifdef CONFIG_X86_32
__SYSCALL_I386(2, sys_fork, )
#else
__SYSCALL_I386(2, __ia32_sys_fork, )
#endif
#ifdef CONFIG_X86_32
__SYSCALL_I386(3, sys_read, )
#else
__SYSCALL_I386(3, __ia32_sys_read, )
#endif
#ifdef CONFIG_X86_32
__SYSCALL_I386(4, sys_write, )
#else
       

#define __SYSCALL_I386(nr, sym, qual) [nr] = sym,可以获得此时ia32_sys_call_table会变成:

__visible const sys_call_ptr_t ia32_sys_call_table[__NR_syscall_compat_max+1] = {
   [0 ... __NR_syscall_compat_max] = &sys_ni_syscall,

   [0] = sys_restart_syscall,
   [1] = sys_exit,
   [2] = sys_fork,
   [3] = sys_read,
   [4] = sys_write,
   [5] = sys_open,
   ...
};

open的调用号是 0x05 ,所以这里调用了 sys_open ,该定义在 fs/open.c 中:

image-20211214172832652

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
    if (force_o_largefile())
        flags |= O_LARGEFILE;

    return do_sys_open(AT_FDCWD, filename, flags, mode);
}

而宏 SYSCALL_DEFINE3 及相关定义如下:

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)                
        SYSCALL_METADATA(sname, x, __VA_ARGS__)       
        __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

#define __SYSCALL_DEFINEx(x, name, ...)                                 
        asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))       
                __attribute__((alias(__stringify(SyS##name))));         
                                                                        
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));  
                                                                        
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__));      
                                                                        
        asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))       
        {                                                               
                long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));  
                __MAP(x,__SC_TEST,__VA_ARGS__);                         
                __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));       
                return ret;                                             
        }                                                               
                                                                        
        static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

嗯…是个看起来很恶心人的宏

其中SYSCALL_METADATA 保存了调用的基本信息,供调试程序跟踪使用。这里不用管。

__SYSCALL_DEFINEx 用于拼接函数,函数名被拼接为 sys##_##open,参数也通过 __SC_DECL 拼接,最终得到展开后的定义:

asmlinkage long sys_open(const char __user * filename, int flags, umode_t mode)
{
    if (force_o_largefile())
        flags |= O_LARGEFILE;

    return do_sys_open(AT_FDCWD, filename, flags, mode);
}

也就是说这里用宏来实现把相应的系统调用转化成具体的函数。

如果之前试过#define debug(x) cout<<#x<<"="<<x<<endl; 的不会陌生。不过这里的实现更加具体和多样

sys_open 是对 do_sys_open 的封装

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
    struct open_flags op;
    int fd = build_open_flags(flags, mode, &op);
    struct filename *tmp;

    if (fd)
        return fd;

    tmp = getname(filename);
    if (IS_ERR(tmp))
        return PTR_ERR(tmp);

    fd = get_unused_fd_flags(flags);
    if (fd >= 0) {
        struct file *f = do_filp_open(dfd, tmp, &op);
        if (IS_ERR(f)) {
            put_unused_fd(fd);
            fd = PTR_ERR(f);
        } else {
            fsnotify_open(f);
            fd_install(fd, f);
        }
    }
    putname(tmp);
    return fd;
}

getname 将处于用户态的文件名拷到内核态,然后通过 get_unused_fd_flags 获取一个没用过的文件描述符,然后 do_filp_open 创建 struct file , fd_install 将 fd 和 struct file 绑定(task_struct->files->fdt[fd] = file),然后返回 fd。

fd一直返回到 do_syscall_32_irqs_on ,被设置到 regs->ax (eax) 中。接着返回 entry_INT80_32 继续执行,最后执行 INTERRUPT_RETURN 。 INTERRUPT_RETURN 在 arch/x86/include/asm/irqflags.h 中定义为 iret ,负责恢复先前压栈的寄存器,返回用户态。系统调用执行完毕。

结语

其过程的确实如操作系统教材讲述的那样,找到要调用的那条系统调用函数,然后由内核帮忙将用户的数据传递,进入内核态前保存一下信息,由内核执行对应的系统调用,结束后将返回值(也就是用户要的东西)传给用户,并返回用户态。

不过理论和实际的实现差距总是很大,不过学习理论还是有指导意义的。实现不了只是现在太菜了。

参考文章

https://arthurchiao.art/blog/system-call-definitive-guide-zh/#42-64-bit-%E5%BF%AB%E9%80%9F%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8

https://blog.csdn.net/gatieme/article/details/50779184

https://www.binss.me/blog/the-analysis-of-linux-system-call/?c=529#your_comment

最后

以上就是自信冰棍为你收集整理的系统调用内部数据结构以及执行过程的初步分析的全部内容,希望文章能够帮你解决系统调用内部数据结构以及执行过程的初步分析所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(49)

评论列表共有 0 条评论

立即
投稿
返回
顶部