深入理解系统调用

Posted howin

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解系统调用相关的知识,希望对你有一定的参考价值。

系统调用概念

1. 什么是系统调用

简单来说,系统调用就是用户程序和硬件设备之间的桥梁。

用户程序在需要的时候,通过系统调用来使用硬件设备。

系统调用的存在,有以下重要的意义:

1)用户程序通过系统调用来使用硬件,而不用关心具体的硬件设备,这样大大简化了用户程序的开发。

比如:用户程序通过write()系统调用就可以将数据写入文件,而不必关心文件是在磁盘上还是软盘上,或者其他存储上。

2)系统调用使得用户程序有更好的可移植性。

只要操作系统提供的系统调用接口相同,用户程序就可在不用修改的情况下,从一个系统迁移到另一个操作系统。

3)系统调用使得内核能更好的管理用户程序,增强了系统的稳定性。

因为系统调用是内核实现的,内核通过系统调用来控制开放什么功能及什么权限给用户程序。

这样可以避免用户程序不正确的使用硬件设备,从而破坏了其他程序。

4)系统调用有效的分离了用户程序和内核的开发。

用户程序只需关心系统调用API,通过这些API来开发自己的应用,不用关心API的具体实现。

内核则只要关心系统调用API的实现,而不必管它们是被如何调用的。

系统调用在系统中的关系如下图所示:

技术图片

2. Linux上的系统调用实现原理

要想实现系统调用,主要实现以下几个方面:

1. 通知内核调用一个哪个系统调用

2. 用户程序把系统调用的参数传递给内核

3. 用户程序获取内核返回的系统调用返回值

下面看看Linux是如何实现上面3个功能的。

2.1 通知内核调用一个哪个系统调用

每个系统调用都有一个系统调用号,系统调用发生时,内核就是根据传入的系统调用号来知道是哪个系统调用的。

在x86架构中,用户空间将系统调用号是放在eax中的,系统调用处理程序通过eax取得系统调用号。

系统调用号定义在内核代码:arch/x86/include/asm/unistd.h 中,可以看出linux的系统调用不是很多。

2.2 用户程序把系统调用的参数传递给内核

系统调用的参数也是通过寄存器传给内核的,在x86系统上,系统调用的前5个参数放在ebx,ecx,edx,esi和edi中,如果参数多的话,还需要用个单独的寄存器存放指向所有参数在用户空间地址的指针。

一般的系统调用都是通过C库(最常用的是glibc库)来访问的,Linux内核提供一个从用户程序直接访问系统调用的方法。

参见内核代码:rch/x86/include/asm/unistd.h :

里面定义了6个宏,分别可以调用参数个数为0~6的系统调用

syscall0(type,name)

_syscall1(type,name,type1,arg1)

_syscall2(type,name,type1,arg1,type2,arg2)

_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)

_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)

_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)

_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)

超过6个参数的系统调用很罕见,所以这里只定义了6个。

2.3 用户程序获取内核返回的系统调用返回值

获取系统调用的返回值也是通过寄存器,在x86系统上,返回值放在eax中。

3. 一个简单的系统调用的实现

了解了Linux上系统调用的原理,下面就可以自己来实现一个简单的系统调用。

环境准备

首先配置内核选项如下图所示(调试内核必须这样配置):

技术图片

技术图片

技术图片

之后使用busybox制作根文件系统,我下载的是1.31.1版本。

首先配置busybox使用静态链接,否则在根文件系统顶层目录下面要拷贝编译器库文件,很麻烦。

技术图片

之后使用命令make -j$(nproc) && make install编译安装busybox。默认安装路径为源码目录的_install下面。

再到家目录下面新建rootfs文件夹,将_install目录中的所有文件拷贝过去。并且新建几个目录(dev proc sys home等)和文件(dev/console dev/null dev/tty*)。

技术图片

准备init脚本?件放在根?件系统跟?录下(rootfs/init),在init?件中添加这些内容:

