《Linux内核分析》第四周学习笔记 扒开系统调用的三层皮(上)

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

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

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

1. 通过库函数完成系统调用:库函数将系统调用封装起来。

2. 用户态与内核态

  • 内核态:一般现代CPU有几种指令执行级别。在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别对应着内核态
  • 用户态:在相应的低级别执行状态下,代码的掌控范围有限,只能在对应级别允许的范围内活动
  • 如intel x86 CPU有四种不同的执行级别0-3,Linux只使用0级表示内核态,3级表示用户态
  • 权限级别的划分使系统更稳定

3. 区分用户态与内核态的方法

(主要是通过代码段选择寄存器cs和偏移量寄存器eip)

  • cs寄存器的最低两位表明了当前代码特权级
  • CPU每条指令的读取都是通过cs:eip这两个寄存器

(上述两个判断由硬件完成)

  • 一般在Linux中,(逻辑)地址空间是显著标志:0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都能访问

注:逻辑地址是进程的地址空间中的。

4. 中断处理

  • 中断处理是从用户态进入内核态的主要方式
  • 系统调用只是一种特殊的中断
  • 寄存器上下文,从用户态切换到内核态时:必须保存用户态的寄存器上下文,同时将内核态的寄存器相应的值放入当前CPU
  • 中断/int指令会在堆栈上保存一些寄存器的值:如用户态栈顶地址、当前的状态字、当时cs:eip的值(当前中断程序的入口)

5. 保护现场与恢复现场

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

    #define SAVE_ALL  //将其他寄存器的值push到内核堆栈中
  • 恢复现场:退出中断程序,恢复保存寄存器的数据(中断处理结束前最后一件事)

    #RESTORE_ALL  //将用户态保存的寄存器pop到当前CPU中
  • iret指令:iret指令与中断信号(包括int指令)发生时的CPU的动作相反

6. 中断处理的完整过程

  • 第一步

    interrupt(ex:int 0x80)-save //int 0x80指系统调用
    cs:eip/ss:esp/eflags(current)to kernel stack,then //中断将cs:eip、ss:esp(当前堆栈段栈顶)、eflags(当前标志寄存器)保存到内核堆栈中
    load cs:eip(entry of a specific ISR)and //将当前中断信号相关联的中断服务入口加载到cs:eip
    ss:esp(point to kernel stack). //同时将当前指向内核信息的的堆栈段和esp也加载到CPU中
  • 第二步

    SAVE_ALL
    -... //内核代码,完成中断服务,(完成中断服务后可能)发生进程调度
    //如果发生了进程调度,则当前的状态都会暂时保存在系统中。当其他进程调度切换回当前进程时,则接着执行RESTORE_ALL
  • 第三步

    RESTORE_ALL
  • 第四步

    iret -pop cs:eip/ss:esp/eflags from kernel stack

二、系统调用概述

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

1. 系统调用概述

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

  • 把用户从底层的硬件编程中解放出来
  • 极大的提高了系统的安全性
  • 使用户程序具有可移植性(用户程序与具体硬件被抽象的接口替代,没有非常紧密的关系)

2. API和系统调用

  • 应用程序接口(API)与系统调用不同

    • API只是一个函数定义
    • 系统调用通过软件中断trap向内核发出一个明确的请求
  • Libc库定义的一些API引用了封装例程(唯一目的是发布系统调用,直接调用函数就可以出发系统调用)
    • 一般每个系统调用对应一个封装例程
    • 库再用这些封装例程定义出给用户的API
  • 不是每个API都对应一个特定的系统调用
    • API可能直接提供用户态的服务,如一些数学函数
    • 一个单独的API可能调用几个系统调用
    • 不同的API可能调用了同一个系统调用
  • 返回值
    • 大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用
    • -1在多数情况下表示内核不能满足进程的请求
    • Libc中定义的errno变量包含特定的出错码

用户态<->内核态

3. 系统调用的三层皮

系统调用的三层皮:xyz(API)、system_ call(中断向量)、sys_xyz(中断向量对应的中断服务程序)

