LINUX内核分析第四周学习总结——扒开系统调用的“三层皮”

LINUX内核分析第四周学习总结——扒开系统调用的“三层皮”

标签(空格分隔): 20135321余佳源

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


扒开系统调用的“三层皮”

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

(一)如何区分用户态、内核态

1. 一般现在的CPU有几种不同的指令执行级别

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

为什么会有权限级别的划分?
答:当所有程序员写的代码都有特权指令时,系统很容易崩溃,没有访问权限划分容易使得系统混乱。

Intel x86 CPU有四种不同的执行级别0—3,Linux只使用了其中的0级和3级来表示内核态和用户态。

2.如何区分用户态、内核态

 CPU每条指令的读取都是通过cs:eip(代码段选择寄存器:偏移量寄存器)这两个寄存器,由硬件完成判断。
内核态时,cs与eip的值可以访问任意地址
用户态时,cs与eip只可以访问0x00000000—0xbfffffff的地址空间

P.S.这里的地址空间指逻辑地址而非物理地址


(二)中断处理

中断处理是用户态进入内核态主要的方式,系统调用只是一种特殊的中断

  1. 硬件中断,中断服务进程
  2. 用户态执行系统调用,进入内核态

FOR EXAMPLE

interrupt(ex:int 0X80)//系统调用
save cs:eip/ss:esp/eflags(current)to kernel stack,then load cs:eip(entry of a specific ISR)and ss:eip(point to kenerl stack)
//保存当前堆栈段寄存器和当前栈顶和标志位寄存器到内核堆栈中,然后加载当前系统调用相关中断服务例程入口到cs:eip中,把当前的堆栈段和栈顶加载到CPU
SAVE_ALL//进入内核态
...//内核代码,完成中断服务,发生进程调度
RESTORE_ALL//恢复现场,只有在进程调度执行完后才会被执行
iret - pop cs:eip/ss:esp/eflags from kernel stack
中断发生之后第一件事就是保存现场;同样,中断处理结束前的最后一件事情就是恢复现场。也就是说,SAVE ALL之后就是内核态了;restore all之后再返回用户态。

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

注意:从用户态切换到内核态时

必须保存用户态的寄存器上下文

中断指令会把内核态相应的寄存器值放在当前CPU中

中断/int指令会在堆栈上保存一些寄存器的值(eg:用户态栈顶地址(ss:esp),标志寄存器(eflags),cs:eip(为了返回的时候popl弹出保存的返回地址)。同时,将相关联的中端服务历程的入口加载到cs:eip,把当前的堆栈段esp也加载到CPU里面)

系统调用需要int触发,int 80要模拟中断,由硬件来处理,80号中断即为系统调用

中断发生后第一件事就是保存现场,进入中断处理程序,保存需要用到的push到寄存器的值。

中断处理结束前最后一件事是恢复现场,就是退出中断程序,恢复用户态的保存寄存器的数据

iret对应着中断信号恢复指令。


二、系统调用概述和系统调用的三层皮

系统调用

  • 是内核提供的最基本、最重要的服务设施
  • 所有内核服务都通过系统调用的形式提供

(一)系统调用的意义

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

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

  2. 极大的提高了系统的安全

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

(系统调用减少了系统与硬件之间的耦合,所以极大提高了系统可移植性)

(二)操作系统提供的API和系统调用的关系

  1. 应用编程接口(API)和系统调用是不同的,使用API是为了让用户从底层硬件编程中解放出来。
  • API只是一个被封装好的函数定义
  • 系统调用通过软中断向内核发出一个明确的请求
  1. Libc库定义的一些API引用了封装例程(wrapper routine,唯一目的就是发布系统调用)使程序员在写代码时不用以汇编指令触发系统调用而是直接调用函数。
  • 一般每个系统调用对应一个封装例程
  • 库再用这些封装例程定义出给用户的API
  1. 不是每个API都对应一个特定的系统调用
  • API可能直接提供用户态的服务,例如一些数学函数没有用到系统调用
  • API与系统调用不是单一的一对一的关系
  1. 返回值
  • 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
  • 返回值-1在多数情况下表示内核不能满足进程的请求
  • Libc中定义的errno变量包含特定的出错码

(三)系统调用的三层皮

一层皮:API

二层皮:中断向量对应的中断服务程序

三层皮:系统调用对应的很多不同种类的服务程序

详细过程:

