跟踪分析Linux内核5.0系统调用处理过程

2022-10-25 15:32:40 浏览数 (2)

跟踪分析Linux内核5.0系统调用处理过程

  • 实验要求
  • 实验环境
  • 实验步骤
    • 一、下载Linux内核5.0并编译
    • 二、挂载 menuOS
    • 三、跟踪分析系统调用函数`sys_sync`和`sys_syncfs`
  • 实验分析
  • 实验结论
    • 一、系统调用流程
    • 二、执行态切换过程

原创作品转载请注明出处https://github.com/mengning/linuxkernel/

作者:136


实验要求

代码语言:javascript复制
实验:举例跟踪分析Linux内核5.0系统调用处理过程
	编译内核5.0
	qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
	选择系统调用号 "36" 的系统调用进行跟踪分析
	https://github.com/mengning/menu
	给出相关关键源代码及实验截图

实验环境

  • Linux 4.15.0
  • Ubantu 18.04.2
  • gcc 7.3.0
  • Windows 10 VMware Workstation

实验步骤

一、下载Linux内核5.0并编译

  1. 下载Linux内核5.0 从Linux Core 5.0 Source Code中下载相应源码压缩包。
代码语言:javascript复制
$ wget https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar.xz

解压缩下载的压缩包。

代码语言:javascript复制
$ xz -d linux-5.0.1.tar.xz    #-d解压缩,解压缩后压缩包消失
$ tar -xvf linux-5.0.1.tar    #-x解压缩,-v显示过程,-f指定文件名

此时,在当前目录下创建了一个linux-5.0.1的文件夹,进入该文件夹。

代码语言:javascript复制
$ cd linux-5.0.1
  1. 安装内核编译工具并编译 安装内核编译工具build-essentialflex bisonlibssl-devlibelf-devlibncurses-dev。如果不想在安装中途弹出命令行交互信息,可以加上命令-y
代码语言:javascript复制
$ sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev

依次配置内核编译工具,这里可以根据自身情况选择若干个命令加入系统".config"文件,并调出调整窗口来设置允许断掉调试。这里使用默认的 32 位配置文件,输入make -j4进行编译,让make最多允许4个编译命令同时执行,这里的参数不多于两倍的本机内核数。

代码语言:javascript复制
#$ make defconfig         #按照默认值生成.config 
$ make i386_defconfig     #生成32位x86的配置⽂文件 
#$ make config            #遍历选择编译内核功能 
#$ make allyesconfig      #启⽤内核全部功能 
#$ make allnoconfig       #内核功能选项全部为否 
$ make menuconfig         #开启文本菜单选项,对窗口有限制,尽量调⼤窗⼝
#$ xrandr -s 1920x1440   #调整窗口大小,xrandr可以查看有哪些值可以选择
$ make -j4               #or make -j*,*为cpu核⼼数

==题外话:==我在修改完.config文件后的依次启动 Ubantu 时,跳出启动时出现'SMBus Host Controller not enabled'错误提示,进不到图形界面,可以参考这篇文章来在开机时进入终端窗口,编辑blacklist.conf文件,禁止i2c_piix4驱动的加载。

二、挂载 menuOS

根文件系统首先是内核启动时所 mount(挂载)的第一个文件系统,内核代码映像文件保存在根文件系统中,而系统引导启动程序会在根文件系统挂载之后从中把一些基本的初始化脚本和服务等加载到内存中去运行。

1. 下载 menuOS 代码 首先,在linux-5.0.1目录的上一级下载menuOS的代码,这里选择从GitHubclone下来。然后进入menu目录,安装在 64 位环境下编译 32 位的工具libc6-dev-i386

代码语言:javascript复制
$ git clone https://github.com/mengning/menu.git 
$ cd menu 
$ sudo apt-get install libc6-dev-i386 # 在64位环境下编译32位需安装 

2. 设置 rootfs 我们下载下来的menu文件夹里是针对Linux-3.18.6的,我们需要修改其Makefile文件,在menu文件夹下进行rootfs编译,回到上一级目录发现已经生成了rootfs.img文件,说明编译挂载成功。

代码语言:javascript复制
$ make rootfs    #如果不想使用makefile直接编译,可以自己创建一个rootfs文件,拷贝init文件,并参照makefile中的相关命令

3. 启动 menuOS 实际上,Makefile中是使用qemu-system-i386来启动 32 位的linux-5.0.1内核的menuOS。如图,出现menuOS的界面,挂载成功。

