深入理解系统调用

一、搭建调试环境

1、按照下面的命令下载内核源码并配置内核选项,进行编译。

sudo apt install axel
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
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
make -j4
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

在配置内核选项中需要注意(1)打开debug相关选项 (2)关闭KASLR,否则会导致打断点失败 


运行最后一条命令时会kernel panic,因为没有文件系统,下面制作根文件系统。

2、借助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
make menuconfig
make -j4 && 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/

在make menuconfig时要注意设置编译成静态链接,不⽤动态链接库。

 

添加init脚本,给init脚本添加可执行权限

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Wellcome MengningOS!"
echo "--------------------"
cd home
/bin/sh
chmod +x init

打包根文件系统,并运行测试

find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage-initrd rootfs.cpio.gz

运行成功。

二、编写系统调用程序

因为学号后两位是96,在/home/zd/linux-5.4.34/arch/x86/entry/syscalls目录下查阅syscall_64.tbl文件,96号系统调用为__x64_sys_gettimeofday,对应的函数是gettimeofday。

在网上查询关于gettimeofday函数的使用方法,编写测试函数,使用汇编指令触发该系统调用。

(1)在C语言中可以使用函数gettimeofday()函数来得到时间。它的精度可以达到微秒

(2)函数原型

  #include<sys/time.h>

  int gettimeofday(struct  timeval*tv,struct  timezone *tz )

(3)返回值:gettimeofday()会把目前的时间用tv 结构体返回,当地时区的信息则放到tz所指的结构中。

C代码调用

#include<stdio.h>
#include<sys/time.h>
int main()
{
    struct timeval tv;
    struct timezone tz;
    gettimeofday(&tv,&tz);
    printf("tv_sec:%ld\n",tv.tv_sec);
    printf("tv_usec:%ld\n",tv.tv_usec);
    printf("tz_minuteswest:%d\n",tz.tz_minuteswest);
    printf("tz_dsttime:%d\n",tz.tz_dsttime);
    return 0;
}

内嵌汇编调用

#include<stdio.h>
#include<sys/time.h>
int main()
{
    struct timeval tv;
    struct timezone tz;
    int flag;
    asm volatile(
        "movq %1, %%rdi\n\t"
        "movq %2, %%rsi\n\t"
        "movq $0x4E, %%rax\n\t"
        "syscall\n\t"
        "movq %%rax, %0\n\t"
        :"=m"(flag)
        :"b"(&tv),"c"(&tz)
    );
    printf("tv_sec:%ld\n",tv.tv_sec);
    printf("tv_usec:%ld\n",tv.tv_usec);
    printf("tz_minuteswest:%d\n",tz.tz_minuteswest);
    printf("tz_dsttime:%d\n",tz.tz_dsttime);
    return 0;
}

运行结果如下

三、GDB跟踪系统调用的内核处理过程

1、启动qemu虚拟机

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

2、再开一个终端,启动调试

cd linux-5.4.34/
gdb vmlinux
(gdb) target remote:1234
(gdb) b __x64_sys_gettimeofday

3、命令中在gettimeofday系统调用处打上断点,在qemu中运行调用函数,程序会停在断点,等待调试

4、在调试终端输入bt查看系统调用的函数和顺序

从终端信息我们可以知道,函数的调用顺序为entry_SYSCALL_64 () —> do_syscall_64() —> __x64_sys_gettimeofday()

5、不断输入n,进行单步调试,观察系统调用的整个过程,直到调用结束,继续运行,qemu中输出运行的结果

(gdb) n
do_syscall_64 (nr=140730116323824, regs=0xffffc900001b7f58)
    at arch/x86/entry/common.c:300
