一、系统调用流程分析
系统调用系统调用就是用户空间应用程序和内核提供的服务之间的一个接口。由于服务是在内核中提供的,因此无法执行直接调用;相反,我们必须使用一个进程来跨越用户空间与内核之间的界限,这实际上就是系统调用的过程。我们首先来开一个系统调用流程的示意图。
Linux中的系统调用的实现会根据不同的架构而有所变化,而且即使在某种给定的体系结构上也会有所不同,我们看到上述是一个简单的系统调用的例子。每一个个系统调用都是通过一个单一的入口点多路传入内核的。eax 寄存器用来标识应当调用的某个系统调用,这在 C 库中做了指定(来自用户空间应用程序的每个调用)。当加载了系统的 C 库调用索引和参数时,就会调用一个软件中断(0x80 中断),它将执行 system_call 函数(通过中断处理程序),这个函数会按照 eax 内容中的标识处理所有的系统调用。在经过几个简单测试之后,使用 system_call_table 和 eax 中包含的索引来执行真正的系统调用了。从系统调用中返回后,最终执行 syscall_exit,并调用 resume_userspace 返回用户空间。然后继续在 C 库中执行,它将返回到用户应用程序中。
上一篇文章实际上我们经讲过了关于system_call_table相关的内容了,我们实际上可以简单的理解为一个跳转列表一样的东西,那么系统是如何通过这些列表来找到具体的服务程序的呢?这里面实际上非常类似于中断处理的过程,因为系统调用过程的本身就是引发一次中断的过程。这里面有一个非常好的词就是trap。实际上这是一次主动引起的异常(大体上可以这么理解)。那么在分析之前我们首先先看这个system_call_table是如何参与系统调用过程的呢?我们来看这样一张图。
本群免费分享有关于C/C++,Linux,golang,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,ffmpeg,TCP/IP,协程,DPDK,嵌入式等学习资料,请大家加群私聊管理领取。QQ群:1106675687在腾讯课堂有相关的直播,课程地址:https://ke.qq.com/course/417774?flowToken=1026140免费报名立即学习。请大家点点关注,后面持续更新,谢谢大家的观看。
首先我们分析一下这个过程,首先在用户空间中,我们编写的程序中有一个系统调用的过程,这个时候首先他会进入到系统调用的控制程序中去,在这个过程中首先要做的就是保存现场,然后就是要定位到这个系统调用列表中去。我们看到这个过程中首先程序会找到sys_call_table的基地址然后在进行相应的运算之后得出一个偏移地址,这里面是采用eax*4来得到这样一个地址的。入口地址实际上就是系统服务例程的基地址,我们具体需要什么样的功能实现都在这个服务程序中。
上面分析我们知道实际上eax就是提供一个索引来确定要调用sys_call_table中那个表项对用的系统调用。上图中就是一些具体的例子。其中的offset就是eax提供的索引编译当然是计算过的。然后是对应的symbol和表项,最后是系统中他们所在的位置。
那么接下来问题就来了,上面我们仅仅是知道系统调用过程中的上面部分的过程情况,那么在系统调用的执行过程中,具体代码的执行过程是什么样子的呢?这里面我们结合第四章讲义System Call(可以到网易云课堂中孟宁老师的Linux内核分析课程中下载)最后给出的系统调用过程的汇编伪代码以及实际的运行过程来分析一下system_call具体的执行过程。首先我们还是来看一张流程图(为什么这么多图呢因为这样可以少写点字,省得麻烦呀,图的形式更为清晰也容易理解)
我们看到这里面进程的调度时机发生在返回之前,有可能会有一些其他的外部请求来引起这样的进程调度过程,这里面我们实际上是结合讲义中的伪代码以及一些扩充的细节来完成的这个过程图。就像视频中所讲的那样实际上在系统从用户态陷入内核态的时候(trap~)就是开始调用的时机,然后系统就会按照上述的过程(实际上第一章图片是一个总体的描述,大家可以和流程图结合起来看)进行,然后再返回到用户态下。上面的图画的实在辛苦哦应该省去大家不少阅读时间吧,如果有什么错误欢迎指正出来啊。
(1)用户态:
首先先找到系统调用号,我们来看unistd.h头文件中这样一段代码:
#define __NR_restart_syscall 0
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
#define __NR_waitpid 7
......
#define _syscall0(type,name)
type name(void)
{
long __res;
__asm__ volatile ("int $0x80"
: "=a" (__res)
: "0" (__NR_##name));
__syscall_return(type,__res);
}
在这个头文件中,我们定义了一系列的宏,这些宏就是系统调用对用的系统调用号,比如for,它通过组装将name替换成fork之后进行二次展开就成了_NR_fork,因此在前面定义_NR_fork 就是2,因此fork对应的系统调用号就是2,而系统调用就是通过0x80号中断号找到对应的系统调用服务程序,而真正执行系统调用服务程序时就已经陷入内核态,在此之前我们需要将系统调用的调用号,调用参数进行传递,在陷入内核态前有这样一段汇编代码:(示例代码的系统调用有两个参数)
0: 89 da mov %ebx,%edx
2: 8b 4c 24 08 mov 0x8(%esp,1),%ecx
6: 8b 5c 24 04 mov 0x4(%esp,1),%ebx
a: b8 4a 00 00 00 mov $0x4a,%eax
f: cd 80 int $0x80
它首先把两个参数传入寄存器,然后将0x4a号(示例系统调用函数的系统调用号,不同的系统调用会不同,会根据前面得到的系统调用号传入eax)系统调用传入eax寄存器中,最后就调用int陷入内核态。
(2)内核态:
系统调用服务程序:
ENTRY(system_call)
pushl %eax # save orig_eax
SAVE_ALL
GET_THREAD_INFO(%ebp)
# system call tracing in operation
testb $(_TIF_SYSCALL_TRACE|_TIF_SYSCALL_AUDIT),TI_flags(%ebp)
jnz syscall_trace_entry
cmpl $(nr_syscalls), %eax
jae syscall_badsys
syscall_call:
call *sys_call_table(,%eax,4)
它首先将用户态的一些寄存器信息保存在自己的堆栈上(内核堆栈),save_all就是一个宏,他将依次压入:%es%ds%eax%ebp%edi%esi%edx%ecx%ebx,而es,ds,eax,ebp均有各自的用处,所以允许传递的最大参数的个数为后面5 个,如果更多就传递指针,通过copy_from_user函数在指针处获取参数 ,用户态信息保存完了,参数也保存了就call具体的系统函数,函数调用跳转表就是保存的系统函数的函数指针,通过系统调用号找到具体的系统函数就开始执行。
系统函数跳转表:
ENTRY(sys_call_table)
.long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */
.long sys_exit
.long sys_fork
.long sys_read
.long sys_write
.long sys_open /* 5 */
.long sys_close
.long sys_waitpid
.long sys_creat
.long sys_link
.long sys_unlink /* 10 */
.long sys_execve
首先恭喜您,能够认真的阅读到这里,如果对部分理解不太明白,建议先将文章收藏起来,然后对不清楚的知识点进行查阅,然后在进行阅读,相应你会有更深的认知。如果您喜欢这篇文章,就点个赞或者【关注我】吧!!