深入理解系统调用——linux内核分析第二次作业

一、   搭建Linux内核调试环境

1.1   下载linux内核源码、配置内核选项、编译运行

   我这里的根目录为~/linux,所有操作按照老师所给课件按步骤执行即可,具体命令就不在这里赘述了。具体步骤为:

  1.1.1    下载内核源码

  1.1.2    配置内核选项

        打开Compile the kernel with debug info、Provide GDB scripts for kernel debugging 和Kernel debugging。关闭Randomize the address of the kernel image (KASLR),如下图所示

  1.1.3   编译运行内核 

    make后使用qemu加载启动镜像,能够加载,由于没有⽂件系统最终会kernel panic     如下图所示:

 

1.2   借助busybox制作根文件系统

  1.2.1安装busybox

        a) 下载busybox源码

        b) 配置编译选项

        c) 打开静态链接选项——Build static binary (no shared libs),如下图所示

    

          d) 编译后安装

  1.2.2 制作内存根⽂件系统镜像

    其中包括准备init脚本⽂件,打包成内存根⽂件系统镜像:

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

    制作完成后的目录结构:

  

   测试根文件系统,看内核启动完成后是否执⾏init脚本:

   打印出了我们写进init中的字符:TestOS, 执行成功,内存根文件系统制作完毕

1.3 使⽤gdb跟踪调试Linux内核

  首先启动qemu模拟器:qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S –s,此时qemu会处于stoped状态

  同目录下再启动一个terminal,并进入源代码目录进入~linux/linux 5.4.34/

  执行以下命令(后三个是gdb命令):

       加载内核符号表:gdb vmlinunx

       建立连接:target remote:1234

       打上断点:break start_kernel

       执行:c

       发现出现了Remote ‘g’ packet reply is too long的错误 —— gdb从qemu接收回来的g包大于gdb定义的g包的大小

  

   查阅相关资料,暂时没找到稍微简单的解决方法,只有卸载当前gdb,重新下载gdb的源码,修改其中某处的代码然后重新编译并安装,具体过程: 

  a) 卸载gdb

  sudo apt-get --purge remove gdb

  b)     下载某一版本的gdb源码并解压

  wget http://ftp.gnu.org/gnu/gdb/gdb-8.0.1.tar.gz

  c)     找到gdb-8.0.1/gdb/remote.c源文件,将

if (buf_len > 2 * rsa->sizeof_g_packet) error (_("Remote 'g' packet reply is too long: %s"), rs->buf);

  修改为:

if (buf_len > 2 * rsa->sizeof_g_packet) {
     rsa->sizeof_g_packet = buf_len ; 
     for (i = 0; i < gdbarch_num_regs (gdbarch); i++) {
         if (rsa->regs->pnum == -1) 
            continue; 
         if (rsa->regs->offset >= rsa->sizeof_g_packet) 
            rsa->regs->in_g_packet = 0; 
        else 
            rsa->regs->in_g_packet = 1; 
       } 
}        

  d)     重新make并安装,具体为:

  1      ./configure

  2       make

  3       sudo make install

  重新尝试:

  

  成功捕捉到断点。

 

二、   步入正题——系统调用

2.1 系统调用的理论基础

  首先,我们需要弄清楚从用户态通过系统调用进入内核态的这一完整流程。不妨先看一下有关这一过程的部分名词定义:

  用户态:指非特权状态。在此状态下,执行的代码被硬件限定,不能进行某些操作。

  内核态:是操作系统内核所运行的模式,运行在该模式的代码,可以无限制地对系统存储、外部设备进行访问

  系统调用:为了让应用程序有能力访问系统资源,每个操作系统都提供了一套接口,以供应用程序使用,这就是系统调用。它规定了用户进程进入内核的具体位 置。它本身并非内核函数,但它是由内核函数实现,进入内核后,不同的系统调用会找到各自对应的内核函数(根据系统调用号),这些内核函数被称为系统调用的“服务例程”。

  API:即应用程序接口 ,是程序员在用户空间下可以直接使用的函数接口。是一些预定义的函数,比如常用的read()、malloc()、free()、abs()函数等,这些函数都具有一定功能,说明了如何获得一个给定的服务,跟内核没有必然的联系。

  系统调用和api的区别api是函数的定义,规定了这个函数的功能,跟内核无直接关系。而系统调用是通过中断向内核发请求,实现内核提供的某些服务。有时候,某些API所提供的功能会涉及到与内核空间进行交互。那么,这类API内部会封装系统调用。而不涉及与内核进行交互的API则不会封装系统调用。

  在弄清了相关概念后,我们通过下面的图来系统地、动态地了解从api到内核函数这一过程:

 

  再从代码层面抽象地看一下应⽤程序、封装例程、系统调⽤处理程序及系统调⽤服务例程之间的关系:

  

  首先要指出,传统的32位linux内核使用int 0x80中断指令触发中断,这种方法进行查询入口地址时会多次访问内存,操作较为复杂、耗时。因此引入了sysenter(32位)和syscall(64位)快速系统调用,这种机制将我们所需要的系统调用处理程序入口的地址信息存入寄存器,速度增快,查询时速度增快。由于我们的系统内核是64位,因此我们专注于syscall指令(如上图)

  从上图我们可以看到,在用户态下调用xyz()(api)时,当运行到SYSCALL这条指令时,跳转到entry_SYSCALL_64,这是liunx系统中所有系统调用的入口点。entry_SYSCALL_64不是一段普通的函数,它是一段汇编代码,同时,我们将系统调用号用rax寄存器进行传递,entry_SYSCALL_64通过系统调用号来查询对应的内核处理函数并跳转到相应的内核处理函数执行,完毕后再按顺序逐步返回到用户态。

  那么,SYSCALL指令是如何知晓entry_SYSCALL_64的所在之处的呢?这是在内核的初始化过程完成的。

  内核的初始化完成了以下的函数调用过程:

  start_kernel > trap_init > cpu_init > syscall_init, 其中start_kernal是内核启动的入口函数。而syscall_init 中的如下代码: 

wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

  使得entry_SYSCALL_64在内核初始化的过程中被加载到了msr寄存器中。在后续的执行过程中,一旦出现了SYSCALL这条指令,就会直接去访问msr寄存器。

  知道了这一过程,我们需要在gdb环境中证明这一论断,并追踪整个系统调用的过程。在此之前,显然我们首先要成功触发一次系统调用,并给该系统调用所对应的内核处理函数打上断点,通过逐步执行弄清这一流程。

2.2 触发69号系统调用

  我的学号的末尾为69,查阅syscall_64.tbl文件:

  

   可知69号系统调用为msgsnd,他的作用是在消息队列上进行收发消息。对应的内核处理函数为__x64_sys_msgsnd。我们需要写一个小程序来触发它。

  由于我们只需要弄清系统调用的过程,因此不必在乎某一特定的系统调用所传递的除系统调用号以外的其他参数、返回值以及它具体执行的功能等等,我们只需要专注于触发这一系统调用就好了,因此我们使用内联汇编小程序new_test.c可以这么写:  

int main()
{
  asm volatile(
  "movl $0x45,%eax\n\t" //使⽤EAX传递系统调⽤号69
  "syscall\n\t" //触发系统调⽤ );
  return 0;
}

  ok,接下来编译它,由于我们搭建的系统不支持动态链接,因此这里我们在使用gcc编译时要用-static静态编译参数)。

  然后将生成的可执行文件文件拷贝至rootfs/home文件夹下,最后,由于我们对系统做了修改,因此要再次使用命令:

    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
  重新打包成内存根文件系统镜像。

  打包完毕后,我们的构建系统的根目录下应该已经有new_test这一可执行文件了,用qemu运行,检查一下。如下图所示:

  

   new_test这一旨在触发69号系统调用的程序已经被植入于我们构建的系统中。

  接下来我们首先需要检查是否成功触发了69号系统调用,然后逐步跟踪了解内核的处理过程、系统调用入口的保存现场、恢复现场和系统调用返回等流程

 

三、 使用gdb跟踪调试

3.1 通过断点捕获情况检查是否成功触发69号系统调用

  按照1.3提到的,我们加载系统镜像:

    qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s

  同时,运行gdb,进入调试环境。

  根据前面提到的,69号系统调用的内核处理函数为__x64_sys_msgsnd,因此我们给它打上断点: b __x64_sys_msgsnd,如下图

  并continue,果然,qemu进入stoped,gdb捕获到了断点 —— __x64_sys_msgsnd函数,69号系统调用触发成功。如下图:

 

 3.2 验证内核初始化所做的工作

  前面提到,内核的初始化完成了以下的函数调用过程:start_kernel > trap_init > cpu_init > syscall_init, 并在syscall_init中完成将entry_SYSCALL_64的入口地址存入msr寄存器中,这里我们用gdb来调试验证以下,其实也就是分别给这几个函数打上断点,检测断点捕获情况和执行顺序。

  依次break这些函数的函数名后执行:

  

 

  上面的图清晰地告诉我们,内核确实是按我们所提到的顺序执行。

 3.3 关注系统调用的保存现场和恢复现场

  正如2.1所提到的,entry_SYSCALL_64是系统调用的入口点,它完成了保存现场,调用对应的内核处理函数、恢复现场、系统调用返回等工作。为了分析这一流程,我们需要关注以下enrty_SYSCALL_64这段代码:

  在linux5.4.34/arch/x86/kernel/entry中找到enrty_64.s,它是enrty_SYSCALL_64的所在。

  3.3.1 保存现场

  入口处:

 

 

   可以看到,他没有使用sava_all命令保存现场,而使用了特殊的swapgs,来快照式地保存现场,加快了系统调用的速度。

   swapgs之后push了一堆寄存器,将其入内核堆栈,然后转换成pt_regs结构体......如下图所示:

  

  具体地,可以参考这篇文章:

    https://www.cntofu.com/book/104/SysCall/syscall-2.md

  然后,执行了do_syscall_64这一内核函数

#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    struct thread_info *ti;

    enter_from_user_mode();
    local_irq_enable();
    ti = current_thread_info();
    if (READ_ONCE(ti->flags) & _TIF_WORK_SYSCALL_ENTRY)
        nr = syscall_trace_enter(regs);

    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
    } else if (likely((nr & __X32_SYSCALL_BIT) &&
              (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
        nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
                    X32_NR_syscalls);
        regs->ax = x32_sys_call_table[nr](regs);
#endif
    }

    syscall_return_slowpath(regs);
}
#endif

  在该函数调用中,rax 里存储了系统调用号,根据系统调用号在系统调用表 sys_call_table 中找到相应的内核处理函数进行调用,并将返回值保存进rcx寄存器。

  3.3.2 恢复现场

  结束处:

  

 

   执行了一个宏USERGS_SYSRET64, 在linux5.4.34/arch/x86/include/asm/irqflags.h中有他的如下定义:

  

   原来这个宏做了两部分工作:swapgs——恢复现场和sysretq——系统调用返回。

四、总结

  概括起来,我们总共完成内核调试环境的构建、在该环境下触发69号系统调用、追踪系统调用、分析系统调用的过程包括保存现场、恢复现场、内核堆栈状态的变化等工作

posted on 2020-05-22 02:11  dextttter  阅读(567)  评论(0编辑  收藏  举报

导航