深入理解Linux系统调用

实验要求

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

实验环境

  • Ubuntu 18.04
  • VMware workstation Pro 14

实验步骤

  1. 下载Linux内核源码并配置QMenu虚拟环境
  2. 配置内核选项,并编译
    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
  3. 制作根目录
            mkdir rootfs
            git clone https://github.com/mengning/menu.git
            cd menu
            sudo apt install gcc-multilib
            gcc -pthread -o init linktable.c menu.c test.c -m32 -static
            cd ../rootfs
            cp ../menu/init ./
            find . | cpio -o -Hnewc | gzip -9 > ../rootfs.img

    准备init脚本并放在rootfs/init目录下

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

    给init脚本添加可执行权限

    chmod +x init

    打包成内存根文件系统镜像

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

    启动qemu

    qemu-system-i386 -kernel bzImage -initrd rootfs.img

    4.跟踪系统调用

    首先查看自己学号对应的系统调用号。我学号末尾是93,对应的是ftruncate函数

    ftruncate函数目的是修改文件的大小,

          函数原型: int truncate(const char *path,off_t length)
          参数: path为文件名,length为为文件的最终大小。off_t为长整型,long int
             (1) 最终大小比原来大,向后扩展。
                (2) 最终大小比原来小,删除后边的部分。

          在menu里面的test.c中加入下面两个函数,并在主函数更新。

    重新编译menu中的test.c文件,开启qemu,输入update,与update-asm函数,执行成功。

     输入update命令,执行Update函数,修改文件大小为500字节

     输入update-asm命令,执行UpdateAsm函数,修改文件大小为200字节

    使用gdb逐步跟踪系统调用

    qemu-system-i386 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -S -s -append nokaslr
    file vmlinux
    target remote:1234
    b sys_ftruncate
    c

     

     5.分析

    通过上图可以发现,在使用int 0x80中断之后,CPU会运行arch/x86/entry/entry_32.S中的指令

    分析entry_32.S代码(分析过程已写在注释中)

    1 #这段代码就是系统调用处理的过程,其它的中断过程也是与此类似
     2 #系统调用就是一个特殊的中断,也存在保护现场和回复现场
     3 ENTRY(system_call)          #这是0x80之后的下一条指令
     4     RING0_INT_FRAME         # can't unwind into user space anyway
     5     ASM_CLAC
     6     pushl_cfi %eax          # save orig_eax
     7     SAVE_ALL                 #保护现场
     8     GET_THREAD_INFO(%ebp)
     9                     # system call tracing in operation / emulation
    10     testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags(%ebp)
    11     jnz syscall_trace_entry
    12     cmpl $(NR_syscalls), %eax
    13     jae syscall_badsys
    14 syscall_call:
    15     # 调用了系统调用处理函数,实际的系统调用服务程序
    16     call *sys_call_table(,%eax,4)#定义的系统调用的表,eax传递过来的就是系统调用号,在例子中就是调用的systime
    17 syscall_after_call:
    18     movl %eax,PT_EAX(%esp)      # store the return value
    19 syscall_exit:
    20     LOCKDEP_SYS_EXIT
    21     DISABLE_INTERRUPTS(CLBR_ANY)    # make sure we don't miss an interrupt
    22                     # setting need_resched or sigpending
    23                     # between sampling and the iret
    24     TRACE_IRQS_OFF
    25     movl TI_flags(%ebp), %ecx
    26     testl $_TIF_ALLWORK_MASK, %ecx  # current->work
    27     jne syscall_exit_work          #退出之前,syscall_exit_work 
    28     #进入到syscall_exit_work里边有一个进程调度时机
    29 
    30 restore_all:
    31     TRACE_IRQS_IRET
    32 restore_all_notrace:        #返回到用户态
    33 #ifdef CONFIG_X86_ESPFIX32
    34     movl PT_EFLAGS(%esp), %eax  # mix EFLAGS, SS and CS
    35     # Warning: PT_OLDSS(%esp) contains the wrong/random values if we
    36     # are returning to the kernel.
    37     # See comments in process.c:copy_thread() for details.
    38     movb PT_OLDSS(%esp), %ah
    39     movb PT_CS(%esp), %al
    40     andl $(X86_EFLAGS_VM | (SEGMENT_TI_MASK << 8) | SEGMENT_RPL_MASK), %eax
    41     cmpl $((SEGMENT_LDT << 8) | USER_RPL), %eax
    42     CFI_REMEMBER_STATE
    43     je ldt_ss           # returning to user-space with LDT SS
    44 #end
    46     RESTORE_REGS 4          # skip orig_eax/error_code
    47 irq_return:
    48     INTERRUPT_RETURN      #iret(宏),系统调用过程到这里结束

     实验总结

    Linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。从某种角度来看,系统调用和普通的函数调用非常相似。区别仅仅在于,系统调用由操作系统核心提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态。

      系统调用是怎么工作的?

        其原理是进程先用适当的值填充寄存器,然后调用一个特殊的指令,这个指令会跳到一个事先定义的内核中的一个位置。在Intel CPU中,这个由中断0x80实现。硬件知道一旦你跳到这个位置,你就不是在限制模式下运行的用户,而是作为操作系统的内核--由用户态转为内核态。

        进程可以跳转到的内核位置叫做sysem_call。这个过程检查系统调用号,这个号码告诉内核进程请求哪种服务。然后,它查看系统调用表(sys_call_table)找到所调用的内核函数入口地址。接着,就调用函数,等返回后,做一些系统检查,最后返回到进程(或到其他进程,如果这个进程时间用尽)。

        进程号是由eax寄存器存储的,参数一般是由ebx、ecx、edx、esl、edl、ebp来存储的。  

     

     

posted @ 2020-05-26 13:13  Benjamin&Annie  阅读(263)  评论(0编辑  收藏  举报