Socket与系统调用深度分析
本次实验将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,本次实验基于Linux内核5.0.0版本Ubuntu操作系统,32位MenuOS。
一、理论分析
Socket API编程接口之上可以编写基于不同网络协议的应用程序,Socket接口在用户态通过系统调用机制进入内核,本次实验的目的是追踪在调用了Socket API接口之后,系统如何从用户态转到了内核态,以及在内核态中发生了什么,在做实验之前要明白以下基础概念:
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。如果所有的程序都能使用这些指令,那么你的系统一天死机n回就不足为奇了。所以,CPU将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通的应用程序只能使用那些不会造成灾难的指令。Intel的CPU将特权级别分为4个级别:RING0,RING1,RING2,RING3。
linux的内核是一个有机的整体。每一个用户进程运行时都好像有一份内核的拷贝,每当用户进程使用系统调用时,都自动地将运行模式从用户级转为内核级,此时进程在内核的地址空间中运行。
当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态(或简称为内核态)。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈。每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。当正在执行用户程序而突然被中断程序中断时,此时用户程序也可以象征性地称为处于进程的内核态。因为中断处理程序将使用当前进程的内核栈。这与处于内核态的进程的状态有些类似。
在上图中,我们通过调用中断指令 int 0x80实现由用户态转向内核态,,当运行到int 0x80这条中断指令时,跳转到entry_INT80_32,这是liunx系统中所有系统调用的入口点。entry_INT80_32不是一段普通的函数,它是一段汇编代码,同时,我们将系统调用号用eax寄存器进行传递,entry_INT80_32通过系统调用号来查询对应的内核处理函数并跳转到相应的内核处理函数执行,完毕后再按顺序逐步返回到用户态。
那么int 0x80这条指令在哪里初始化的呢,通过查资料我们发现Linux内核在初始化阶段完成了对页式虚存管理的初始化以后,便调用trap_int()和init_IRQ()两个函数进行中断机制的初始化。其中trap_init()主要对系统保留的中断向量的初始化,而init_IRQ()则主要是用于外设的中断。
在这里我们通过gdb打断点验证下。
由上图可知调用栈为:startup_32_smp-->i386_start_kernel -->start_kernel --> trap_init --> idt_setup_traps-->idt_setup_from_table,在idt_setup_from_table中int 0x80就被初始化了,初始化之后,我们的程序的进行系统调用会统一通过这个入口进入内核态。
二、实验过程
在上次实验中我们实现了一个利用socket的基于TCP的连接,完成了客户端于服务器之间的hello/hi功能,本次实验会结合源码分析在这个通信的过程中,系统底层到底发生了什么。
在main函数中我们发现,当在MenuOS中输入replyhi时,实际上我们调用了StartReplyhi这个函数,那么再来看StartReplyhi的源代码。
我们发现StartReplyhi在满足条件时调用了Replyhi这个函数。
我们发现在Replyhi()函数中依次调用了六个函数:InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()、ShutdownService(),大概就是这六个函数调用了socket API,继续看它们的源码。
InitializeService()的宏定义,调用了PrepareSocket()和InitServer()两个函数,继续看源码。
在初始化的时候,调用了<arpa/inet.h>提供的socket(),bind(),listen()函数。
在ServiceStart()函数中,调用了accept()函数。
在RecvMsg()和SendMsg()中调用了revc()和send()这两个函数。
在ServiceStop()中调用了close()这个函数。
在ShutdownService()中调用了close()函数,至此socket连接结束。
我们再来看客户端代码。
依次调用了OpenRemoteService()、SendMsg()、RecvMsg()、CloseRemoteService()这几个函数,我们再到<syswrapper.h>中寻找它们的定义。
至此我们搞清楚了hello/hi程序是如何调用Linux系统提供的Socket API。
(1)服务端
加载套接字库,创建套接字(socket());
绑定套接字到一个IP地址和一个端口上(bind());
将套接字设置为监听模式等待连接请求(listen());
请求到来后,接受连接请求,返回一个新的对应于此次连接的套接字(accept());
用返回的套接字和客户端进行通信(send()/recv());
返回,等待另一个连接请求;
关闭套接字,关闭加载的套接字库(closesocket())。
(2)客户端
加载套接字库,创建套接字(socket());
向服务器发出连接请求(connect());
和服务器进行通信(send()/recv());
关闭套接字,关闭加载的套接字库(closesocket())。
打开gdb,打上断点,连接远程服务器,打开MenuOS,执行hello/hi程序。
共发现14个断点,我们根据返回的call值到源代码中查找这些sys_socketcall到底是在实现哪些功能,我们根据提示找到SYSCALL_DEFINE2所在的代码,在目录LinuxKernel/linux-5.0.1/net/socket.c中,下面是截取的SYSCALL_DEFINE2定义的部分。
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args) { unsigned long a[AUDITSC_ARGS]; unsigned long a0, a1; int err; unsigned int len; if (call < 1 || call > SYS_SENDMMSG) return -EINVAL; call = array_index_nospec(call, SYS_SENDMMSG + 1); len = nargs[call]; if (len > sizeof(a)) return -EINVAL; /* copy_from_user should be SMP safe. */ if (copy_from_user(a, args, len)) return -EFAULT; err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); if (err) return err; a0 = a[0]; a1 = a[1]; switch (call) { case SYS_SOCKET: #call=1 err = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: #call=2 err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: #call=3 err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: #call=4 err = __sys_listen(a0, a1); break; case SYS_ACCEPT: #call=5 err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: #call=6 err = __sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_GETPEERNAME: #call=7 err = __sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_SOCKETPAIR: #call=8 err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]); break; case SYS_SEND: #call=9 err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], NULL, 0); break; case SYS_SENDTO: #call=10 err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], a[5]); break; case SYS_RECV: #call=11 err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], NULL, NULL); break; case SYS_RECVFROM: #call=12 err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], (int __user *)a[5]); break; case SYS_SHUTDOWN: #call=13 err = __sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: #call=14 err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); break; case SYS_GETSOCKOPT: #call=15 err = __sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); break; case SYS_SENDMSG: #call=16 err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_SENDMMSG: #call=17 err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], true); break; case SYS_RECVMSG: #call=18 err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_RECVMMSG: #call=19 if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME)) err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], (struct __kernel_timespec __user *)a[4], NULL); else err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], NULL, (struct old_timespec32 __user *)a[4]); break; case SYS_ACCEPT4: #call=20 err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], a[3]); break; default: err = -EINVAL; break; } return err;
根据之前的14次断点的返回信息,我们对应每个call值对应的系统调用,原来的断点值序列为:1,1,1,1,2,4,5,1,3,10,9,10,9,5。
对应的系统调用分别为:socket,socket,socket,socket,bind,listen,accept,socket,connect,sendto,send,sendto,send,accept。
与之前分析的客户端与服务器端的函数调用相匹配,至此实验完成。