深入理解系统调用

一、实验要求

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

二、查找系统调用

  我的学号后两位是56,对应系统调用为clone

查阅资料可知,系统调用clone是用来创建轻量级进程(即线程)的,主要用于线程库的实现。它的函数原型如下

#define _GNU_SOURCE
#include <sched.h>
int clone(int (*func)(void*),void *child_stack,int flags,void *func_arg,....
           /*pid_t *ptid,struct user_desc *tls,pid_t *ctid*/);
                  
                                     Return process ID of child on success,or -1 on error

新线程被创建后,就会运行参数func指向的函数,该函数的参数则由参数func_arg指定。因为clone产生的子进程共享父进程内存,所以它不能使用父进程的栈。相反,调用者必须分配一块大小适中的内存空间供子进程的栈使用,同时将这块内存的指针置于参数child_stack中。参数flags服务于双重目的。首先,其低字节中存放着子进程的终止信号,子进程退出时其父进程将收到这一信号。(如果克隆产生的子进程因信号而终止,父进程依然会收到SIGCHLD信号)该字节也可能为0,这时将不会产生任何信号。
clone()函数中的flags参数是各位掩码的组合。其参数如下:

  • 共享文件描述符:CLONE_FILES

  如果指定了该标志,父子进程会共享同一个打开文件描述符表。也就是说,无论哪个进程对文件描述符的分配与释放都会影响另一个进程。

  • 共享与文件系统相关的信息:CLONE_FS

  如果指定了该标志,那么父子进程将共享与文件系统相关的信息:权限掩码、根目录以及当前工作目录。也就是说无论在哪个进程中调用umask()、chdir()或者chroot(),都将影响到另一个进程。

  • 共享对信号的处置设置:CLONE_SIGHAND

  如果设置了该标志,那么父子进程将共享同一信号处置表。无论在哪个进程中调用sigaction()或者signal()来改变对信号处置的设置,都会影响其他进程对信号的处置。

  • 共享父进程的虚拟内存:CLONE_VM

  如果设置了该标志,父子进程将会共享同一份虚拟内存页。无论哪一个进程更新了内存,或是调用了mmap()、munmap(),另一进程同样会观察到变化。

  • 线程组:CLONE_THREAD

  若设置了该标志,则会将子进程置于父进程的线程组中。如果未设置该标志,那么会将子进程置于新的线程组中。

  • 线程库支持:CLONE_PARENT_SETTID、CLONE_CHILD_SETTID和CLONE_CHILD_CLEARTID

  为实现POSIX线程,Linux2.6提供了对CLONE_PARENT_SETTID、CLONE_CHILD_SETTID和CLONE_CHILD_CLEARTID的支持。这些标志将会影响clone()对参数ptid、ctid的处理。如果设置了CLONE_PARENT_SETTID,内核会将子进程的线程ID写入ptid所指向的位置。如果设置了CLONE_CHILD_SETTID,那么clone()会将子线程的线程ID写入指针ctid所指向的位置。如果设置了CLONE_CHILD_CLEARTID,则会在子进程终止时将ctid所指向的内存清零。

  • 线程本地存储:CLONE_SETTLS

如果设置了该标志,那么参数tls所指向的user_desc结构会对线程所使用的线程本地存储缓冲区加以描述。

  • 共享systemV信号量的撤销值:CLONE_SYSVSEM

  如果设置了该标志,父子进程会将共享同一个SystemV信号量撤销值列表。

  • 每进程挂载命名空间:CLONE_NEWNS
  • 将子进程的父进程置为调用者的父进程:CLONE_PARENT

  默认情况下,当调用clone()创建新进程时,新进程的父进程就用clone()进程。如果设置该标志,那么调用者的父进程就成为子进程的父进程。

  • 进程跟踪:CLONE_PTRACE和CLONE_UNTRACED

  如果设置了CLONE_PTRACE且正在跟踪子进程,那么也会对子进程进行跟踪。从Linux2.6起,即可设置CLONE_UNTRACED标志,这也意味着跟踪进程不能强制其子进程设置为CLONE_PTRACE

  • 挂起父进程直至子进程退出或者调用exec():CLONE_VFORK

  如果设置了该标识,父进程将一直挂起,直至子进程调用exec()或者_exit()来释放虚拟内存资源为止。

二、触发系统调用

编写以下程序,使用clone来触发系统调用

#include <stdio.h>
#include <malloc.h>

#include <sched.h>
#include <signal.h>

#include <sys/types.h>
#include <unistd.h>


#define FIBER_STACK 8192
int a;
void * stack;

int do_something()
{
    printf("This is son, the pid is:%d, the a is: %d
", getpid(), ++a);
    free(stack); 
    exit(1);
}

int main()
{
    void * stack;
    a = 1;
    stack = malloc(FIBER_STACK);//为子进程申请系统堆栈

    if(!stack)
    {
        printf("The stack failed
");
        exit(0);
    }
    printf("creating son thread!!!
");

    clone(&do_something, (char *)stack + FIBER_STACK, CLONE_VM|CLONE_VFORK, 0);//创建子线程

    printf("This is father, my pid is: %d, the a is: %d
", getpid(), a);
    exit(1);
}

编译内核,制作根文件系统步骤略过,已经有很多同学写的很详细了。

在编译完上述程序后,将其放入根文件系统的home目录下,启动qemu,运行该程序可以获得以下结果。可以看到子进程被成功创建,并运行了指定的do_something函数,由于指定了CLONE_VM标志,使得子进程能够和父进程共享内存,表现为他们两个打印出的变量a值相同

三、通过gdb跟踪该系统调用的内核处理过程

通过如下命令重新启动qemu

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

另开一个终端,设置gdb连接qemu的端口(很不幸。。。qemu启动刚好也会触发这个系统调用,然后卡死,gdb此时使用bt一直提示Selected thread is running.实在不知道如何解决这个问题)

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

 不过通过查阅资料可以知道系统调用的进入点是entry_SYSCALL_64,对应的汇编代码如下

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 */
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 */

在执行系统调用前,entry_SYSCALL_64主要做了这些事情:①切换gs寄存器从用户态到内核态,通过swapgs指令实现 ②保存中断上下文 ③初始化内核堆栈,然后执行do_syscall_64真正处理系统调用

继续查看执行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

这个函数所做的内容就比较明确了,它首先查阅了系统调用表,然后调用了对应的系统调用,执行完以后就要由内核态返回用户态了

整个系统调用的流程如下图所示

原文地址:https://www.cnblogs.com/cccc2019fzs/p/12952156.html