一、实验内容
1.找一个系统调用,系统调用号为学号最后2位相同的系统调用
2.通过汇编指令触发该系统调用
3.通过gdb跟踪该系统调用的内核处理过程
4.重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化
二、实验过程
1、环境配置
下载Linux5.4.34源码
sudo apt install axel axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux5.4.34.tar.xz xz -d linux-5.4.34.tar.xz tar -xvf linux-5.4.34.tar
安装开发工具
sudo apt install build-essential sudo apt install qemu # install QEMU sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
配置内核选项
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)
下载busybox并编译
#下载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 #记得要编译成静态链接,不⽤动态链接库。 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/
准备init脚本⽂件放在根⽂件系统跟⽬录下(rootfs/init),添加如下内容到init⽂件。
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome ToMyWorldOS!" cd home /bin/sh
给init脚本添加可执行权限
chmod +x init
打包成内存根⽂件系统镜像
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
可以看到运行结果如下:
成功执行了init脚本,说明根文件系统制作完毕。
三、汇编指令触发该系统调用
我的学号后两位为34,打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl
,查看对应的系统调用。
可以看到,34号对应的系统调用为pause,内核处理函数是__x64_sys_pause。
通过man命令查看pause系统调用的相关信息:
man pause
pause() causes the calling process (or thread) to sleep until a signal is delivered that either terminates the process or causes the invoca‐ tion of a signal-catching function.
pause系统调用可以使进程或线程休眠,直至终止进程或唤醒进程的信号传递过来。pause()函数当信号被捕捉到并且信号捕捉函数也返回时才返回-1。
首先,编写系统调用测试程序
#include<stdio.h> #include<errno.h> #include<unistd.h> #include<signal.h> #include<stdlib.h> void catch_sigalrm(int signo) { ; } unsigned int mysleep(unsigned int seconds) { int ret; struct sigaction act,oldact; act.sa_handler = catch_sigalrm; sigemptyset(&act.sa_mask); act.sa_flags = 0; ret = sigaction(SIGALRM,&act,&oldact); if(-1 == ret) { perror("sigaction error"); exit(1); } alarm(seconds); ret = pause(); if(-1 == ret && errno == EINTR) { printf("pauses success\n"); } ret =alarm(0); sigaction(SIGALRM,&oldact,NULL); return ret; } int main() { while(1) { mysleep(3); printf("-----------\n"); } return 0; }
代码来源:https://www.cnblogs.com/wanghao-boke/p/11334979.html
这里,alarm函数的作用是设置信号传送闹钟,设置信号SIGALRM在经过参数seconds秒后发送给目前的进程。SIGALARM的处理函数为catch_sigalrm,什么也不做。这里会不断进行alarm的设定,并在pause处将发生系统调用将当前线程阻塞,当alarm时间过去3s时,发出信号并完成处理函数,就会引起pause函数的返回,对进程进行唤醒。这样,程序在不断阻塞、唤醒、阻塞、唤醒......
接下来,对程序进行编译,并且要重新打包内存根文件系统镜像:
gcc -o pause pause.c -static #无法使用动态链接,所以使用静态编译 find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
接下来使用gdb进行跟踪调试,这里,我遇到了一个问题,在断点之后continue出现reply is too long:
经过查询,发现需要重新下载并修改gdb/remote.c中的部分代码:
解决了这个问题之后,开始进行gdb调试。首先回到linux目录下启动qemu:
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
重开一个窗口开启gdb调试工具:
gdb vmlinux
设置断点__x64_sys_pause,连接到1234端口,并继续让内核继续运行:
待内核初始化完成,进入根文件系统的目录/home/syscall下,执行pause
在gdb工具运行界面进行单步调试。
从上图可以看到,在执行pause程序时,确实停在了断点1:__x64_sys_pause处,输入bt查看堆栈情况,可以看到在执行到__x64_sys_pause时,栈中保留了三个函数的上下文,分别为__x64_sys_pause、do_syscall_64、entry_SYSCALL_64。值得注意的是,在单步执行到schedule函数时,gdb命令行明显等待了一会。我认为这里是发生了进程调度切换,当前进程主动释放掉CPU资源进入睡眠,当alarm发出信号时,发生中断,唤醒当前进程。所以,大约等待了3s,gdb才重新刷新。
四、分析
首先,系统调用pause真正调用的核心部分很简单,只有几行,如下:
while循环的条件部分检查当前进程是否有信号处理,alarm会触发信号,一开始没有信号,所以第一次可以进入while循环。
然后设置当前进程的状态为TASK_INTERRUPTIBLE,这是针对等待某事件或其他资源而睡眠的进程设置的。在内核发送信号给该进程时表明等待的事件已经发生或资源已经可用,进程状态变为 TASK_RUNNING,此时只要被调度器选中就立即可恢复运行。
最后是schedule函数,主动调度释放掉CPU占有,并且把当前进程从运行队列中移除。以上,就是pause系统调用的全部内容。
另外,需要理清整个系统调用的执行过程。从断点处的调用栈可以看到,先调用了entry_SYSCALL_64,然后调用了do_syscall_64,最后才是具体的系统调用函数__x64_sys_pause。
具体来说,有以下几个步骤:
- 用户态的程序触发了系统调用,进入内核态,通过MSR寄存器找到中断函数入口,程序执行到entry_SYSCALL_64,在entry_SYSCALL_64中,主要保存进程现场
2. 执行do_syscall_64函数,通过syscalltable得到系统调用号,去执行具体的系统调用内核处理函数。
3. 执行__x64_sys_pause函数,即上面的while循环部分。运行完之后,回到do_syscall_64,执行剩下部分进行现场恢复,并准备好回到用户态。do_syscall_64执行完后回到entry_SYSCALL_64,继续恢复 现场,进行堆栈的切换。