深入理解系统调用
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
指令,恢复了rdi
和rsp
寄存器。系统调用完成。
总结
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
以上是关于深入理解系统调用的主要内容,如果未能解决你的问题,请参考以下文章