深入理解系统调用
一、实验要求
- 找一个系统调用,系统调用号为学号最后2位相同的系统调用;
- 通过汇编指令触发该系统调用;
- 通过gdb跟踪该系统调用的内核处理过程;
- 重点阅读分析系统调用入口的保存现场、恢复现场和系统调用返回,以及重点关注系统调用过程中内核堆栈状态的变化。
二、实验目的
- 理解Linux操作系统调用;
- 了解系统调用过程中内核堆栈状态的变化过程。
三、实验环境
实验楼环境:https://www.shiyanlou.com/courses/195/learning
四、实验过程
(一)查找所需要的编号以及环境测试
进入根目录下,通过 find . -name 'syscall_64.tbl' 查找系统调用表,我的学号尾号为79,因此查表之后,选择的系统调用为 getcwd
getcwd系统调用介绍:
每个进程都有两个目录相关属性:根目录和当前目录,分别用于解释绝对路径和相对路径getcwd()会返回当前工作目录的绝对路径,如果当前目录不属于当前进程的根目录(例如:该进程使用chroot设置了一个新的文件系统根目录,但是没有将当前目录的根目录替换成新的)。
更新实验menu得环境,使用新版本
(二)编写汇编触发系统调用
首先判断C语言能否使用该系统调用
#include <unistd.h> int main() { char buf[80]; getcwd(buf, sizeof(buf)); printf("current working directory : %s\n", buf); }
修改menuOS中test代码。添加关于getcwd得系统调用,此处使用上述代码代替,后期换成汇编代码执行。
将C语言中的调用函数部分改成汇编执行
搭建好了MenuOS后进行调试跟踪
(三) gdb跟踪内核处理过程
首先启动muneOS ,启动后会暂停继续执行,然后使用命令行进入gdb系统调试。
设置系统系统调用得阻塞点。
启动成功。执行我们在MenuOS中编写得getcwd命令,可以发现他会调用getcwd系统调用,因此会被gdb中断,开始查看调用流程。
重新编译。主要命令如下:
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s #开启新的terminal gdb target remote:1234 #设置断点 b sys_getcwd
c
执行该命令后,就会发现系统已经被阻塞。可以进行调试跟踪。可以看到是从3212行开始调用
由于实验环境的代码可读性较差,因此招了一份源码如下所示。
源码分析如下所示。
系统调用的源码如下所示:
YSCALL_DEFINE2(getcwd, char __user *, buf, unsigned long, size) { int error; struct path pwd, root;//一个存放当前目录,一个存放根目录 char *page = __getname(); if (!page) return -ENOMEM; rcu_read_lock(); //加锁,防止读取时当前路径被修改 get_fs_root_and_pwd_rcu(current->fs, &root, &pwd); //获取根目录和当前目录项。get_fs_root_and_pwd_rcu函数就是根据fs结构来获取&root和&pwd error = -ENOENT; if (!d_unlinked(pwd.dentry)) { unsigned long len; char *cwd = page + PATH_MAX; int buflen = PATH_MAX; prepend(&cwd, &buflen, "\0", 1); error = prepend_path(&pwd, &root, &cwd, &buflen); rcu_read_unlock();//读取完成,释放上边加的锁 if (error < 0) goto out; /* Unreachable from current root */ if (error > 0) { error = prepend_unreachable(&cwd, &buflen); if (error) goto out; } error = -ERANGE; len = PATH_MAX + page - cwd; if (len <= size) { error = len; if (copy_to_user(buf, cwd, len)) error = -EFAULT; } } else { rcu_read_unlock(); } out: __putname(page); return error; }
此外还对getcwd方法的C语言源码进行了阅读
char * __getcwd (char *buf, size_t size) { char *path; char *result; //确定缓冲区的长度 size_t alloc_size = size; if (size == 0) { if (buf != NULL) { __set_errno (EINVAL); return NULL; } alloc_size = MAX (PATH_MAX, __getpagesize ()); } //申请缓冲区 if (buf == NULL) { path = malloc (alloc_size); if (path == NULL) return NULL; } else path = buf; int retval; //调用系统调用获取当前工作目录 retval = INLINE_SYSCALL (getcwd, 2, path, alloc_size); //获取成功 if (retval >= 0) { //重新设置缓冲区 if (buf == NULL && size == 0) buf = realloc (path, (size_t) retval); if (buf == NULL) buf = path; return buf; } //如果出错了,且出错原因是文件路径太长,则执行generic_getcwd获取当前文件路径。 generic_getcwd是内部函数,源码不可见。 if (errno == ENAMETOOLONG) { if (buf == NULL && size == 0) { free (path); path = NULL; } result = generic_getcwd (path, size); if (result == NULL && buf == NULL && size != 0) free (path); return result; } assert (errno != ERANGE || buf != NULL || size != 0); if (buf == NULL) free (path); return NULL; } weak_alias (__getcwd, getcwd)
四、实验总结(分析系统调用的工作机制)
系统调用主要分为以下五步,指令触发系统调用(这里是syscall),保存现场,中断处理,恢复现场,中断返回,第一步在用户态,剩下四步在内核态。从系统调用的整个过程来看,
首先用户态程序执行某些代码,发生syscall,触发系统调用;然后系统进入内核态,完成内核初始化后,调用entry_SYSCALL_64 ()。完成现场的保存,将关键寄存器压栈,包括程序计数器,内存地址等相关信息,方便系统调用完成后继续执行。
然后从CPU内部的MSR寄存器来查找系统调⽤处理⼊⼝,更改CPU的指令指针(eip/rip)到系统调⽤处理⼊⼝ ,调用do_syscall_64()。do_syscall_64()函数根据系统调用号,调用相关的函数,比如本例中的getcwd()。调用结束后,保存现场和恢复现场时的CPU寄存器也通过CPU内部的存储器快速保存和恢复 。系统调用返回,回到用户态程序