zoukankan      html  css  js  c++  java
  • 第四周 扒开系统调用的三层皮(上)

    • 用户态,内核态和中断

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

    用户态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:
    ",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
    	"//系统调用传递第一个参数使用ebx,这里是null
            "mov $0xd,%%eax
    	"//传递系统调用号13(16进制即0xd)
            "int $0x80
    	"
            "mov %%eax,$0
    	"//通过eax这个寄存器返回系统调用值,和普通函数一样
            :"=m"(tt)
        );
        t = localtime(&tt);
        printf("time:%d:%d:%d:%d:%d:%d:
    ",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系统遵循统一标准,系统调用基本一样)。

  • 相关阅读:
    记一次文件转码与二进制查看学习
    JAVA线程池 之 Executors (二) 原理分析
    JAVA线程池 之 Executors (一) 简介
    问题:部分mysql版本问题
    Java中一个方法字节码的长度会影响程序并发下的性能?
    AccessController.doPrivileged
    Nginx配置WebService、MySQL、SQL Server、ORACLE等代理
    C# 序列化Json时如何忽略JsonProperty(PropertyName =“ someName”)
    C# 文件上传(另一台服务器的共享目录)
    C# 导入Excel读取图片上传
  • 原文地址:https://www.cnblogs.com/20135305yg/p/5277195.html
Copyright © 2011-2022 走看看