系统调用过程

陈民禾  原创作品转载请注明出处 ——《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000

一.复习上周内容

       上周主要学习了内核的启动过程可以简单地这么来看:start_kernel从内核一启动的时候它会一直存在,这个就是0号进程,idle就是一个while0,一直在循环着,当系统没有进程需要执行的时候就调度到idle进程,我们在windows系统上会经常见到,叫做system idle,这是一个一直会存在的0号进程,然后呢就是0号进程创建了1号进程,这个init_process是我们的1号进程也就是第一个用户态进程,也就是它默认的就是根目录下的程序,也就是常会找默认路径下的程序来作为1号进程,1号进程接下来还创建了kthreadd来管理内核的一些线程,这样整个程序就启动起来了。

二.内核态、用户态、中断等概念的介绍

用户态和内核态的区分:

       现代计算机机中都有几种不同的指令级别,在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态,而在相应的低级别执行状态下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动。举例:Intrel x86 CPU有四种不同的执行级别0-3,Linux只使用了其中的0级和3级来分别表示内核态和用户态。操作系统让系统本身更为稳定的方式,这样程序员自己写的用户态代码很难把整个系统都给搞崩溃,内核的代码经过仔细的分析有专业的人员写的代码会更加健壮一些,整个程序会更加稳定一些,注意:这里所说的地址空间是逻辑地址而不是物理地址。

     用户态和内核态的很显著的区分就是:CS和EIP, CS寄存器的最低两位表明了当前代码的特权级别;CPU每条指令的读取都是通过CS:EIP这两个寄存器:其中CS是代码段选择寄存器,EIP是偏移量寄存器,上述判断由硬件完成。一般来说在Linux中,地址空间是一个显著的标志:0xc0000000以上的地址空间只能在内核态下访问,0xc00000000-0xbfffffff的地址空间在两种状态下都可以访问。

中断处理是从用户态进入到内核态的主要的方式:

      也可能是用户态程序执行的过程中调用了一个系统调用陷入了内核态当中,这个叫做trap,系统调用只是一种特殊的中断。
      寄存器上下文:
            ——从用户态切换到内核态的时候
                  必须保存用户态的寄存器上下文
                  要保存哪些?
                  保存在哪里?
      中断/int指令会在堆栈上保存一些寄存器的值
            ——如:用户态栈顶地址、当时的状态字、当时的cs:eip的值
      中断发生的后的第一件事就是保护现场,保护现场就是进入中断的程序保存需要用到的寄存器数据,恢复现场就是退出中断程序,恢复、保存寄存器的数据。
       
 #define SAVE_ALL                                                      RESTORE_ALL
       "cld\n\t"\                                                      popl %ebx;
       "pushl %es\n\t"\                                                popl %ecx;
       "pushl %ds\n\t"\                                                popl %ebx;
       "pushl %eax\n\t"\                                               popl %edx;
       "pushl %ebp\n\t"\                                               popl %esi; 
       "pushl %edi\n\t"\                                               popl %edi; 
       "pushl %esi\n\t"\                                               popl %ebp;  
       "pushl %edx\n\t"\                                               popl %eax; 
       "pushl %ecx\n\t"\                                               popl %ds;              
       "pushl %ebx\n\t"\                                               popl %es;
       "movl $" STR(_KERNEL_DS)",%edx\n\t"\                            addl $4,%esp;
       "movl %edx,%ds\n\t"\                                            iret;
       "movl %edx,%es\n\t"

      iret指令与中断信号(包括int指令),发生时的CPU的动作正好相反。

仔细分析一下中断处理的完整过程:

      interrupt(ex:int0x80)-save//发生系统调用      
       cs:eip/ss:esp/ss:esp/efalgs(current)to kernel stack,then load cs:eip(entry of a specific ISR)and ss:esp(point to kernel stack) //保存了cs:eip的值,保存了堆栈寄存器当前的栈顶,当前的标志寄存器,当前的保存到内核堆栈里面,当前加载了中断信号和系统调用相关联的中断服务程序的入口,把它加载到当前cs:eip的里面,同时也要把当前的esp和堆栈段也就是指向内核的信息也加载到cpu里面,这是由中断向量或者说是int指令完成的。这个时候开始内核态的代码。
SAVE_ALL
     -...//内核代码,完成中断服务,可能会发生进程调度  
    RESTOER_ALL                //完成之后再返回到原来的状态
    iret-pop    
     cs:eip/ss:eip/eflag from kernel stack

三.系统调用概述

系统调用的意义:
      操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用:1.把用户从底层的硬件编程中解放了出来;2.极大地提高了系统的安全性使用户程序具有可移植性;用户程序与具体硬件已经被抽象接口所替代。
