Linux实验二:深入理解系统调用

一、 实验要求:

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

 

二、实验环境:

  1. 虚拟机版本:VMware Workstation 15

  2. 操作系统版本:Ubuntu19.10

  3. 内核版本:linux_5.4.34

 

三、实验前的准备工作

  • 配置VScode

  • 配置内核选项

  • * 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
    • [*] 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

 

* 制作根文件系统

  • 电脑加电启动⾸先由bootloader加载内核,内核紧接着需要挂载内存根⽂件系统,其中包含必要的设备驱动和⼯具, bootloader加载根⽂件系统到内存中,内核会将其挂载到根⽬录/下,然后运⾏根⽂件系统中init脚本执⾏⼀些启动任务,最后才挂载真正的磁盘根⽂件系统。

  • 我们这⾥为了简化实验环境,仅制作内存根⽂件系统。这⾥借助BusyBox 构建极简内存根⽂件系统,提供基本的⽤户态可执⾏程序。

  • 具体过程

  • # 下载busybox
    
    • ⾸先从https://www.busybox.net下载 busybox源代码解压,解压完成
    后,跟内核⼀样先配置编译,并安装。
    • 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
    
    
    
    
    
    # 安装busybox
    • make menuconfig
    • 记得要编译成静态链接,不⽤动态链接库。
    • Settings --->
    • [*] Build static binary (no shared libs)
    • 然后编译安装,默认会安装到源码⽬录下的 _install ⽬录中。
    • make -j$(nproc) && make install
    
    # 制作根文件系统的镜像
    • 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/
    
    
    // 打包成内存根⽂件系统镜像
     find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../
    rootfs.cpio.gz
    测试挂载根⽂件系统,看内核启动完成后是否执⾏init脚本
    qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage
    -initrd rootfs.cpio.gz

    注意:这里由于老师示范与我自己安装的很多文件夹的目录有所不同,所以,在运行上述命令时,要考虑到这一点,在有关于路径的问题上自己斟酌。

  • 跟踪调试linux内核的基本方法
  • 纯命令⾏下启动虚拟机
  • qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
  • ⽤以上命令先启动,然后可以看到虚拟机⼀启动就暂停了。加-nographic -append "console=ttyS0"参数启动不会弹出QEMU虚拟机窗⼝,可以在纯命令⾏下启动虚拟机,此时可以通过“killall qemu-system-x86_64”命令强⾏关闭虚拟机。
  • - cd linux-5.4.34/
    - gdb vmlinux
    - (gdb) target remote:1234
    - (gdb) b start_kernel
    - c、 bt、 list、 next、 step.... 

     

  • 至此,在正式开始实验前的准备工作已经完毕。我们有了一个可以加载指定文件系统镜像的简单虚拟机。

 

 

四、正式实验:

  • 一些简单的背景知识
  •   

    Linux的系统调⽤

  •   
    • 当⽤户态进程调⽤⼀个系统调⽤时, CPU切换到内核态并开始执⾏system_call(entry_INT80_32或entry_SYSCALL_64)汇编代码,其中根据系统调⽤号调⽤对应的内核处理函数。

    • 具体来说,在Linux中通过执⾏int $0x80或syscall指令来触发系统调⽤的执⾏,其中这条int$0x80汇编指令是产⽣中断向量为128的编程异常(trap)。

    • 另外Intel处理器中还引⼊了sysenter指令(快速系统调⽤),因为Intel专⽤AMD并不⽀持,在此不再详述。我们只关注int指令和syscall指令触发的系统调⽤,进⼊内核后,开始执⾏对应的中断服务程序entry_INT80_32或entry_SYSCALL_64

 

  • Linux的系统调⽤号

    • 内核通过给每个系统调⽤⼀个编号来区分,即系统调⽤号,将API函数xyz()和系统调⽤内核函数 sys_xyz()关联起来了。

    • 内核实现了很多不同的系统调⽤,⽤户态进程必须指明需要执⾏哪个系统调⽤,这需要使⽤EAX寄存器传递⼀个名为系统调⽤号的参数。除了系统调⽤号外,系统调⽤也可能需要传递参数

 

