Linux内核分析——扒开系统调用的三层皮(上)

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

一、用户态、内核态和中断处理过程

1、用户通过库函数与系统调用联系起来。

2、在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应着内核态。而在相应的低级别执行状态下代码的掌控范围受到限制。只能在对应级别允许的范围内活动。

3、intel x86 CPU有四种不同的执行级别0-3。Linux只取两种,0级是内核态,3级是用户态。

4、如何区分用户态与内核态?

     cs寄存器的最低两位表明了当前代码的特权级

     CPU每条指令的读取都是通过cs:eip这两个寄存器:cs是代码段选择寄存器,eip是偏移量寄存器

     上述判断由硬件完成

     一般来说在Linux中,地址空间是一个显著的标志:0xc0000000以上的空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问(这里所说的地址空间是逻辑地址而不是物理地址)。

5、中断处理是从用户态进入内核态的主要方式。

6、寄存器上下文

     从用户态切换到内核态时,必须保存用户态的寄存器上下文到内核堆栈中,同时会把当前内核态的一些信息加载,例如cs:eip指向中断处理程序入口。

     如:用户态栈顶地址、当时的状态字、当时的cs:eip的值

7、中断发生后的第一件事就是保存现场 - SAVE_ALL

     中断处理结束前最后一件事是恢复现场 - RESTORE_ALL

二、系统调用概述

1. 系统调用的意义:

    操作系统为用户态进程与硬件设备进行交互提供了一组接口——系统调用。

   (1)把用户从底层的硬件编程中解放出来。

   (2)极大地提高了系统的安全性

   (3)使用户程序具有可移植性

2、API(应用编程接口)

    与系统调用区别:

   (1)API只是一个函数定义

   (2)系统调用通过软中断向内核发出一个明确的请求。

   (3)API可直接提供用户态服务;一个API调用几个系统调用;不同API可调用同一个系统调用。

3、Libc库

   (1)定义的一些API引用了封装例程(唯一目的就是发布系统调用)

   (2)一般每个系统调用对应一个封装例程。

   (3)库再用这些封装例程定义出给用户的API;

4、返回值

   (1)大多封装例程返回一个整数,其值依赖于相应的系统调用;

   (2)-1表示内核不能满足进程的请求;

   (3)Libc定义的errorno变量包含特定出错码;

5、系统调用的三层皮

   (1)1API(xyz)

   (2)中断向量(system_call)

   (3)中断服务程序(sys_xyz) 

 

   (1)系统调用的服务例程中,中断向量0x80与system_call绑定起来。(Linux中可以通过执行int $128来执行系统调用。)

   (2)system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即系统调用号。

   (3)系统调用号将xyz与sys_xyz关联起来。调用号在eax寄存器中。

系统调用的参数传递:

   (1)函数调用——压栈

   (2)用户态到内核态——寄存器传递。

           每个参数长度不能超过32位,个数不能超过6个。

三、使用库函数API和C代码中嵌入汇编代码触发同一个系统调用

1、使用库函数API来获取系统当前时间

使用time(),代码如下:

#include<stdio.h>

#include<time.h>

int  main()

{

    time_t tt; 

    struct tm *t;   //构造一个结构体,方便读取

    tt = time(NULL);   //time系统调用

    t = localtime(&tt); 

    printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year+1900, t->tm_mon, t->tm_mday,  t->tm_hour, t->tm_min, t->tm_sec);

    return 0;

}

2、使用C代码中嵌入汇编代码触发系统调用获取系统当前时间

代码如下:

#include<stdio.h>

#include<time.h>

int  main()

{

    time_t tt; 

    struct tm *t;

    asm volatile( 

    "mov $0,%%ebx\n\t"   //把ebx清零,相当于传参数

    "mov $0xd,%%eax\n\t"    //把0xd放入eax中,即系统调用号13,指time

    "int $0x80\n\t"  

    "mov %%eax,%0\n\t"    //返回值是在eax中,%0指tt,返回值放到tt中去。

        : "=m" (tt)  

    ); 

    t = localtime(&tt); 

    printf("time:%d:%d:%d:%d:%d:%d\n", t->tm_year+1900, t->tm_mon, t->tm_mday,  t->tm_hour, t->tm_min, t->tm_sec);

    return 0;

}

四、总结

   系统调用(System Call)是操作系统为在用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互提供的一组接口。当用户进程需要发生系统调用时,CPU 通过软中断切换到内核态开始执行内核系统调用函数。在Linux 下三种发生系统调用的方法:

   1、通过 glibc 提供的库函数

        glibc 是 Linux 下使用的开源的标准 C 库,它是 GNU 发布的 libc 库,即运行时库。glibc 为程序员提供丰富的 API(Application Programming Interface),除了例如字符串处理、数学运算等用户态服务之外,最重要的是封装了操作系统提供的系统服务,即系统调用的封装。

   2、使用 syscall 直接调用

    如果 glibc 没有封装某个内核提供的系统调用时,就没办法通过上面的方法来调用该系统调用。此时我们可以利用 glibc 提供的syscall 函数直接调用。

   3、通过int指令陷入

    如果我们知道系统调用的整个过程的话,应该就能知道用户态程序通过软中断指令int 0x80 来陷入内核态(在Intel Pentium II 又引入了sysenter指令),参数的传递是通过寄存器,eax 传递的是系统调用号,ebx、ecx、edx、esi和edi 来依次传递最多五个参数,当系统调用返回时,返回值存放在 eax 中。

   

 

posted @ 2016-03-19 18:09  20135235马悦  阅读(457)  评论(0编辑  收藏  举报