系统调用学习

系统调用学习

系统调用的实质就是函数调用,只是调用的函数是系统函数,处于内核态。

API与系统调用的区别:

  • 每个系统调用对应一个服务例程,但一个API可对应多个系统调用。
  • 有些API直接提供用户态服务不需要用到系统调用。

为什么不直接调用内核函数执行?就可以省掉系统调用这个步骤?

用户空间程序不能直接执行内核代码,内核在受保护的地址空间上,不允许用户进程在内核地址空间上读写,极大地提高了系统的安全性,内核在请求接口时可以检查请求的正确性,其次,使得编程更加容易,相当于提供了一组接口。还有一点是有了隔离层使得程序更具有移植性。

系统调用其实是:应用程序以某种方式通知系统,告诉内核自己需要执行一个系统调用,靠软中断向内核发出请求,通过引发一个异常CPU切换到内核态去执行异常处理程序(即系统调用处理程序)(Linux对系统调用的调用早期是必须通过执行int $0x80汇编指令,产生向量为128的异常(异常是CPU内部出现的中断)),接着进程会传递系统调用号给内核识别所需系统调用,进入系统调用服务程序而后找到系统调用号所对应系统调用服务例程进行处理,其实服务例程才是实际上处理数据的程序。而系统调用处理程序,只是实现了从用户态到内核态转换后的一些必要处理而已,再由服务例程返回系统调用,最终CPU由内核态切回用户态,完成系统调用。

系统调用的宏观过程:

  • 用户向内核传递一个系统调用号(EAX寄存器负责传递)

    • Linux有几百个系统调用,为每一个系统调用定义了一个唯一的编号,此编号称为系统调用号,定义在linux/arch/x86/include/asm/unistd_.h中

      #define __NR_io_setup 0
       34 __SC_COMP(__NR_io_setup, sys_io_setup, compat_sys_io_setup)
       35 #define __NR_io_destroy 1
       36 __SYSCALL(__NR_io_destroy, sys_io_destroy)
       37 #define __NR_io_submit 2
       38 __SC_COMP(__NR_io_submit, sys_io_submit, compat_sys_io_submit)
       39 #define __NR_io_cancel 3
       40 __SYSCALL(__NR_io_cancel, sys_io_cancel)
       ...
       ...
       725 #define __NR_pwritev2 287
       726 __SC_COMP(__NR_pwritev2, sys_pwritev2, compat_sys_pwritev2)
       727 #define __NR_pkey_mprotect 288
       728 __SYSCALL(__NR_pkey_mprotect,  sys_pkey_mprotect)
       729 #define __NR_pkey_alloc 289
       730 __SYSCALL(__NR_pkey_alloc,    sys_pkey_alloc)
       731 #define __NR_pkey_free 290
       732 __SYSCALL(__NR_pkey_free,     sys_pkey_free)
       733 #define __NR_statx 291
       734 __SYSCALL(__NR_statx,     sys_statx)
      
      

      可以看到目前有291个系统调用。

  • 系统调用处理程序通过此号从系统调用表中找到相应内核函数执行(系统调用服务例程)利用系统调用号作为下标,找到系统调用的封装例程。(bar()在内核态有对应的sys_bar(),则sys_bar()就是系统调用服务例程)

    • 为了把系统调用号与服务例程关联起来,内核利用了系统调用表,
  • 返回:通过syscall_exit_work()函数(由汇编语言编写)。所有的系统调用都返回一个整数,大部分封装例程返回一个整数,值依赖于相应的系统调用。(负数表示一个出错条件)

系统调用过程

初始化

执行int $0x80汇编指令:内核初始化期间调用trap_init()建立IDT(中断描述符表)中128好向量对应的表项

在arch/x86/kernel/traps.c的trap_init()函数中可以看到:

#ifdef CONFIG_X86_32

set_system_trap_gate(SYSCALL_VECTOR, &system_call);

set_bit(SYSCALL_VECTOR, used_vectors);

#endif

其中SYSCALL_VACTOR值为0x80,通过这个值将段选择子、偏移量、类型、DPL装入门描述符的相关域。

