一、实验要求
- 找一个系统调用,系统调用号为学号最后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
这个函数所做的内容就比较明确了,它首先查阅了系统调用表,然后调用了对应的系统调用,执行完以后就要由内核态返回用户态了
整个系统调用的流程如下图所示