Socket与系统调用深度分析
一.socket函数接口工作流程图
上一篇博文中我们简单分析了这个模型,本节里面我们将在此基础上深入内核里分析。
二.系统调用与中断相关概念
这里我们会涉及到一些概念,先让我们熟悉一下它们!
用户空间:指的就是用户可以操作和访问的空间,这个空间通常存放我们用户自己写的数据等等;而内核空间则是系统内核来操作的一块空间,这块空间里面存放系统内核的函数、接口等。
不管对于Linux还是Windows, 他们都具有自己用户空间和内核空间。当一个程序运行时,如果它是在用户空间下执行,我们把此时运行得程序的这种状态成为用户态,而当这段程序执行在内核的空间执行时,这种状态称为内核态。
在linux用户态下的我们是不能直接接触到kernel的,那我们该如何从用户态转到内核态呢?
用户空间的程序无法直接执行内核代码。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。
软件中断。Linux操作系统一般是通过软件中断从用户态切换到内核态。软件中断和我们常说的中断(硬件中断)不同之处在于,它是通过软件指令触发而并非外设引发的中断,也就是说,又是编程人员开发出的一种异常(该异常为正常的异常)。
那么在Linux下,这个异常具体就是调用int $0x80的汇编指令,这条汇编指令将产生向量为0x80的编程异常。
之所以系统调用需要借助这个中断异常来实现,是因为这个异常实际上就是通过系统门陷入内核(除了int 0x80外用户空间还可以通过int3——向量3、into——向量4 、bound——向量5等异常指令进入内核,而其他异常无法被用户空间程序利用,都是由系统使用的)。
好了,现在我们知道是先通过软件中断调用了0x80的这个编程异常,这个编程异常对应的是中断描述符表IDT中的第128项——也就是对应的系统门描述符。门描述符中含有一个预设的内核空间地址,它指向了系统调用处理程序:system_call()(别和系统调用服务程序混淆,这个程序在entry.S文件中用汇编语言编写)。
现在我们就知道通过系统描述符中的内核空间来找到系统调用程序,从而进入到内核态。可很显然,所有的系统调用都会统一地转到这个地址,但Linux一共有三百多个系统调用都从这里进入内核后,又该如何派发到它们到各自的服务程序去呢?
首先Linux为每个系统调用都进行了编号 (0—NR_syscall),同时在内核中保存了一张系统调用表sys_call_table ,该表中保存了系统调用编号和其对应的服务例程,因此在系统调入通过系统门陷入内核前,需要把系统调用号一并传入内核。在x86上,这个传递动作是通过在执行int0x80前把调用号装入eax寄存器实现的。这样系统调用处理程序一旦运行,就可以从eax中得到数据,然后再去系统调用表中寻找相应服务例程了。
下面我们将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在上一节构件的linux5.0.1内核下进一步跟踪验证。
三.跟踪并分析socket api直至内核代码
我们在上一节构件的MenuOS环境下进行本节的实验
1.原理解析
首先我们查看内核源码内系统调用表与系统调用编号
gedit ~/MenuOS/linux-5.0.1/arch/sh/include/uapi/asm/unistd_64.h
可以看到该头文件下一共定义了共394个编号
我们本节感兴趣的是和socket相关的编号。如下图(编号220-236)
获取到了系统调用号有什么用呢?系统又是如何利用这些系统调用号的呢?
以系统调用号NR_syscall作为下标,可找出系统调用表sys_call_table中对应表项的内容,它正好是该系统调用的响应函数sys_name的入口地址。有了sys_call_table这张表,就很容易根据特定系统调用在表中的偏移量,找到对应的系统调用响应函数的入口地址。
用户空间的进程执行一个系统调用的时候,这个系统调用号就被用来指明到底是要执行哪个系统调用。而这一过程不需要指明系统调用的名称。
由于所有的系统调用都会使用户态陷入内核态,与此同时把系统调用号一并传给内核。
2.系统调用命令转换为syscall中断
2.1系统调用模块初始化
start_kernel --> trap_init --> cpu_init --> syscall_init
2.2系统调用表初始化
由/linux-5.0.1/arch/x86/entry/entry_64.S 完成
2.3执行系统调用
SYM_CODE_START(entry_SYSCALL_64)
... /* IRQs are off. */ movq %rax, %rdi movq %rsp, %rsi call do_syscall_64 /* returns with IRQs disabled */ * [do_syscall_64](https://github.com/torvalds/linux/blob/ab851d49f6bfc781edd8bd44c72ec1e49211670b/arch/x86/entry/common.c#L282) #ifdef CONFIG_X86_64 __visible void do_syscall_64(unsigned long nr, struct pt_regs *regs) { ... if (likely(nr < NR_syscalls)) { nr = array_index_nospec(nr, NR_syscalls); regs->ax = sys_call_table[nr](regs); ... } #endif
2.4内核开始服务
系统调用的参数由各通用寄存器传递,然后执行syscall,以内核态进入入口地址system_call
3.socket内核处理函数跟踪分析
基于已经集成在MenuOS中的replyhi程序来追踪socket调用过程
首先打开MenuOS系统
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append nokaslr -s
接着我们在MenuOS目录下另外新开一个终端。输入如下命令用gdb连接Menu OS服务器,端口1234,开始调试Menu OS系统:
gdb file linux-5.0.1/vmlinux target remote:1234
接着我们打上10个断点 输入
b __sys_socket
b __sys_bind
b __sys_listen
b __sys_connect
b __sys_accept4
b __sys_recvmsg
b __sys_sendmsg
b __sys_recvfrom
b __sys_sendto
b __sys_shutdown
查看我们打的断点信息
Info breakpoints
接着在gdb中按c运行Menu OS
可以看到,首先就等待了。然后我们在Menu OS中打开服务器,即输入replyhi:
此时我们就在终端中看到运行到第一个断点处停下。
输入list可以看到此处的函数源码
接着我们继续输入c继续运行,想要查看源码的位置输入list即可看到
可以看到依次运行了sys_bind listen accept4,执行到accept时,中断,然后在Menu OS中输入hello打开客户端,如下图:
之后就可以依次跟踪到后续的函数。在需要追踪的函数出,输入list可以观察到相应的源码。
总结
通过gdb跟踪的断点可以看到完整的这个hello/hi的内核执行过程。
replyhi调用了服务端的socket()、bind()、listen()、accept()、recv()、send()、close()
MenuOS中输入hello之后调用了客户端的socket()、connect()、send()、recv()、close()