《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. Spring IOC容器创建bean过程浅析

    1. 背景 Spring框架本身非常庞大,源码阅读可以从Spring IOC容器的实现开始一点点了解.然而即便是IOC容器,代码仍然是非常多,短时间内全部精读完并不现实 本文分析比较浅,而完整的IOC ...

  2. BZOJ3578:GTY的人类基因组计划2(集合hash,STL)

    Description GTY召唤了n个人来做实验,GTY家的房子很大,有m个房间一开始所有人都在1号房间里,GTY会命令某人去某个房间等待做实验,或者命令一段区间的房间开始实验,实验会获得一些实验信 ...

  3. 2298: [HAOI2011]problem a

    Description 一次考试共有n个人参加,第i个人说:"有ai个人分数比我高,bi个人分数比我低."问最少有几个人没有说真话(可能有相同的分数) 这句话的意思就等于是说第i个 ...

  4. node.js cheerio API

    安装 npm install cheerio load var cheerio = require('cheerio'), $ = cheerio.load('<ul id=“fruits”&g ...

  5. Node.js实战(一)之概述

    , 一.Node.js介绍 Node.js是一个Javascript运行环境(runtime environment),发布于2009年5月,由Ryan Dahl开发,实质是对Chrome V8引擎进 ...

  6. stm32中assert_param的用法说明

    stm32中assert_param的用法说明   首先是要知道条件判断语句 这个运算符分成三部分: (条件) ? (条件成立执行部分) :(条件不成立执行部分) 就这么简单 例如:a=(x>y ...

  7. Base64Util 工具类

    package com.org.utils; import java.io.ByteArrayOutputStream; public class Base64Util { private stati ...

  8. 20155320《网络对抗》Exp2 后门原理与实践

    20155320<网络对抗>Exp2 后门原理与实践 [实验内容] (3.5分) (1)使用netcat获取主机操作Shell,cron启动 (2)使用socat获取主机操作Shell, ...

  9. WPF编程,窗口保持上次关闭时的大小与位置。

    原文:WPF编程,窗口保持上次关闭时的大小与位置. 版权声明:我不生产代码,我只是代码的搬运工. https://blog.csdn.net/qq_43307934/article/details/8 ...

  10. P4385 [COCI2009]Dvapravca

    首先特判掉蓝点数量\(<2\)的情况.没有蓝点答案就是\(n\),有一个蓝点可以枚举一个红点,选择过这个蓝点和红点的一条线和在无穷远处的平行线(即这条线对应的两个半平面). 这里认为过一个点是与 ...