zoukankan      html  css  js  c++  java
  • 深入理解系统调用

    实验目的

    • 找一个系统调用,系统调用号为学号最后2位相同的系统调用,即05的系统调用fstat
    • 通过汇编指令触发该系统调用
    • 通过gdb跟踪该系统调用的内核处理过程
    • 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化

    调用流程

    在应用程序内,调用一个系统调用的流程是怎样的呢?

    我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

    操作系统通过系统调用为运行于其上的进程提供服务。

    当用户态进程发起一个系统调用, CPU 将切换到 内核态 并开始执行一个 内核函数 。 内核函数负责响应应用程序的要求,例如操作文件、进行网络通讯或者申请内存资源等。

    如上图,系统调用执行的流程如下:

    1. 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
    2. 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
    3. CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
    4. 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;

    执行态切换

    应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。

    Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。

    内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。

    总结起来, 执行态切换 过程如下:

    1. 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
    2. CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
    3. 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
    4. 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
    5. 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
    6. 系统调用处理函数 执行 ret 指令切换回 用户态 ;

    实验步骤

    下载并编译内核

    wget wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.2tar.xz

    xz -d linux-5.0.2.tar.xz

    tar -xvf linux-5.0.2.tar

    git clone https://github.com/mengning/menu.git

    安装编译工具

    sudo apt install build-essential flex bison libssl-dev libelf-dev   libncurses-dev

    配置内核

    cd linux-5.0.2
    make i386_defconfig
    make menuconfig
    make -j 8

     进入Menu目录,修改Makefile文件
    将内核版本改为对应的版本
    编译make rootfs 生成一个镜像文件 rootfs.img
    启动该镜像

    qemu-system-i386 -kernel arch/x86/boot/bzImage -initrd rootfs.img

     跟踪系统调用分析

    qemu-system-i386 -kernel linux-5.0.2/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
    cd linuxkernel/linux-5.0.2
    
    gdb
    
    (gdb)vmlinux
    
    (gdb) target remote:1234

    跟踪测试函数

    #include <errno.h>
    #include <stddef.h>
    #include <sys/stat.h>
    
    #include <sysdep.h>
    #include <sys/syscall.h>
    
    /* Get information about the file FD in BUF.  */
    int
    __fxstat (int vers, int fd, struct stat *buf)
    {
      if (vers == _STAT_VER_KERNEL || vers == _STAT_VER_LINUX)
        return INLINE_SYSCALL (fstat, 2, fd, buf);
    
      __set_errno (EINVAL);
      return -1;
    }
    
    hidden_def (__fxstat)
    weak_alias (__fxstat, _fxstat);
    #undef __fxstat64
    strong_alias (__fxstat, __fxstat64);
    hidden_ver (__fxstat, __fxstat64)

    使用嵌入汇编的方式把系统调用展示如图:

    可以看出,系统调用执行了内核的封装例程。
    用户进程只需要把相应的调用号放入eax寄存器,内核就在内核态完成相应的计算,把用户所要求的结果返给用户进程,参与其他运算。

    实验总结
    当调用一个系统调用时,CPU从用户态切换到内核态并执行一个system_call和系统调用内核函数。在Linux中通过执行int 0x80来触发系统调用,内核为每个系统调用分配一个系统调用号,用户态进程必须明确指明系统调用号,并使用EAX寄存器来传递。
    系统调用可能需要参数,但是不能通过像用户态进程函数中将参数压栈的方式传递,因为用户态和内核态有不同的堆栈,必须使用寄存器EBX、ECX、EDX、ESI、EDI、EBP来传递参数。若参数较多,则把指向内存的指针存入寄存器。

  • 相关阅读:
    BZOJ2809: [Apio2012]dispatching
    BZOJ1455: 罗马游戏
    可并堆试水--BZOJ1367: [Baltic2004]sequence
    可并堆模板
    Codeforces870F. Paths
    Codeforces913F. Strongly Connected Tournament
    一练Splay之维修数列第一次
    Codeforces913E. Logical Expression
    Codeforces700C. Break Up
    可持久化KMP
  • 原文地址:https://www.cnblogs.com/xqqu/p/12966631.html
Copyright © 2011-2022 走看看