代码语言:javascript复制
$ qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img

三、跟踪分析系统调用函数sys_syncsys_syncfs

1. 准备工作 为了能够进行断点跟踪,需要我们在make menuconfig打开图形界面,选中kernel-hackcompile-time checks and compiler optionscompile the kernel with debug info

2. 查找需要跟踪的系统函数 首先通过查询系统中/usr/include/asm/unistd_32.h文件,获取与学号对应的系统调用号及其所对应的函数sync(),并且Linux-5.0.1内核中实现了该函数。

代码语言:javascript复制
$ cd '/usr/include/asm' 
$ vim unistd_32.h

3. 添加sync()函数至test.c文件中 打开menu文件夹下test.c文件,向其中加入一个调用sync()函数的函数Sync()并可视化输出,为了方便起见,在main()函数中添加help交互信息。

这里的sync()函数是 void 类型,并且和syncfs()函数关系密切,故研究了两个函数。保存修改后的test.c()文件,重新编译menu文件夹 $ make rootfs,可以看到成功输出了相应的信息。

4. 进行gdb调试 根据文件存放的位置,在rootfs.img所在的目录下开启两个终端分别执行如下命令。

代码语言:javascript复制
$ qemu -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
$ gdb vmlinux
(gdb)b sys_sync   #在系统函数sys_sync处设置断点
(gdb)b sys_syncfs #在系统函数sys_syncfs处设置断点
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)c            #continue,跳到端点处
(gdb)s            #step,一步步调试

如图,设置了两个断点,并启动menuOS

menuOS中输入sync,调用我们写好的sync()函数,相应的在 gdb 调试中进入了系统函数sys_sync()的断点处,函数位于sync.c文件的 123~375 行。

同理,进入下一个断点继续调试,可以看出系统调用函数sys_syncfs位于sync.c的 160~375 行。


实验分析

进一步分析系统调用函数sys_syncsys_syncfs,通过查阅资料我们发现,syncsyncfs起作用的是文件系统缓存,这些缓存是在内核空间管理的。sync会把对文件系统的元数据、缓存的文件数据写入所有底层的文件,对所有文件系统有用。syncfs 需要一个文件描述符,只写入文件描述符指向的文件所在的文件系统上的数据。

代码语言:javascript复制
定义:
    #include <unistd.h>
    void sync(void);
    int syncfs(int fd);

    sync():
        _XOPEN_SOURCE >= 500
        || /* Since glibc 2.19: */ _DEFAULT_SOURCE
        || /* Glibc versions <= 2.19: */ _BSD_SOURCE

    syncfs():
        _GNU_SOURCE
        
描述:sync() causes all pending modifications to filesystem metadata and
     cached file data to be written to the underlying filesystems.

     syncfs() is like sync(), but synchronizes just the filesystem
     containing file referred to by the open file descriptor fd.
     
返回值:sync()总是成功的;
      syncfs()成功时返回1,失败时返回-1,并设置errno来描述错误。
      错误代码: EBADF——文件描述符无效,或文件已关闭;
               EIO——读写的过程中发生错误;
               EROFS、EINVAL——文件所在的文件系统不支持同步。

分别单步进入s、单步跳过n、进入下一个断点c的操作,观察调用栈情况bt,如图可知。 当main()函数使用sync()函数时,寄存器的位置发生了变化,并保存了入口现场,待调用结束后返回。单步进入执行,寄存器只是单纯的在代码行中向下移动一行。当函数调用结束,此时用来存放结果的变量已经获得了值,函数位置回到了main()函数并继续执行。

系统调用的工作机制是:当用户态进程调用一个系统调用时,CPU 切换到内核态并开始执行一个内核函数,由 API、中断向量和中断处理程序协调完成。


实验结论

一、系统调用流程

我们以一个假设的系统调用xyz 如图,系统调用执行的流程如下:

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

在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。系统调用表同理。

Linux 通过软中断实现从用户态内核态的切换。用户态内核态是独立的执行流,因此在切换时,需要准备执行栈并保存寄存器。 内核实现了很多不同的系统调用(提供不同功能),而系统调用处理函数只有一个。 因此,用户进程必须传递一个参数用于区分,这便是系统调用号( system call number )。 在 Linux 中,系统调用号一般通过 eax 寄存器来传递。

二、执行态切换过程

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

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

0 人点赞