4. 系统调用程序及服务例程

  • 当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数

    • 在Linux中是通过执行int $0x80来执行系统调用的, 这条汇编指令产生向量为128的编程异常
    • Intel Pentium II中引入了sysenter指令(快速系统调用),2.6已经支持
    • (系统调用号将xyz和sys_xyz关联起来)
  • 传参:内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为系统调用号的参数
    • 使用eax寄存器

5. 参数传递

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

    • 实际的值
    • 用户态进程地址空间的变量的地址
    • 甚至是包含指向用户态函数的指针的数据结构的地址
  • system_call是linux中所有系统调用的入口点,每个系统调用至少有一个参数,即由eax传递的系统调用号
    • 一个应用程序调用fork()封装例程,那么在执行int $0x80之前就把eax寄存器的值置为2(即_NRfork)。
    • 这个寄存器的设置是libc库中的封装例程进行的,因此用户一般不关心系统调用号
    • 进入sys_call之后,立即将eax的值压入内核堆栈
  • 寄存器传递参数具有如下限制:
    • 每个参数的长度不能超过寄存器的长度,即32位
    • 在系统调用号(eax)之外,参数的个数不能超过6个(ebx,ecx,edx,esi,edi,ebp)
    • 超过6个则将某一个寄存器作为一个指针指向一块内存,进入内存态后可以访问所有地址空间,可以通过那块内存传递数据

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

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

  • C代码time.c :

    #include <stdio.h>
    #include <time.h>
    int main()
    {
    time_t tt;//int型数值
    struct tm *t; //便于输出值可读
    tt = time(NULL);
    t = localtime(&tt);//将tt转换成之前声明的t类型,便于可读
    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
    • 结果:输出系统时间的年:月:日:时:分:秒

3.2 C代码中嵌入汇编代码的写法

详细语法见http://www.cnblogs.com/20135228guoyao/p/5243214.html

3.3 使用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"//使用%eax传递系统调用号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;
    }
    • 编译: gcc time-asm.c -o time-asm -m32
    • 运行: ./time-asm
    • 结果:输出系统时间的年:月:日:时:分:秒(执行结果与C代码一样)

四、实验

  使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用。本次实验中我使用第20号系统调用getpid()函数,用于取得进程识别码。

  • C代码(getpid.c):

    #include <unistd.h>
    #include <stdio.h> int main()
    {
    pid_t pid;
    pid = getpid();
    printf("pid = %d \n",pid);
    return 0;
    }
    • 编译及运行结果见下图:

  • 嵌入汇编代码getpid_asm.c:

    #include <unistd.h>
    #include <stdio.h> int main()
    {
    pid_t pid;
    pid = getpid();
    asm volatile(
    "mov $0,%%ebx\n\t" //系统调用传递第一个参数使用ebx,因为这里没有传入参数所以是null
    "mov $0x14,%%eax\n\t" //使用%eax传递系统调用号20,用16进制表示为0x14
    "int $0x80\n\t" //执行系统调用
    "mov %%eax,%0\n\t" //将%0(即pid的返回值)放到%eax寄存器中
    : "=m" (pid)
    );
    printf("pid = %d \n",pid);
    return 0;
    }
    • 编译及运行结果见下图:

五、总结

  系统调用是操作系统为用户态进程与硬件设备进行交互提供的一组接口,也是一种特殊的中断,可使用户态切换到内核态。当用户态进程调用一个系统调用时,CPU切换到内核态并开始执行一个内核函数。同时我通过实践掌握了用户态进程切换到内核态的具体过程:使用ebx传递系统调用第一个参数、使用eax传递系统调用号、int $0x80指令执行系统调用、最后将返回值存入eax。