300        syscall_return_slowpath(regs);
(gdb) n
301    }
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:184
184        movq    RCX(%rsp), %rcx
(gdb) n
185        movq    RIP(%rsp), %r11
(gdb) n
187        cmpq    %rcx, %r11    /* SYSRET requires RCX == RIP */
(gdb) n
188        jne    swapgs_restore_regs_and_return_to_usermode
(gdb) n
205        shl    $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
(gdb) n
206        sar    $(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
(gdb) n
210        cmpq    %rcx, %r11
(gdb) n
211        jne    swapgs_restore_regs_and_return_to_usermode
(gdb) n
213        cmpq    $__USER_CS, CS(%rsp)        /* CS must match SYSRET */
(gdb) n
214        jne    swapgs_restore_regs_and_return_to_usermode
(gdb) n
216        movq    R11(%rsp), %r11
(gdb) n
217        cmpq    %r11, EFLAGS(%rsp)        /* R11 == RFLAGS */
(gdb) n
218        jne    swapgs_restore_regs_and_return_to_usermode
(gdb) n
238        testq    $(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
(gdb) n
239        jnz    swapgs_restore_regs_and_return_to_usermode
(gdb) n
243        cmpq    $__USER_DS, SS(%rsp)        /* SS must match SYSRET */
(gdb) n
244        jne    swapgs_restore_regs_and_return_to_usermode
(gdb) n
253        POP_REGS pop_rdi=0 skip_r11rcx=1
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:259
259        movq    %rsp, %rdi
(gdb) n
260        movq    PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:262
262        pushq    RSP-RDI(%rdi)    /* RSP */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:263
263        pushq    (%rdi)        /* RDI */
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:271
271        SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi
(gdb) n
273        popq    %rdi
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:274
274        popq    %rsp
(gdb) n
entry_SYSCALL_64 () at arch/x86/entry/entry_64.S:275
275        USERGS_SYSRET64
(gdb) n
0x0000000000448b97 in ?? ()
(gdb) c
Continuing.
(gdb) 

四、系统调用过程分析

基本原理

1、系统调⽤的意义是操作系统为⽤户态进程与硬件设备进⾏交互提供了⼀组接⼝。
2、系统调⽤的库函数就是我们使⽤的操作系统提供的 API(应⽤程序编程接⼝), API 只是函数定义,可能直接提供⼀些⽤户态的服务,并不需要通过系统调⽤与内核打交道 。
3、系统调⽤是通过特定的软件中断(陷阱 trap)向内核发出服务请求, int $0x80和syscall指令的执⾏就会触发⼀个系统调⽤。系统调⽤从⽤户态切换到内核态,在⽤户态和内核态这两种执⾏模式下使⽤的是不同的堆栈,即进程的⽤户态堆栈和进程的内核态堆栈,传递参数⽅法⽆法通过参数压栈的⽅式,⽽是通过寄存器传递参数的⽅式。 
4、int $0x80CPU压栈⼀些关键寄存器,接着内核负责保存现场,系统调⽤内核函数处理完后恢复现场,最后通过iret出栈哪些CPU压栈的关键寄存器。
5、sysentersyscall都借助CPU内部的MSR寄存器来查找系统调⽤处理⼊⼝,可断处理的思路,压栈关键寄存器、保存现场、恢复现场,最后系统调⽤返回。
6、x86-64引⼊了swapgs指令,类似快照的⽅式将保存现场和恢复现场时的CPU寄存器也通过CPU内部的存储器快速保存和恢复,近⼀步加快了系统调⽤ 

下面对过程进行逐步分析

1、首先是内核的初始化,完成了以下的函数调用过程:start_kernel > trap_init > cpu_init > syscall_init, 其中start_kernal是内核启动的入口函数。而syscall_init 中的entry_SYSCALL_64在内核初始化的过程中加载到了msr寄存器中。在后续的执行过程中,一旦出现了SYSCALL这条指令,就会直接去访问msr寄存器。 

2、进入内核态后,swapgs进行现场的保存,并完成寄存器的压栈,可以从arch/x86/entry 目录下查看entry.S文件

3、调用arch/x86/entry/common.c中的do_do_syscall_64,从中断向量表中获得系统调用号,regs->ax = sys_call_table[nr](regs); 对函数进行调用

4、gettimeofday函数定义在kernel/time/time.c中,timeval表示时间信息,timezone表示时区信息,两者都可为NULL,timezone使用copy_to_user直接复制到用户空间,所以着重关注下timeval的获取。

 

调用ktime_get_real_ts64(&ts),获取timeval的信息,这个函数定义在同目录下的timekeeping.c函数中,

5、函数执行完后回到do_do_syscall_64,进行syscall_return_slowpath(regs)的调用,prepare_exit_to_usermode(regs)准备返回用户态

6、再次回到entry_64.S中,进行最后的现场恢复和堆栈的切换

回到用户态程序,继续执行下一条指令,至此,系统调用过程结束。

五、总结

从这次实验中,学习了通过汇编指令来触发系统调用,并用gdb调试跟踪了整个系统调用的过程,让我对linux内核的系统调用过程有了深刻而且系统的理解。下面是对整个过程的总结。

从系统调用的整个过程来看,主要有以下几个阶段:

(1)用户态程序,发生syscall,触发系统调用;

(2)进入内核态,完成内核初始化后,调用entry_SYSCALL_64 ()

(3)完成现场的保存,将关键寄存器压栈,并从CPU内部的MSR寄存器来查找系统调⽤处理⼊⼝,更改CPU的指令指针(eip/rip)到系统调⽤处理⼊⼝ ,调用do_syscall_64()

(4)do_syscall_64()函数中得到系统调用号,调用相关的函数gettimeofday()

(5)调用结束后,保存现场和恢复现场时的CPU存器也通过CPU内部的存储器快速保存和恢复 

(6)系统调用返回,回到用户态程序

参考资料:https://www.cnblogs.com/long5683/p/9999746.html

posted @ 2020-05-25 00:58  zda1234  阅读(515)  评论(0编辑  收藏  举报