Linux实验二:深入理解系统调用_第1张图片

 

 

 

 

 

 

 

 

 

 

  • 本人学号最后两位是87,所以选定如下系统调用
87    common    unlink            __x64_sys_unlink
//功能描述:

//从文件系统中删除一个名称。如果名称是文件的最后一个连接,并且没有其它进程将文件打开,名称对应的文件会实际被删除。
  • 这里我们编写一个简单程序。因为本次实验的重点在于观察系统调用,所以这里仅仅只是简单的调用一下该函数,不考虑函数实际执行的结果。
  • int main() {
            asm volatile(
                            "movl $0,%%edi\n\t"
                            "movl $0x57,%%eax\n\t"//transfer the number of syscall
                            "syscall\n\t"//syscall
                            );
            return 0;
    }

 

  • 因为这里的根文件系统是静态连接,所以gcc编译时需要有-static
    gcc -o _87syscall _87_test.c -static

     

  • 因为rootf有变化,需要重新生成内存根文件镜像
  • find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz

     

  • 重新挂载镜像,然后运行qemu
  • qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

     

  • 接下来,设置断点,运行程序。并查看调用。
  • Linux实验二:深入理解系统调用_第2张图片Linux实验二:深入理解系统调用_第3张图片

 

 

  • 系统在初始化过程中没有捕获断点,推测在初始化过程总没有使用该系统调用。于是运行所写的程序,成功捕获。
  • 用命令,可以看到,具体的调用栈,依次为entry_SYSCALL_64,do_syscall_64,__x64_sys_unlink.
  • 可以看到unlink系统调用实际上执行do_unlinkat,恰巧在系统调用表中找到unlinkat调用,猜测unlink调用已经被淘汰?
  • 逐步单步调试
  • Linux实验二:深入理解系统调用_第4张图片
  • Linux实验二:深入理解系统调用_第5张图片

 

 

 

 

五、分析与总结

ENTRY(entry_SYSCALL_64) UNWIND_HINT_EMPTY /* * Interrupts are off on entry. * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON, * it is too small to ever cause noticeable irq latency. */ swapgs /* tss.sp2 is scratch space. */ movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2) SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp /* Construct struct pt_regs on stack */ pushq $__USER_DS /* pt_regs->ss */ pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */ pushq %r11 /* pt_regs->flags */ pushq $__USER_CS /* pt_regs->cs */ pushq %rcx /* pt_regs->ip */

swapgs指令,快速保存一些寄存器里的参数,然后再在栈中构造一各pt_regs结构用以保存当前的相关信息。

 

GLOBAL(entry_SYSCALL_64_after_hwframe)
    pushq    %rax                    /* pt_regs->orig_ax */

    PUSH_AND_CLEAR_REGS rax=$-ENOSYS

    TRACE_IRQS_OFF

    /* IRQs are off. */
    movq    %rax, %rdi
    movq    %rsp, %rsi
    call    do_syscall_64        /* returns with IRQs disabled */

    TRACE_IRQS_IRETQ        /* we're about to change IF */

接着执行了do_syscall_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);
}

根据系统调用号,执行相关的系统调用。在调用完成后,

syscall_return_slowpath(regs);
  • 上面这个命令返回

__visible inline void syscall_return_slowpath(struct pt_regs *regs) { struct thread_info *ti = current_thread_info(); u32 cached_flags = READ_ONCE(ti->flags); CT_WARN_ON(ct_state() != CONTEXT_KERNEL); if (IS_ENABLED(CONFIG_PROVE_LOCKING) && WARN(irqs_disabled(), "syscall %ld left IRQs disabled", regs->orig_ax)) local_irq_enable(); rseq_syscall(regs);

  • 再向后的函数看的不是很明白,但是结合上面单步调试的两张截图,可以看出,在这之后将保存在栈中的相关数据回复的寄存器中,使得调用前的相关数据得以回复,继续执行。

 

你可能感兴趣的:(Linux实验二:深入理解系统调用)