• 深入理解系统调用


    实验要求

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

    实验内容

    环境构建:

    跟实验一的环境搭建类似,下载一些开发工具和Linux内核

    步骤一、下载内核和开发工具

    sudo apt install build-essential
    sudo apt install qemu # install QEMU
    sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
    sudo apt install axel
    axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz

    步骤二、解压Linux内核,并配置

    #解压
    xz -d linux-5.4.34.tar.xz 
    tar -xvf linux-5.4.34.tar 
    cd linux-5.4.34
    
    #配置
    make defconfig #Default configuration is based on 'x86_64_defconfig'
    make menuconfig
    #打开debug相关选项
    Kernel hacking --->
        Compile-time checks and compiler options --->
            [*] Compile the kernel with debug info #shift+y确定勾选
            [*] Provide GDB scripts for kernel debugging [*] Kernel debugging
    #关闭KASLR,否则会导致打断点失败
    Processor type and features ---->
        [] Randomize the address of the kernel image (KASLR)

    步骤三、编译内核并进行测试

    make -j$(nproc) # nproc gives the number of CPU cores/threads available
    # 测试⼀下内核能不能正常加载运⾏,因为没有⽂件系统终会kernel panic 
    qemu-system-x86_64 -kernel arch/x86/boot/bzImage  #  此时应该不能正常运行

    这一步完成后出现如下界面:

    此时还不能正常工作,需要制作根文件系统

    步骤四、安装并编译busybox制作根文件系统

    cd ..
    axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2 
    tar -jxvf busybox-1.31.1.tar.bz2 
    cd busybox-1.31.1
    
    make menuconfig 
    #记得要编译成静态链接,不⽤动态链接库。
    Settings  --->
        [*] Build static binary (no shared libs) 
    #然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。 
    make -j$(nproc) && make install

    步骤五、制作根文件系统

    cd ..
    mkdir rootfs
     cd rootfs
     cp ../busybox-1.31.1/_install/* ./ -rf
     mkdir dev proc sys home 
     sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/

    步骤六、创建init脚本文件,内容如下:

     #!/bin/sh
     mount -t proc none /proc 
     mount -t sysfs none /sys
     echo "Welcome Liwuji OS!"
     echo "--------------------" 
     cd home
     /bin/sh

    然后将init文件放在rootfs目录下

    并授予其运行权限

    chmod +x init

    步骤七、打包系统镜像,测试文件系统

    #打包成内存根⽂件系统镜像 
    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz 
    #测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本 
    cd .. qemu
    -system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz

    一切完成之后,会出现如下界面:

    可以看到init脚本已经运行了。

    到这里环境已经搭建完成。

    系统调用实验

    我的学号后两位是26,所以打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl文件,查找到系统调用号为26的是msync,函数入口为__x64_sys_msync。

    根据系统调用号编写测试代码MsyncTest.c。如下:

    #include<stdio.h>
    int main(){
        asm volatile(
            "movl $0x1a,%eax
    	"//使用EAX传递系统调用号26
            "syscall
    	"
        );
        return 0;
    }

    我们将MsyncTest.c文件放在/rootfs/home目录下,由于我们搭建的系统不支持动态链接,因此这里我们在使用gcc编译时要用-static静态编译参数)。

    gcc MsyncTest.c -o MsyncTest -static

    gdb调试

    重新执行根文件系统(重新打包,用qemu运行)

    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

    cd ..

    qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

    然后重新开一个命令行,在源码目录下启动gdb
        
    gdb vmlinux
    可能会出现下面的问题:

    只要照它的提示做就ok了

    接着在gdb中运行:
    target remote:1234

    再给对应的系统调用打上断点

    然后continue就完成了。

    实验分析

    如果一个内存区域是用来写入文件,那么被修改过的内存和文件在一段时间内将是不相同的,如果此时需要将进程写入至内存文件中,需要采用msync();函数所需头文件#include < sys/mmmap.h>

    函数结构为int msync(caddr_t addr,size_t length ,int flags);

    addr:内存映射时候返回的内存区域
    length:内存映射时所指定的长度
    flags: 指定内存和磁盘的同步方式
    MS_ASYNC:被修改过的内存区域很快就会被同步。MS_ASYNC和MS_SYNC中仅有一个同时被使用;
    MS_SYNC:在内存区域中被修改的页面在msync()系统调用返回前就被写入磁盘;
    MS_INVALIDATE:这个选项让内核决定是否要把对内存映射区域的修改写入到磁盘中。它并不确保修改的内容不会被写入,只是告诉内核不需要保存此内容;
    0:类似于MS_ASYNC 意味着如果有合适的页面需要保存的时候,内核则会将数据写入至磁盘中。

    一般说来,进程在映射空间的对共享内容的改变并不直接写回到磁盘文件中,往往在调用munmap()后才执行该操作。可以通过调用msync()实现磁盘上文件内容与共享内存区的内容一致。

    系统调用相关概念

    在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态:

    系统态(也称为管态或核心态),操作系统在系统态运行
    用户态(也称为目态),应用程序只能在用户态运行。
    在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为特权指令和非特权指令两类。

    特权指令——在系统态时运行的指令

    对内存空间的访问范围基本不受限制,不仅能访问用户存储空间,也能访问系统存储空间,
    特权指令只允许操作系统使用,不允许应用程序使用,否则会引起系统混乱。

    非特权指令——在用户态时运行的指令

    一般应用程序所使用的都是非特权指令,它只能完成一般性的操作和任务,不能对系统中的硬件和软件直接进行访问,其对内存的访问范围也局限于用户空间。

      内核中将系统调用作为一个特殊的中断来处理。系统调用是通过中断机制实现的,并且一个操作系统的所有系统调用都通过同一个中断入口来实现。在Unix/Linux系统中,系统调用像普通C函数调用那样出现在C程序中。但是一般的函数调用序列并不能把进程的状态从用户态变为核心态,而系统调用却可以做到。C语言编译程序利用一个预先确定的函数库(一般称为C库),其中有各系统调用的名字。C库中的函数都专门使用一条指令,把进程的运行状态改为核心态。Linux的系统调用是通过中断指令“INT 0x80”实现的。每个系统调用都有惟一的号码,称作系统调用号。所有的系统调用都集中在系统调用入口表中统一管理。系统调用入口表是一个函数指针数组,以系统调用号为下标在该数组中找到相应的函数指针,进而就能确定用户使用的是哪一个系统调用。不同系统中系统调用的个数是不同的,目前Linux系统中共定义了221个系统调用。另外,系统调用表中还留有一些余项,可供用户自行添加。当CPU执行到中断指令“INT 0x80”时,硬件就做出一系列响应,其动作与上述的中断响应相同。CPU穿过陷阱门,从用户空间进入系统空间。相应地,进程的上下文从用户堆栈切换到系统堆栈。接着运行内核函数system_call()。首先,进一步保存各寄存器的内容;接着调用syscall_trace( ),以系统调用号为下标检索系统调用入口表sys_call_table,从中找到相应的函数;然后转去执行该函数,完成具体的服务。执行完服务程序,核心检查是否发生错误,并作相应处理。如果本进程收到信号,则对信号作相应处理。最后进程从系统空间返回到用户空间。

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

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

    执行态切换过程:

    应用程序在用户态准备好调用参数,执行 int 指令触发软中断 ,中断号为 0x80 ;

    CPU 被软中断打断后,执行对应的中断处理函数 ,这时便已进入内核态 ;

    系统调用处理函数准备内核执行栈 ,并保存所有寄存器 (一般用汇编语言实现);

    系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;

    系统调用处理函数准备返回值并从内核栈中恢复 寄存器 ;

    系统调用处理函数执行 ret 指令切换回用户态 ;

    实验总结

    通过本次实验,我学会了使用gdb调试代码,跟进一步理解系统调用的原理,以及用户态和核心态之间的切换流程。对Linux内核有了更加深刻的了解。总而言之,这次实验让我受益匪浅。

  • 相关阅读:
    关于TensorFlow2的tf.function()和AutoGraph的一些问题解决
    voxelmorph配置
    python处理nii格式文件
    mysql总结
    JVM内存模型
    Java线程池面试
    java NIO基础
    面试日记
    PhoenixFD插件流体模拟——UI布局【Gird】详解
    PhoenixFD插件流体模拟——UI布局【Resimulation】详解
  • 原文地址:https://www.cnblogs.com/Liwj57csseblog/p/12968566.html
Copyright © 2020-2023  润新知