首页 > 技术文章 > 第四周 扒开系统调用的三层皮(上)

20135305yg 2016-03-17 13:57 原文

  • 用户态,内核态和中断

和系统调用打交道的方式:通过库函数,把系统调用给封装起来

用户态vs内核态:

      一般现代CPU都有几种不同的指令执行级别

      在高级别的状态下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态

      在相应的低级别执行状态下,代码的掌控范围会受到限制,只能在对应级别允许的范围内活动

      为什么有权限级别的划分:为了防止系统崩溃以及恶意代码的入侵,通过划分权限级别来让系统更稳定

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

      区分:在Linux中,地址空间是一个显著的标志,0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问

             (地址空间指的是逻辑地址而不是物理地址,逻辑地址:进程的地址空间里边的)

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

系统调用只是一种特殊的中断

当用户态切换到内核态时,必须保存用户态的寄存器上下文

中断/int指令会在堆栈上保存一些寄存器的值,如:用户态栈顶地址,当时的状态字,当时的cs:eip的值

中断发生后第一件事:保存现场(进入中断程序,保存需要用到的寄存器的数据)

SAVE_ALL:把其他的寄存器的值给push到内核堆栈里边去

中断处理结束前最后一件事:恢复现场(退出中断程序,恢复保存寄存器的数据)

RESTOTRE_ALL:把用户态保存的寄存器再popl出来

Iret指令与中断信号(包括int指令)发生时的CPU做的动作相反

  • 系统调用概述

系统调用概述:

      系统调用的意义

     

      API和系统调用

     

     

      应用程序,封装程序,系统调用处理程序及系统调用服务例程之间的关系

     

系统调用的三层皮: xyz(API), system_call(中断向量对应的中断服务程序), sys_xyz (不同种类的服务程序)

系统调用的服务历程:

     

系统调用的参数传递方法:

      系统调用也需要输入输出参数,例如

            实际的值

            用户态进程地址空间的变量的地址

            包含指向用户态函数的指针的数据结构的地址

           

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

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

   代码:  

time.c
#include <stdio.h>
#include <time.h>
int main()
{
    time_t tt;//int型数值
    struct tm *t;
    tt = time(NULL);
    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;
}

   通过gcc time.c -o time -m32编译后打印出当前系统的时间

用汇编方式处罚系统调用获取当前系统时间

   代码:

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(16进制即0xd)
        "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;
}

      系统调用返回值使用eax存储

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

选择24号和47号系统调用,分别获取当前用户uid(用户ID)和gid(组ID),即模拟Linux系统“id”命令。

编写两段代码,分别使用库函数API和C代码中嵌入汇编代码,源码如下:

uidgid.c(使用库函数API方式):

程序中通过调用getuid()和getgid()函数来获取当前执行用户uid和gid

uidgid_asm.c(使用C代码中嵌入汇编代码方式):

内嵌汇编代码版本源码中将原来两行通过API函数获取uid和gid的代码注释掉,用汇编代码替换。

首先将ebx寄存器清零,表示无参数传入。

然后分别将0x18和0x2f(十进制24和47)赋值给eax寄存器,表示需要调用的系统调用号,24为getuid,47为getgid。

执行int 0x80来执行系统调用。

之后eax寄存器保存了返回值,将它分别赋值给输出uid或gid变量。

完成整个汇编代码的系统调用。

分别编译两个源码文件:

分别执行系统id命令以及两个编译好的程序:

上面的截图分别表示普通用户ubuntu和管理员用户root分别执行系统自带命令id,库函数API方式uidgid,内嵌汇编方式uidgid_asm这三种方式运行得到的结果是一样的。

通过实验执行结果可知,程序成功完成了系统调用获取当前用户uid和gid的操作,通过内嵌汇编代码可以清晰的看出调用系统调用的工作过程。

首先将ebx寄存器清零,表示无参数传入。

然后分别将0x18和0x2f(十进制24和47)赋值给eax寄存器,表示需要调用的系统调用号,24为getuid,47为getgid。

执行int 0x80来执行系统调用。

之后eax寄存器保存了返回值,将它分别赋值给输出uid或gid变量。

完成整个汇编代码的系统调用。

在Linux系统中是通过激活0x80中断来触发系统调用的,需要调用的系统调用号实现赋值给eax存储器,如果有传入参数可赋值给ebx寄存器,如果多于1个则按顺序赋值给ebx、ecx、edx、esi、edi、ebp,如果超过6个则通过指针变量指向另一片堆栈区,如果无参数传入则赋值为0。

  • 总结

虽然Intel X86 CPU有4种执行级别0~3,但是在Linux系统中仅使用了0和3级,分别表示内核态和用户态。

一些涉及底层、硬件、核心的操作必须在内核态下才允许执行,为操作系统程序和驱动程序专享,普通程序仅能执行在用户态下。如果普通程序需要涉及内核态的操作,就需要通过系统调用来实现。这样做的好处是屏蔽平台相关操作降低了软件开发难度,增强了系统安全性,使程序具有更好的移植性(Linux系统及其他Unix系统遵循统一标准,系统调用基本一样)。

推荐阅读