用户态进程中,xyz()函数是系统调用对应的API,该编程接口里封装了一个系统调用,会触发一个int 0x80的中断,产生向量为128的编程异常。该中断对应着内核态的内核代码入口起点system_call,执行SAVE_ALL,执行到中断服务程序sys_xyz()时,进入程序处理,该中断服务程序执行完后,会ret_from_sys_call,在ret中可能会发生进程调度,如果没发生就会iret,返回用户态,继续执行。

(四)系统调用的参数传递方法

  1. 内核实现了很多不同的系统调用, 进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数

  2. system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号。具体过程如下:

1.一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即__NR_fork)。
2.这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
3.进入sys_call之后,立即将eax的值压入内核堆栈

超过6个,就将某一个寄存器作为指针,指向内存,进入内核态后可以访问所有地址空间,即通过内存传递数据

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

(一)使用库函数API获取当前系统时间

编译:

gcc time.c -o time -m32

之后,再输入

./time

结果:

打印出的就是系统时间下的 年:月:日:时:分:秒

PS:year由于个人喜好 +1960 ,所以现在是2076,正常应该是 +1900,月份应该是3月,即 mon+1

(二)C代码中嵌入汇编代码的写法

内嵌汇编常用修饰符:

(三)用汇编方式触发系统调用获取系统当前时间

time_asm.c代码:

#include <stdio.h>
#include <time.h>
int main()
{
    time_t tt;//int型数值
    struct tm *t;
    asm volatile(
        "mov $0,%%ebx\n\t"//系统调用传递第一个参数使用ebx,这里是null
        "mov $0xd,%%eax\n\t"//传递系统调用号13(13的16进制即为d)
        "int $0x80\n\t"//发生中断
        "mov %%eax,$0\n\t"//通过eax这个寄存器返回系统调用值
        :"=m"(tt)
    );
    t = localtime(&tt);
    printf("time:%d:%d:%d:%d:%d:%d:\n",t->tm_year+1960,t->tm_mon,t->tm_mda,t->tm_hour,t->tm_min,t->tm_sec);
    return 0;
}

  1. 系统调用返回值使用eax存储,与普通函数一样。

  2. 编译:

     gcc time-asm.c -o time-asm -m32
    
     运行:
    
     . /time-asm
    
     结果与上一个代码一致
    
  3. 本段代码让我们更清楚地知道用户态对内核态做了什么

四、使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用

我的选择是!!!
第20号系统调用,getpid

1.使用库函数API:

/* getpid.c */
#include <unistd.h>
#include <stdio.h>

int main()
{
    pid_t pid;
    pid = getpid();
    printf("pid = %d \n",pid);
    return 0;
}

getpid函数用来取得目前进程的进程ID,许多程序利用取到的此值来建立临时文件,以避免临时文件相同带来的问题。

运行结果如下:

2.嵌入汇编:

/* asm_getpid.c */
#include <unistd.h>
#include <stdio.h>

int main()
{
    pid_t pid;
    pid = getpid();
    asm volatile(
            "mov $0x14,%%eax\n\t" /* 将系统调用号20放入eax中。 */
            "int $0x80\n\t" /* 中断向量号0x80,即128。int 128 执行系统调用。 */
            "mov %%eax,%0\n\t" /* 返回值保存在eax中,将它赋值给pid */
            : "=m" (pid)   
        );  
    printf("pid = %d \n",pid);
    return 0;
}

运行结果:

操作原理:getpid系统调用是第20号,所以首先要将这个系统调用号放入eax寄存器中,然后使用int 128指令执行系统调用,这时就会执行eax中的第20号系统调用。返回值是保存在eax寄存器中,所以把它赋值给0号也就是pid。

五、小结

  • 即便是最简单的程序,在进行输入输出等操作时也会需要调用操作系统所提供的服务,也就是系统调用。
  • Linux下的系统调用是通过中断(int 0x80)来实现的。
  • 在start_kernel中的trap_init将(系统调用的)中断向量和汇编代码的入口(system_call)绑定,一旦执行int指令(如int0x80),cpu就会自动跳转到绑定的汇编代码入口处执行中断服务程序,此时cpu进入内核态,在服务处理结束返回用户态之前,可能会发生进程调度,调度完成后,cpu才会返回用户态。
  • Linux 采用的是 C 语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即最后一个参数先入栈,而第一个参数则最后入栈。
posted @ 2016-03-20 00:01  20135321余佳源  阅读(302)  评论(0编辑  收藏  举报