操作系统提供的API和系统调用的关系:
     API(应用程序编程接口)和系统调用:应用编程接口和系统调用是不同的:1.API只是一个函数定义;2.系统调用通过软中断向内核发出了一个明确的请求。
     Libc库定义的一些API引用了封装例成,唯一目的就是发布系统调用:1.一般每个系统调用对应一个封装例程;2.库函数再用这些封装例程定义出给用户的API(把系统调用封装成很多歌方便程序员使用的函数,不是每个API都对应一个特定的系统调用)
     API可能直接提供用户态的服务 如:一些数学函数 1.一个单独的API可能调用几个系统调用2.不同的API可能调用了同一个系统调用返回:大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用-1在多数情况下表示内核不能满足进程的请求,Libc中定义的errno变量包含特定的出错码;下面一张图可以表示它们的工作过程:
    
       x,y,z就是函数,系统调用应用程序编程接口,这个应用程序编程接口里面封装了一个系统调用,这会触发一个0x80的一个中断,这个中断向量就对应着SYSTEM_CALL这个内核代码的入口的起点,sys_xyz是对应的中断服务程序,在中断服务程序执行完之后,它可能会ret_from_sys_call, 之后就经过这个函数进行处理, 这是一个进程调度的时机,如果没有发生系统调用的时机,如果没有发生系统调用,它就会ireturn可能就会返回到用户态接着执行。
我们要扒开系统调用的三层皮,我们讲这三层皮分别是:xyz、system_call和sys_xyz
      第一个就是API、第二个就是中断向量对应的这些也就是中断服务程序,中断向量对用的系统调用它有很多种不同的服务程序,比如sys_xyz,这就是三层皮。
      我们仔细看一下系统调用的服务历程:中断向量0x80与system_call绑定起来:
      当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行第一个内核函数
      1.在Linux中是通过执行ini $0x80来执行系统调用的,这条汇编指令产生向量为128的编程异常
      2. Intel Pentium ll中引进了sysenter指令(快速系统调用)
系统调用号将xyz和sys_xyz关联起来了:
      传参:
      1.内核实现了很多不同的系统调用
      2.进程必须指明需要哪些系统调用,这需要传递一个系统调用号的参数,使用eax寄存器
      系统调用也需要输入输出参数,例如: 
     1.实际的值 2.用户态进程地址空间的变量的地址 3.甚至是包含指向用户态函数的指针的数据结构的地址
 system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号
     2.一个应用程序调用fork(0封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即_NR_fork)
     3.这个寄存器的设置是libc库中封装例程进行的,因此用户一般不关心系统调用号
     4.进入sys_call之后,立即将eax的值压入内核堆栈
寄存器传递参数有如下限制:
     1.每个参数的长度不能超过寄存器的长度,即32位
     2.在系统调用号eax之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)
     超过6个怎么办?做一个把某个寄存器作为指针,指向一块内存,这样进入内核态之后可以访问所有内存空间,这就是系统调用的参数传递方式。
 四.库函数API和C代码中嵌入汇编代码两种方式系统调用
 首先选择一个系统调用,我选的是write,然后是用c语言写一段正常熟悉的系统调用代码,如下:
复制代码
#include<stdio.h>
#include<unistd.h>
int main(void)
{
    write(1,"hello world!5124\n",13);
    return 0;
 }
复制代码

   下面是我的命令行内容:

      其中,write有三个参数,第一个是表示写到终端屏幕上,1可以认为是屏幕的代号,第二个参数是写的内容,我是把hello world!写到屏幕上,并换行,第三个参数是写入的字符串长度,长度要大于等于要输出的字符串长度,否则只能输出字符串的一部分。程序执行结果如下:

然后是把这段代码转写为嵌入式汇编,嵌入式汇编的格式如下:
复制代码
_asm_(
     汇编语句模块:
     输出部分:函数调用时候的参数
     输入部分:函数调用时候的参数
     破坏描述部分):
     即格式为asm("statements":output_regs:input_regs:clobbered_regs);
可以看成是一个函数,有时候可以加一个_volatile_来选择让编译器优化或者不让编译器优化。
复制代码

代码如下:

复制代码
#include<stdio.h>
#include<unistd.h>
 int main()
{
   int a;
   char *ch="hello world!\n";
 
    asm volatile(
        "movl $0x4,%%eax\n\t"
        "movl $0x1,%%ebx\n\t"
        "movl $0x1,%%ecx\n\t"
        "movl $0xd,%%edx\n\t"
        "int $0x80\n\t"
        "movl %%eax,%0\n\t"
        :"=m"(a)
        :"s"(ch)
        );
      return 0;
 }
复制代码

 write系统调用有三个参数,分别是:写入的位置,内容和长度,所以转化为汇编对应的寄存器为eax(系统调用号为4),ebx(参数),ecx(输出位置),edx(参数长度)

执行代码如下:
五.实验感想
       计算机科学中有一句话,任何计算机相关问题都可以通过加一个中间层来解决。操作系统的系统调用也是这样,system_call将api和系统函数连接起来,这样可以保证内核的安全,不会因为用户的失误操作而造成问题。操作系统为了安全,把一些重要的调用放在内核部分,这样只能通过触发系统调用来完成相应功能,这样可以保证内核的安全,但是不可避免的也造成了系统调用的消耗比较大。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值