《Linux内核分析》第四周学习笔记的更多相关文章

  1. linux内核分析第四周学习笔记

    linux内核分析第四周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  2. 20135320赵瀚青LINUX内核分析第四周学习笔记

    赵瀚青原创作品转载请注明出处<Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 概述 本周的内容主要是讲解系 ...

  3. Linux内核分析——第四周学习笔记20135308

    第四周 扒开系统调用的“三层皮” 一.内核.用户态和中断 (一)如何区分用户态.内核态 1.一般现在的CPU有几种不同的指令执行级别 ①在高级别的状态下,代码可以执行特权指令,访问任意的物理地址,这种 ...

  4. Linux内核分析——第四周学习笔记

    扒开系统调用的三层皮[上] 前言:以下笔记除了一些讲解视频中的概念记录,图示.图示中的补充文字.总结.分析.小结部分均是个人理解.如有错误观点,请多指教! 补充:[系统调用的参数传递方法]视频中讲解简 ...

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

    LINUX内核分析第四周学习总结--扒开系统调用的"三层皮" 标签(空格分隔): 20135321余佳源 余佳源 原创作品转载请注明出处 <Linux内核分析>MOOC ...

  6. Linux内核分析第二周学习笔记

    linux内核分析第二周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  7. linux内核分析第一周学习笔记

    linux内核分析第一周学习笔记 标签(空格分隔): 20135328陈都 陈都 原创作品转载请注明出处 <Linux内核分析>MOOC课程http://mooc.study.163.co ...

  8. Linux内核分析第四周学习总结——系统调用的工作机制

    Linux内核分析第四周学习总结--系统调用的工作机制 内核态 执行级别高,可以执行特权指令,访问任意物理地址,在intel X86 CPU的权限分级为0级. 用户态 执行级别低,只能访问0x0000 ...

  9. LINUX内核分析第四周学习总结——扒开应用系统的三层皮(上)【转】

    转自:http://www.cnblogs.com/lalacindy/p/5276874.html 张忻(原创作品转载请注明出处) <Linux内核分析>MOOC课程http://moo ...

  10. Linux内核分析第四周学习总结

    朱国庆+原创作品转载请注明出处 + <Linux内核分析>MOOC课程http://mooc.study.163.com/course/USTC-1000029000 扒开系统调用的三层皮 ...

随机推荐

  1. FIO_工具_专业

    一.FIO工具安装: 1.查看fio是否安装 [root@localhost /]#rpm –qa|grep fio 2.源码安装(推荐) 官网地址:http://freecode.com/proje ...

  2. 3.HBase In Action 第一章-HBase简介(1.1.1 大数据你好呀)

    Let's take a closer look at the term Big Data. To be honest, it's become something of a loaded term, ...

  3. The Rock Game

    Before the cows head home for rest and recreation, Farmer John wantsthem to get some intellectual st ...

  4. Spring 加载Controller逻辑的源码笔记

    org.springframework.web.servlet.handler.AbstractHandlerMethodMapping#initHandlerMethods 进行加载Controll ...

  5. php可逆加密解密函数

    很多PHP程序员调试使用echo.print_r().var_dump().printf()等,虽然对于有较丰富开发经验的程序员来说这些也已经足够了,他们往往可以在程序执行的过程中,通过输出特定变量的 ...

  6. CentOS下iptables详解

    一:前言   防火墙,其实说白了讲,就是用于实现Linux下访问控制的功能的,它分为硬件的或者软件的防火墙两种.无论是在哪个网络中,防火墙工作的地方一定是在网络的边缘.而我们的任务就是需要去定义到底防 ...

  7. day2-课堂笔记

    #面向对象 函数=方法 系统内建函数:len().id() 对象函数

  8. MP实战系列(十一)之封装方法详解(续一)

    之前写的封装方法详解,比较简要. 今天我主要讲增加和删除及其修改.查的话得单独再详讲. 增删改查,无论是Java或者C#等等,凡是对数据库操作的都离不开这四个. 一.增加方法讲解 MyBatis Pl ...

  9. 'utf-8' codec can't decode byte 0xbc in position 1182: invalid start byte

    2.如果是字符集出现错误,建议多选择几种字符集测试一下: 选择的经验是: 如果是爬取到的网页文件,可以查看网页文件的meta标签下的charset属性值.例如: <meta charset=&q ...

  10. .NET Core中向已存在文件的特定位置写入数据

    本例使用.NET Core向一个文本文件中的特定位置写入数据,来模拟文件上传中的断点续传是如何在服务器端实现的. 新建一个.NET Core控制台项目FileContinueToWrite,其Prog ...