本文最后更新于:2020年6月29日 晚上

* 昨天总结了main函数前世今生的问题,跟着源码一步步看。。。今天来看看系统调用是什么。。。→_→ *

系统调用(System Call)是应用程序与操作系统与内核之间的接口

  1. 系统调用(System Call)的定义

    • 现代操作系统将可能产生冲突的系统资源(包括文件、I/O等设备)保护起来,阻止应用程序直接访问

    • 为了让应用程序有能力访问系统资源,也为了让程序借助操作系统做一些必须由操作系统支持的行为,每个操作系统都提供一套接口,以供应用程序使用

    • 这些接口往往通过中断实现,比如Linux使用0x80号端口作为系统调用的入口,Windows采用0x2E号中断作为系统调用入口

  2. 系统调用的弊端

    • 使用不便:操作系统提供的系统调用接口过于原始,没有进行很好的包装,使用起来不方便

    • 各个操作系统之间调用不兼容

    • 解决方法:运行库作为系统调用与程序之间的抽象层,可以简化使用,统一形式

    • 运行时库将不同的操作系统的系统调用包装成统一固定的接口,使得同样的代码在不同的操作系统下都可以直接编译并产生一致的效果,即源代码级别上的可移植性

  3. 系统调用的原理

    • 现代操作系统中通常有两种特权级别:用户态(User Mode)和内核态(Kernel Mode)。操作系统根据不同的特权,使不同的代码运行在不同的模式上以限制其权利,提高稳定性和安全性

    • 系统调用是运行在内核态的,而应用程序基本都是运行在用户态的

    • 操作系统一般通过中断(Interrupt)来从用户态切换到内核态

    • 中断是一个硬件或软件发出的请求,要求CPU暂停当前的工作转手去处理更重要的事情。中断一般具有两个属性,一个称为中断号(从0开始),一个称为中断处理程序(Interrupt Service Routine, ISR)不同的中断具有不同的中断号,而中断处理程序又与中断号一一对应。在内核中,有一个数组称为中断向量表(Interrupt Vector Table)这个数组的第n项包含了指向第n号中断的中断处理程序指针

    • 当中断到来时,CPU会暂停执行当前执行的代码,根据中断的中断号,在中断向量表中找到对应的中断处理程序,并调用它。中断处理程序执行完成之后,CPU会继续执行之前的代码

      CPU中断过程

  1. Linux系统调用流程

    • 在x86下,系统调用由0x80中断完成,各个通用寄存器用于传递参数,EAX寄存器用于表示系统调用的接口号。当系统调用返回时,EAX寄存器又作为调用结果的返回值

    • 每个系统调用都对应于内核源代码中的一个函数,他们都以“sys_”开头。(定义路径:linux-2.4.0\include\asm-i386\Unistd.h)

    • 基于int的Linux经典系统调用实现

      Linux系统中断流程

  2. Linux系统调用源码剖析

    • _syscall0宏函数:无参的系统调用的封装第一个参数为系统调用的返回值类型,第二个参数是系统调用的的名称,其展开后形成一个与系统调用名称同名的函数

      //linux-2.4.0\include\asm-i386\Unistd.h 
      #define _syscall0(type,name) \
      type name(void) \
      { \
      long __res; \
      __asm__ volatile ("int $0x80" \//volatile防止编译器对代码优化
      		: "=a" (__res) \//表示用EAX输出返回数据并存储在__res中
      		: "0" (__NR_##name)); \//表示和输出相同的寄存器EAX传递参数
      __syscall_return(type,__res); \
      }
    • _syscall1宏函数带有一个参数的系统调用的封装,通过EBX寄存器传入。x86下的Linux支持的系统调用参数至多有6个,分别使用6个寄存器来传递参数(EBX、ECX、EDX、ESI、EDI、EBP)

      //linux-2.4.0\include\asm-i386\Unistd.h 
      #define _syscall1(type,name,type1,arg1) \
      type name(type1 arg1) \
      { \
      long __res; \
      __asm__ volatile ("int $0x80" \
      	: "=a" (__res) \
      	//把arg1强制转化为long,然后存放在EBX里作为输入。编译器也会生成相应的代码保护原来的EBX的值不被破坏
      	: "0" (__NR_##name),"b" ((long)(arg1))); \
      __syscall_return(type,__res); \
      }
    • __syscall_return宏函数:用于检查系统调用返回值,并将其转化为C语言的errno错误码。在Linux中系统调用使用返回值传递错误码,如果返回值为负数,表明调用失败,返回值的绝对值就是错误码C语言中大多数函数以返回-1表示调用失败,将错误信息存储在名为errno的全局变量中

      #define __syscall_return(type, res)					\
      do {									\
      	if ((unsigned long)(res) >= (unsigned long)(-125)) {		\
      		errno = -(res);						\
      		res = -1;						\
      	}								\
      	return (type) (res);						\
      } while (0)
    • 切换堆栈:在实际执行中断向量表中的第0x80号元素所对应的函数之前,CPU还要进行栈切换。在Linux中,用户态和内核态使用不同的栈,两者各自负责各自的函数调用。所谓当前栈即ESP所指的栈空间寄存器SS保存当前栈所在的页

      用户栈切换至内核栈步骤如下:

      • 保存当前ESP、SS的值
      • 将ESP、SS的值设置为内核栈的相应值

      内核栈切换至用户栈步骤如下:

      • 恢复原来的ESP、SS的值

      • 用户态的ESP和SS的值保存在内核栈上,由中断指令自动地由硬件完成

        CPU除了切入内核态之外,还自动完成:

      • 找到当前进程地内核栈(每一个进程都有自己的内核栈)

      • 在内核栈中依次压入用户态的寄存去SS、ESP、EFLAGS、CS、EIP

      当内核从系统调用中返回时,需调用iret指令返回到用户态,iret指令会从内核栈中弹出寄存器SS、ESP、EFLAGS、CS、EIP的值,使栈恢复到用户态的状态

      中断时用户栈和内核栈切换

  • 中断处理程序在int指令切换栈之后,程序流程就切换到了中断向量表中记录的0x80号中断处理程序

    ![Linux i386 中断服务流程](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNjI1MjEyNzQ0MzMz?x-oss-process=image/format,png)
    
    **初始化中断向量表**
    //linux-2.4.0\arch\i386\kernel\Traps.c
    //0-19号中断对应的中断处理程序包括算数异常(除零、溢出)、页缺失(page fault)、无效指令
    
    void __init trap_init(void)
    {
    	......
    	
    	set_trap_gate(0,&divide_error);
    	set_trap_gate(1,&debug);
    	set_intr_gate(2,&nmi);
    	set_system_gate(3,&int3);	/* int3-5 can be called from all */
    	set_system_gate(4,&overflow);
    	set_system_gate(5,&bounds);
    	set_trap_gate(6,&invalid_op);
    	set_trap_gate(7,&device_not_available);
    	set_trap_gate(8,&double_fault);
    	set_trap_gate(9,&coprocessor_segment_overrun);
    	set_trap_gate(10,&invalid_TSS);
    	set_trap_gate(11,&segment_not_present);
    	set_trap_gate(12,&stack_segment);
    	set_trap_gate(13,&general_protection);
    	set_trap_gate(14,&page_fault);
    	set_trap_gate(15,&spurious_interrupt_bug);
    	set_trap_gate(16,&coprocessor_error);
    	set_trap_gate(17,&alignment_check);
    	set_trap_gate(18,&machine_check);
    	set_trap_gate(19,&simd_coprocessor_error);
    	
    	//系统调用对应的中断号,在linux-2.4.0\include\asm-i386\Hw_irq.h中,SYSCALL_VECTOR定义0x80
    	set_system_gate(SYSCALL_VECTOR,&system_call);
    
    	......
    }
    **调用int 0x80之后,最终执行system_call函数**
    
    
    ENTRY(system_call)
    	......
    	SAVE_ALL//宏函数SAVE_ALL将各种寄存器压入栈中,即系统调用传入的参数
    	......
    	cmpl $(nr_syscalls), %eax//比较EAX和nr_syscalls,nr_syscalls是比最大的调用号大1的值,如果eax(用户传入的系统调用号)大于等于nr_syscalls,则这个系统调用无效,则会跳转至syscall_badsys执行,反之执行syscall_call
    	jae syscall_badsys
    
    syscall_call:
    	call *sys_call_table(0,%eax,4)//系统调用表中,每一个元素(long类型)都是一个系统调用函数的地址。因此调用的是sys_call_table上偏移量为0+%eax*4上的元素的值指向的函数
    	
    	......
    	RESTORE_REGS//执行结束后,使用宏函数恢复之前SAVE_ALL保存的的寄存器
    	......
    	iret//最后通过指令iter从中断处理程序中返回
    ![Linux系统调用流程](https://imgconvert.csdnimg.cn/aHR0cDovL2ltZy5ibG9nLmNzZG4ubmV0LzIwMTcwNjI1MjMxNDEzNzIy?x-oss-process=image/format,png)

综上所述,Linux系统调用流程为:main -> function -> _syscall -> int 0x80 -> __init trap_init -> system_call -> __syscall_return

* so happy…so tired…→_→ *