• 【操作系统基础】系统接口与系统调用


    本文参考MOOC哈工大操作系统课程与课件
    主要基于Linux 0.11系统展开
    ”Author:Mayiming“

    一、操作系统接口

    1. 什么是操作系统接口?

    如众多接口一样,接口的存在主要是为使用者提供一种方便易懂的途径使用一件东西(不必搞懂这件东西背后的实现原理),接口屏蔽了其背后的复杂性,提供了方便和简洁的使用方法。
    操作系统中的接口体现的形式就是函数,函数的存在给使用者提供了方便的调用方法,屏蔽了函数内部实现的细节。

    2. 操作系统接口体现在哪?

    在linux中体现在命令行、shell、代码中执行的函数等,在windows中体现在界面按钮、各种应用程序。
    这些内容的本质都是通过代码实现的,这些代码又是通过操作系统提供的接口来操作计算机硬件实现的具体功能。

    3. 命令行是怎么执行代码的?

    例如:我们执行以下命令

    hostname:~$ ./output "hello"
    ECHO:hello
    
    • 1
    • 2

    其中output.c代码为

    #include 
    int main(int argc, char * argv[])
    { printf(“ECHO:%s\n”, argv[1]); }
    
    • 1
    • 2
    • 3

    output由output.c编译而来

    gcc –o output output.c
    
    • 1

    命令在shell中执行,而shell依然是一段代码 /bin/sh

    int main(int argc, char * argv[])
    { char cmd[20];
    while(1) { scanf(%s”, cmd);  % 无限循环  shell永不退出
    if(!fork()) {exec(cmd);}   % 创建进程-执行命令 (这里的cmd就是output中的代码)
    else {wait();} } //while(1)
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4. 图形界面是怎么执行代码的?

    在这里插入图片描述
    图像界面的框架主要由上图所示,主要流程为:

    1. 硬件输入(比如鼠标点击,键盘输入等)引发中断
    2. 进入系统消息队列,此时在操作系统内核层面
    3. 应用程序从内核中将消息队列取出,进入WinMain()消息循环
    4. 进入WinProc程序,执行程序

    5. POSIX接口

    POSIX: Portable Operating System Interface of Unix(IEEE制定的一个标准族)
    POSIX 提供了一组标准的操作系统接口,使得软件可以在支持POSIX接口的操作系统之间移植。
    在这里插入图片描述

    二、系统调用的实现

    1. 什么是系统调用?

    系统调用(system call)其中system指的就是操作系统内核(前文【操作系统启动过程】中的system模块),call就如在汇编中call指令一样(跳转到call后面标签指向的程序地址进行执行)。
    那么将上述两者结合起来,system call就是指一段代码(用户态)需要跳转到system模块中执行某一段代码(内核态)的过程,至于用户态和内核态到后面会解释。

    2. 为什么要使用系统调用?

    下面红色代码代表在内存低地址(system 系统内核的地址段中),蓝色代码代表在用户程序所在的地址段中。
    在这里插入图片描述
    为什么main()不能直接调用whoami()?

    答案是为了系统安全考虑,如果用户程序可以任意访问任意内存,那么你的计算机、操作系统、各种信息将全部暴露在你下载的应用程序面前,如果遇到恶意程序这是非常恐怖的。另一方面,是为了系统稳定性考虑,系统内核中有许多重要的代码和数据(IDT,GDT),如果遇到应用程序纂改也会导致系统崩溃。

    3.内核态和用户态

    在这里插入图片描述

    如何实现内核态和用户态的隔离的?

    答案是通过硬件设计实现的,也只能通过硬件设计实现。
    具体是通过对寄存器的检查实现的,CPL代表CS寄存器的最低两位,RPL为DS寄存器的最低两位。

    4. 如何进入内核执行代码?

    硬件提供了"主动进入内核的方法",如果无法进入内核执行代码则意味着应用程序无法调动计算机硬件,所以需要一种方法让应用程序安全的进入内核执行代码。

    对于Intel x86,那就是中断指令int,int指令将使CS中的CPL从 3 改成 0 ,“进入内核”,还记得CPL = 0意味着内核态,此时代表内核已经打开了大门,DPL >= CPL检查通过即可JMP到系统内核段中执行代码。

    5. 系统调用的过程

    系统调用的大致过程分为以下三步:
    在这里插入图片描述
    接下来,又出现几个疑问?

    1. 除汇编外似乎没有直接写过INT指令的代码,那么INT是在哪执行的?
    2. INT中断后操作系统又做了什么事情?

    6. 系统调用的实现

    此时我们用C语言中的printf()函数作为例子,解析以下系统调用的过程。
    在这里插入图片描述1. 代码是最上层是C语言代码printf()
    2. 接下来printf()函数内部由C语言函数库解释,其中调用了__syscall3()函数
    3. __syscall3() 函数中给出INT的名字为write
    4. __syscall3() 函数内部执行了一段内嵌汇编,执行了中断 INT 0x80
    5. 执行了INT 0x80引发中断后,就来到了OS内核中,中断返回后会回到用户态
    上述过程中,INT 0x80之外的代码都由C库函数解释(在用户态执行),只有INT 0x80中断过程中会在OS内核中执行。

    下面对上述中断过程展开介绍:
    在linux/include/unistd.h中

    #define _syscall3(type,name,atype,a,btype,b,ctype,c)\ % syscall3代表3个参数
    type name(atype a, btype b, ctype c) \ 
    { long __res;\   % 定义返回变量
    __asm__ volatile(int 0x80:=a”(__res):””(__NR_##name), % __NR_write 为输入,因为name参数为write ,__res为输出
    ”b”((long)(a)),”c”((long)(b)),“d”((long)(c)))); if(__res>=0) return % fd传给ebx, char *buf传给ecx, count 传给edx
    (type)__res; errno=-__res; return -1;}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    ””(__NR_##name) 等价于代码 a”(__NR_##name) % 意思是将__NR_##name值赋给eax寄存器
    
    • 1

    eax寄存器放返回值(即__NR_write系统调用号),ebx/ecx/edx存放参数。

    在linux/include/unistd.h中
    #define __NR_write 4 //一堆连续正整数(数组下标,函数表索引)
    
    • 1
    • 2

    6.1 INT 0x80的处理过程

    回忆一下在前文【操作系统启动过程】中,head.s代码执行后main函数代码执行了一系列的初始化过程

    void sched_init(void) % 初始化系统调用入口 
    { set_system_gate(0x80,&system_call); } % 表示INT 0x80就要调用system_call来处理
    
    • 1
    • 2

    在这里插入图片描述
    下面这段代码的主要功能就是初始化上面这个IDT表,该表第一行为edx,第二行为eax,将中断处理函数的入口写入到IDT中。
    在linux/include/asm/system.h中

    #define set_system_gate(n, addr) \
    _set_gate(&idt[n],15,3,addr); //idt是中断向量表基址,从该表中找到0x80偏移地址
    #define _set_gate(gate_addr, type, dpl, addr)\ //这里dpl就是内核态那里的DPL,gate_addr就是上面那个表的地址,addr是&system_call
    __asm__(“movw %%dx,%%ax\n\t” “movw %0,%%dx\n\t”\  // dx=ax dx=%0
    “movl %%eax,%1\n\t” “movl %%edx,%2:\ //  %1 = eax, %2 = edx
    :”i”((short)(0x8000+(dpl<<13)+type<<8))),“o”(*(( \ // "i"代表立即数,"o"代表内存变量
    char*)(gate_addr))),”o”(*(4+(char*)(gate_addr))),\
    “d”((char*)(addr),”a”(0x00080000))
    // %1 为0x8000 + dpl左移13位 + type左移8位 = 0xEF00 
    // %1为gate_addr的低4位赋
    // %2 为gate_addr的高四位
    // 0x00080000赋给eax
    // 将addr赋给edx
    // movw位字传送16位,movl为字传送32位
    // movl指令以寄存器作为目的时,它会把该寄存器的高4位字节设置为0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    此时段选择符为0x0008,即CS=0x0008(CPL=0),回忆前文【操作系统启动过程】中 jmpi 0,8即为跳转到内核代码段,执行其中system_call
    在这里插入图片描述

    6.2 system_call

    在linux/kernel/system_call.s中

    nr_system_calls=72
    .globl _system_call
    _system_call: cmpl $nr_system_calls-1,%eax //eax中存放系统调用号
    ja bad_sys_call
    push %ds push %es push %fs
    pushl %edx pushl %ecx pushl %ebx //调用的参数
    movl $0x10,%edx mov %dx,%ds mov %dx,%es //内核数据 edx=0x10,ds=dx=0x10,es=dx=0x10 这个段对应内核数据段
    movl $0x17,%edx mov %dx,%fs //fs可以找到用户数据
    call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax 
    // 4代表每个_sys_call的函数地址占4字节
    // _sys_call_table为系统调用表的基地址
    pushl %eax //返回值压栈,留着ret_from_sys_call时用
    ... //其他代码
    ret_from_sys_call: popl %eax, 其他pop, iret
    // _sys_call_table+4*%eax就是相应系统调用处理函数入口
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    在include/linux/sys.h中

    fn_ptr sys_call_table[]=  // 函数指针表
    {sys_setup, sys_exit, sys_fork, sys_read, sys_write,
    ...};
    // sys_write对应的数组下标为4,__NR_write=4
    
    • 1
    • 2
    • 3
    • 4

    call _sys_call_table(,%eax=4,4)就是call sys_write

    7. 整体流程

    1. 开机,执行6.1中的初始化流程,将INT 0x80中断处理函数设为system_call
    2. 用户调用printf()
    3. C库函数展开到_sys_call3()函数
    4. _sys_call3()函数执行INT 0x80中断
    5. 执行system_call,查表得到__NR_write=4
    6. 执行call _sys_call_table(,%eax,4)调用sys_write
      在这里插入图片描述
      在这里插入图片描述
  • 相关阅读:
    接口自动化测试实践指导(下):接口自动化测试断言设置思路
    如何裁剪视频画面?快来看看这个详细教程
    NAS HomeAssistant
    使用TS 封装 自定义hooks,实现不一样的 CRUD
    nignx配置文件种的跳转
    硬盘基础知识
    Python 全栈测试开发 Chapter8:接口测试
    日志模块loguru 最简单用法
    Command SwiftCompile failed with a nonzero exit code
    【路径规划】基于DWA实现机器人动态避障附matlab代码
  • 原文地址:https://blog.csdn.net/qq_37207042/article/details/128064804