开始处理

system_call函数实现了系统调用处理程序,把系统调用号和这个异常处理程序可以用到的所有CPU寄存器保存到相应的栈里面,然后对系统调用号进行有效性检查,如果这个号大于或者等于NR_syscalls,系统调用处理程序终止。如果系统调用号无效,则跳转到syscall_badsys处执行,结果返回一个负的返回码。

若正确,则根据EAX传递的系统调用号调用对应的服务例程。

ENTRY(system_call)
 RING0_INT_FRAME          
 pushl_cfi %eax                      
 SAVE_ALL
 GET_THREAD_INFO(%ebp)
 testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
 jnz syscall_trace_entry
 cmpl $(nr_syscalls), %eax
 jae syscall_badsys
 syscall_call:
 call *sys_call_table(,%eax,4)
 movl %eax,PT_EAX(%esp)      
 syscall_exit:
 LOCKDEP_SYS_EXIT
 DISABLE_INTERRUPTS(CLBR_ANY)    
 TRACE_IRQS_OFF
 movl TI_flags(%ebp), %ecx
 testl $_TIF_ALLWORK_MASK, %ecx  # current->work
 jne syscall_exit_work
 restore_all:
 TRACE_IRQS_IRET
 restore_all_notrace:
 movl PT_EFLAGS(%esp), %eax  # mix EFLAGS, SS and CS
 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
 CFI_REMEMBER_STATE
 je ldt_ss          
 restore_nocheck:
 RESTORE_REGS 4          
 irq_return:
 INTERRUPT_RETURN

另一种进入系统调用的方法是执行sysenter汇编语言指令。(被称为快速系统调用,从用户态到内核态的快速切换方法)

  • 封装例程把系统调用号装入EAX,调用__kenel_vsyscall()将系统调用使用寄存器的内存保存到用户态堆栈中,把用户栈指针拷贝到ebp中执行sysenter指令。
  • CPU切换到内核态,内核执行sysenter_entry()汇编语言函数,此函数与system_call函数操作类似。

如何根据系统调用号找到对应服务例程?

把EAX中的系统调用号乘以4,加上sys_call_table系统调用表的起始地址,从这个地址即可获得指向相应服务例程的指针,内存即找到了需要调用的服务例程。

在arch/x86/kernel/syscall_table_32.S中定义了系统调用表

ENTRY(sys_call_table)
.long sys_restart_syscall   
system call, used for r   
       .long sys_exit
       .long ptregs_fork
       .long sys_read
       .long sys_write
       .long sys_open      

当服务例程执行结束

  • system_call()从EAX获得返回值,把这个返回值存在栈中用户态EAX寄存器曾存放的位置,接着执行syscall_exit终止系统调用处理程序的执行。
  • 而采用syenter指令发出时,sysenter_entry()与system_call()函数有着相同的操作。其中需要iret从内核堆栈中去取当时切换到内核态时所保存的5个参数,而后CPU再切换到用户态,ecx获得当前用户数据栈的指针。

系统调用中的参数传递

之前看到系统调用只传递了EAX寄存器中的系统调用号,但是有些系统调用需要传递多个参数(如mmap),那么系统在用寄存器传递参数时遵循两个原则:

  • 超过寄存器位数的长参数通过制定它们的地址来传递。
  • 多于6个参数系统调用的传递用一个单独的寄存器指向进程地址空间中这些参数值的一个内存区。

之前在写Linux系统编程时经常用到文件操作的系统调用,其中文件描述符就是给与内核参数验证,若参数不对则会返回负数请求系统调用失败。

实验部分(添加系统调用)

知道了原理做实验思路就比较清晰,需要做的是一个增加系统调用日志收集系统。目前正在做,主要步骤是

  • 添加系统调用号

  • 增加系统调用表的表项

  • 增加系统调用函数

  • 修改Makefile以及函数声明

  • 用编写的代码拦截所需系统调用,提供内核接口函数

  • 重新编译内核

  • 在用户态编写代码挂在内核的钩子函数上

  • 运行用户代码打印系统调用日志

你可能感兴趣的:(基础部分)