zoukankan      html  css  js  c++  java
  • 操作系统:系统调用的实现

    内核态与用户态、内核段与用户段

    内核态与用户态是保护模式下的概念

    内核态:具有较高特权,可以访问所有寄存器和存储区,执行所有指令;OS一般运行在内核态
    用户态:较低权限的执行状态,仅能执行规定的指令(如不能随意 jmp ),访问指定的寄存器,应用程序一般只能在用户态运行

    计算机中用两个 bit 来表示四种特权状态,硬件将 0 作为内核态,3 作为用户态,Windows 与 Linux 均只使用两个状态

    如何保证应用程序不能进入到内核态?

    特权保护
    用户态不能直接转向内核态从而执行特权操作,是由硬件保证的
    操作系统将内存分段看待,内核态执行在受保护的内核段,用户态对应用户段,
    用户态的程序不能由用户段直接跳到内核段,而段是由段寄存器来表示
    CPL(Current Privilege Level)当前特权级别,CS 寄存器的最低两位
    DPL(Destination/Descriptor Privilege Level)目标特权级别,段描述符的权限位
    RPL (Request Privilege Level)请求特权级别,访问的数据段 DS 的最低两位,段选择子的最低两位
    简单地说,DPL描述目标内存段的特权级别,CPL表示当前特权级别,当 DPL>=CPL 时,才允许访问
    稍微详细一点说,RPL 可以与 CPL 不同,比如说 CPL=0,以 RPL = 3 请求访问 DPL = 3 的段,当然是允许的
    即 DPL >= RPL 且 DPL >= CPL 时允许访问,这里涉及到较为复杂的与 GDT 相关的实现,暂且不表
    进一步说明:段选择子与段描述符
    进一步说明:数据段与代码段权限检查
    即每次访问时 通过硬件检查 DPL 与 CPL 是否满足条件,进行特权保护


    系统调用

    应用程序不能随意访问内核区域,但是它又需要特权级别完成一些任务,
    中断 是硬件提供的进入内核的唯一方法, int 指令使 CS 中的 CPL 改成 0,可以"进入内核"
    系统调用就是一段包含 int 指令的代码,表现为一系列的内核函数,
    由应用程序来调用,在内核中执行,将结果返回给应用程序
    即系统调用是操作系统提供给上层程序访问内核的接口

    系统调用:

    1. 应用程序以系统调用的方式访问内核,防止程序随意更改、访问数据与指令
    2. 屏蔽底层细节,使应用程序有更好的移植性

    系统调用的过程:
    ① 用户程序通过系统调用触发相应的中断
    ② 操作系统进行中断处理,获取系统调用号(入口地址)
    ③ 操作系统根据系统调用号执行相应的程序代码

    系统调用的实现

    下面是 Linux 0.11 中,write系统调用的实现
    可以看到,系统调用通过内联汇编传递参数、调用中断
    进一步说明:GCC内联汇编简介

    //<unistd.h>
    #define _syscall3(type,name,atype,a,btype,b,ctype,c) 
    type name(atype a,btype b,ctype c) 
    { 
    long __res; 
    __asm__ volatile ("int $0x80" 
    	: "=a" (__res) 
    	: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); 
    if (__res>=0) 
    	return (type) __res; 
    errno=-__res; 
    return -1; 
    }
    
    //write.c
    _syscall3(int,write,int,fd,const char *,buf,off_t,count)
    

    write实现为

    int write(int fd, const char * buf, off_t count){
    	long __res;
    	__asm__ volatile(
    		"int $0x80"
    		: "=a"(__res)
    		: "0" (__NR_write),"b" ((long)(fd)),"c" ((long)(buf)),"d" ((long)(count))
    	);
    	if(__res>=0)
    		return (int) __res;
    	errno =- __res;
    	return -1;
    }
    

    int 0x80 又是如何利用中断执行相应的程序的呢?
    系统在 main 中进行初始化时执行的 sched_init 函数中设置了 0x80 的中断处理

    void sched_init(){
    	set_system_gate(0x80, &system_call);//设置0x80的中断处理
    }
    
    //linux/include/asm/system.h
    #define set_system_gate(n, addr) _set_gate(&idt[n], 15, 3, addr);
    //idt中断向量表基址, n中断处理号,  addr 中断服务程序
    
    #define _set_gate(gate_addr,type,dpl,addr) 
    __asm__ (
    	"movw %%dx,%%ax
    	" 
    	"movw %0,%%dx
    	" 
    	"movl %%eax,%1
    	" 
    	"movl %%edx,%2" 
    	: 
    	: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), 
    	"o" (*((char *) (gate_addr))),  eax的值放到idt[n]的前四个字节
    	"o" (*(4+(char *) (gate_addr))),  edx的值放到idt[n]的后四个字节
    	"d" ((char *) (addr)), 
    	"a" (0x00080000)
    )
    

    相当于

    eax = 0x00080000	edx = addr
    eax = 0x00080000 | (addr << 16)
    edx = addr & 0x0000 | (0x8000+(dpl<<13)+(type<<8)
    eax -> idt[n]前四个字节  edx -> idt[n]后四个字节
    

    这段代码将 system_call 的入口地址与相应的段选择符、DPL 及其他信息
    填充为 IDT 的一个表项
    在这里插入图片描述
    即设置成为
    在这里插入图片描述
    斯巴拉西
    注意这里 DPL 被置为 3,是用户态可访问的
    那么取到表项中的段选择子(Selector)与偏移地址(Offset)合成新的 PC(CS : IP)
    CS = 0x0008,CPL = 0,IP = addr = system_call
    system_call 就是中断 int 0x80 的处理程序,也就是说
    系统调用是通过中断机制实现的

    继续分析,system_call 如何执行用户程序调用的系统调用
    在这里插入图片描述
    简单地来看,现实检查了系统调用号小于 nr_system_call - 1
    然后压栈保存 ds、es、fs,edx、ecx、ebx 的值
    再将 ds、es 设置为内核数据段,cs 已经被设置为内核代码段,fs 指向用户数据段
    通过 _sys_call_table + eax * 4 得到系统调用处理函数的入口地址
    eax 存放系统调用号,4 为 x86 系统地址字节数,_sys_call_table 为系统调用表基址
    pushl eax 压栈保存系统调用返回值,在 ret_from_sys_call 中使用
    在这里插入图片描述
    最后恢复寄存器值,使用 iret 指令从中断返回

    再看 _sys_call_table 是如何组织的
    在 include/linux/sys.h 中
    在这里插入图片描述
    所有的系统调用处理函数的指针,组织成这样的表
    且 fn_ptr 定义为返回值为 int 的函数指针,这里参数列表为空
    在 include/linux/sched.h 中
    在这里插入图片描述
    就这样 call _sys_call_table + eax * 4 执行真正的系统调用功能,这里 eax 为 _NR_write = 4,
    执行 sys_write 函数

    在系统调用返回时 iret 指令会将 CPL 置回3

    再简单说一下 system_call 还做了哪些事
    在系统调用功能函数结束后,检查当前任务的运行状态,如果不在就绪状态就去执行调度程序
    如果在就绪态,但是时间片用完,也会执行调度程序
    即 系统调用返回时会检查进程调度
    若继续执行,则返回用户程序系统调用


    总结一下
    因为安全问题,用户程序不能直接访问内核,由硬件检查 DPL >= CPL 保证
    而有些任务又必须在内核态下完成,系统调用是用户程序访问内核的接口,由中断机制实现
    因此,系统调用实际上是 int 0x80 对应的中断处理程序,再根据系统调用号执行不同的程序,完成相应的功能


    2019/12/15

  • 相关阅读:
    【STL】各容器成员对比表
    C/C++ 笔试、面试题目大汇总2
    运维
    Docker_基础运用
    IntelliJ_idea_Ultimate_2018.1_windows
    python_IED工具下载(pycharm)_windows版
    排序_归并排序_递归
    递归_汉诺塔问题
    递归_变位字
    递归_三角数字和阶乘
  • 原文地址:https://www.cnblogs.com/kafm/p/12721791.html
Copyright © 2011-2022 走看看