技术图片

之后给init脚本添加可执?权限:chmod +x init

打包成内存根?件系统镜像:

技术图片

测试挂载根?件系统,看内核启动完成后是否执?init脚本:

技术图片

qemu运行部分截图如下所示:

技术图片

查看系统调用表和汇编改写

打开/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,查看要选择进行实验的系统调用。

我的学号为48,看到shutdown的时候我心中一阵窃喜。但是又无从下手,所以换了个系统调用,最近在看write系统调用,就从writev下手吧,正好csdn上也有个博客可参考。

技术图片

 

系统调用writev,函数入口为__x64_sys_writev。下面通过一个函数来简单了解writev的使用:

技术图片

gcc编译(这里采用静态编译)后运行,输出结果:

技术图片

简单分析,writev的作用是将多个缓冲区的内容一次性输出到某个位置/内容。

汇编改写手动触发系统调用
新建在 rootfs/home 目录下新建文件 write-asm.c,在后面添加以下内容:

技术图片

gcc编译后查看执行结果,和write.c效果一样。改写成功。

gdb调试与分析

重新打包根文件目录,纯命令?下启动虚拟机。

qemu-system-x86_64 -kernel shiyan/linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0",此时虚拟机会暂停在启动界面。在另一个terminal中开启gdb调试 gdb vmlinux,连接进行调试,target remote:1234。结果如下图所示:

技术图片

技术图片

gdb输入命令c,使得虚拟机继续执行,到初始界面。

技术图片

由之前分析可知,wirtev系统调用触发的函数是__x64_sys_writev,通过gdb在函数入口下断点,然后监听。

技术图片

在虚拟机中执行write-file,会卡住,在gdb界面查看断点分析。

使用 l 命令查看代码情况, n 命令单步执行, step 命令进入函数内部 bt查看堆栈。

技术图片

首先断点定位为到/home/howin/shiyan/linux-5.4.34/fs/read_write.c 中的1128行:

技术图片

进入do_writev函数查看,可知,这里是完成程序内容的地方,前期的保存现场工作已经完成。

执行完这个函数,发现回到了函数堆栈上一层的do_sys_call_64 中 ,接下来要执行的 syscall_return_slowpath 函数要为恢复现场做准备。

技术图片

继续执行,发现再次回到了函数堆栈的上一层,entry_SYSCALL_64 ,接下来执行的是用于恢复现场的汇编指令。

技术图片

技术图片

最后伴随着两个pop指令,恢复了rdirsp寄存器。系统调用完成。

技术图片

总结

1.汇编指令syscall 触发系统调用,通过MSR寄存器找到了中断函数入口(具体细节不考虑),此时,代码执行到/home/howin/shiyan/linux-5.4.34/arch/x86/entry/entry_64.S 目录下的ENTRY(entry_SYSCALL_64)入口,然后开始通过swapgs 和压栈动作保存现场。

技术图片

 

2.然后跳转到了/linux-5.4.34/arch/x86/entry/common.c 目录下的 do_syscall_64 函数,在ax寄存器中获取到系统调用号,然后去执行系统调用内容.

技术图片

3.然后程序跳转到/linux-5.4.34/fs/read_write.c 下的do_writev 函数,开始执行。

技术图片

4.函数执行完后回到步骤3中的syscall_return_slowpath(regs); 准备进行现场恢复操作。

技术图片

5.接着程序再次回到arch/x86/entry/entry_64.S,执行现场的恢复,最后两句,完成了堆栈的切换。

技术图片

参考博客:https://blog.csdn.net/Alan_cqu_cj/article/details/106272204

以上是关于深入理解系统调用的主要内容,如果未能解决你的问题,请参考以下文章

深入理解系统调用

深入理解系统调用

深入理解计算机操作系统(笔记)

深入理解系统调用

深入理解系统调用

深入理解系统调用