Socket与系统调用深度分析
〇,linux系统调用
本次实验的主要内容是从socket接口入手,通过跟踪相关函数在内核中的运行过程了解socket相关的系统调用是如何工作的。那么首当其冲需要解决的问题就是,什么是系统调用?socket等程序接口又是如何使用系统调用来实现自身功能的呢?
但凡学过操作系统的人都不难理解,操作系统分为用户态和内核态,应用程序一般工作在用户态,而操作系统则通过系统调用为工作在其上的进程提供服务:
正如上图所示,运行在用户态的函数xyz()通过中断进入内核态,再通过系统调用处理函数调用相关的服务历程。
一,分析用户函数
要分析socket接口与系统调用之间的关系,首先就要找出socket究竟调用了哪些系统调用。根据上一小节的内容,我们可以从对应的函数入手,找出相应的系统调用。
打开lab3中的main.c文件,在main函数中找到如下代码:
MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi); MenuConfig("hello", "Hello TCP Client", Hello);
按照函数调用的关系依次查找,如StartReplyhi()到Replyhi再到syswrapper.h中,找到如下代码:
#include/* internet socket */ PrepareSocket(addr,port) \ int sockfd = -1; \ struct sockaddr_in serveraddr; \ struct sockaddr_in clientaddr; \ socklen_t addr_len = sizeof(struct sockaddr); \ serveraddr.sin_family = AF_INET; \ serveraddr.sin_port = htons(port); \ serveraddr.sin_addr.s_addr = inet_addr(addr); \ memset(&serveraddr.sin_zero, 0, 8); \ sockfd = socket(PF_INET,SOCK_STREAM,0); #define InitServer() \ int ret = bind( sockfd, \ (struct sockaddr *)&serveraddr, \ sizeof(struct sockaddr)); \ if(ret == -1) \ { \ fprintf(stderr,"Bind Error,%s:%d\n", \ __FILE__,__LINE__); \ close(sockfd); \ return -1; \ } \ listen(sockfd,MAX_CONNECT_QUEUE);
/**/
#define
/**/
#define InitializeService() \
PrepareSocket(IP_ADDR,PORT); \
InitServer();
至此我们已经找到了如socket(),listen(),bind()以及close()等几个函数,通过结合linux系统调用表(https://github.com/mengning/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl)和其他网上资料,我们可以发现Linux 内核为所有与 socket 有关的操作提供了一个统一的系统调用入口,但是在用户程序界面上则通过 C 语言程序库 c.lib 提供诸多库函数,看起来好像都是独立的系统调用一样。内核中为 socket 设置的总入口为 sys_socketcall(),其代码在 net/socket.c 中,而该函数实际上则调用的是SYSCALL_DEFINE2:
1 /*与socket相关的系统调用总入口。 */ 2 /* 3 *函数的第一个参数 call 即为具体的操作码,而参数 args 为指向一个数组的指针,可以根据具体操作码的不同,确定从用户空间复制参数的数量; 4 */ 5 asmlinkage long sys_socketcall(int call, unsigned long __user *args) 6 { 7 unsigned long a[6]; 8 unsigned long a0,a1; 9 int err; 10 11 if(call<1||call>SYS_RECVMSG) 12 return -EINVAL; 13 14 /* copy_from_user should be SMP safe. */ 15 if (copy_from_user(a, args, nargs[call])) 16 return -EFAULT; 17 18 a0=a[0]; 19 a1=a[1]; 20 21 switch(call) 22 { 23 case SYS_SOCKET: 24 err = sys_socket(a0,a1,a[2]); 25 break; 26 case SYS_BIND: 27 err = sys_bind(a0,(struct sockaddr __user *)a1, a[2]); 28 break; 29 case SYS_CONNECT: 30 err = sys_connect(a0, (struct sockaddr __user *)a1, a[2]); 31 break; 32 case SYS_LISTEN: 33 err = sys_listen(a0,a1); 34 break; 35 case SYS_ACCEPT: 36 err = sys_accept(a0,(struct sockaddr __user *)a1, (int __user *)a[2]); 37 break; 38 case SYS_GETSOCKNAME: 39 err = sys_getsockname(a0,(struct sockaddr __user *)a1, (int __user *)a[2]); 40 break; 41 case SYS_GETPEERNAME: 42 err = sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); 43 break; 44 case SYS_SOCKETPAIR: 45 err = sys_socketpair(a0,a1, a[2], (int __user *)a[3]); 46 break; 47 case SYS_SEND: 48 err = sys_send(a0, (void __user *)a1, a[2], a[3]); 49 break; 50 case SYS_SENDTO: 51 err = sys_sendto(a0,(void __user *)a1, a[2], a[3], 52 (struct sockaddr __user *)a[4], a[5]); 53 break; 54 case SYS_RECV: 55 err = sys_recv(a0, (void __user *)a1, a[2], a[3]); 56 break; 57 case SYS_RECVFROM: 58 err = sys_recvfrom(a0, (void __user *)a1, a[2], a[3], 59 (struct sockaddr __user *)a[4], (int __user *)a[5]); 60 break; 61 case SYS_SHUTDOWN: 62 err = sys_shutdown(a0,a1); 63 break; 64 case SYS_SETSOCKOPT: 65 err = sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); 66 break; 67 case SYS_GETSOCKOPT: 68 err = sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); 69 break; 70 case SYS_SENDMSG: 71 err = sys_sendmsg(a0, (struct msghdr __user *) a1, a[2]); 72 break; 73 case SYS_RECVMSG: 74 err = sys_recvmsg(a0, (struct msghdr __user *) a1, a[2]); 75 break; 76 default: 77 err = -EINVAL; 78 break; 79 } 80 return err;
通过解读上述代码并对照相关的系统调用标,我们似乎已经找到了相关代码的系统调用入口,接下来我们可以通过gdb来验证我们的猜想。
二,跟踪系统调用
在gdb中,对sys_socketcall,sys_bind,sys_listen设置断点,并跟踪,结果如下:
上图的结果在两点上不符合我们的预期或者说值得探究:一,对与sys_bind和sys_listen的跟踪没有反馈;二,sys_socketcall在启动qemu是即被调用,且每次的调用次数(即call的值)又代表什么。
针对第一个问题,我们注意到每次跟踪sys_socketcall时,都会定位到这样一个函数即SYSCALL_DEFINE2()
通过查阅资料可知,实际上对sys_socketcall的调用是通过该函数实现的,而该函数的代码如下:
1 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args) 2 { 3 unsigned long a[AUDITSC_ARGS]; 4 unsigned long a0, a1; 5 int err; 6 unsigned int len; 7 8 if (call < 1 || call > SYS_SENDMMSG) 9 return -EINVAL; 10 call = array_index_nospec(call, SYS_SENDMMSG + 1); 11 12 len = nargs[call]; 13 if (len > sizeof(a)) 14 return -EINVAL; 15 16 /* copy_from_user should be SMP safe. */ 17 if (copy_from_user(a, args, len)) 18 return -EFAULT; 19 20 err = audit_socketcall(nargs[call] / sizeof(unsigned long), a); 21 if (err) 22 return err; 23 24 a0 = a[0]; 25 a1 = a[1]; 26 27 switch (call) { 28 case SYS_SOCKET: 29 err = __sys_socket(a0, a1, a[2]); 30 break; 31 case SYS_BIND: 32 err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); 33 break; 34 case SYS_CONNECT: 35 err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); 36 break; 37 case SYS_LISTEN: 38 err = __sys_listen(a0, a1); 39 break; 40 case SYS_ACCEPT: 41 err = __sys_accept4(a0, (struct sockaddr __user *)a1, 42 (int __user *)a[2], 0); 43 break; 44 case SYS_GETSOCKNAME: 45 err = 46 __sys_getsockname(a0, (struct sockaddr __user *)a1, 47 (int __user *)a[2]); 48 break; 49 case SYS_GETPEERNAME: 50 err = 51 __sys_getpeername(a0, (struct sockaddr __user *)a1, 52 (int __user *)a[2]); 53 break; 54 case SYS_SOCKETPAIR: 55 err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]); 56 break; 57 case SYS_SEND: 58 err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], 59 NULL, 0); 60 break; 61 case SYS_SENDTO: 62 err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], 63 (struct sockaddr __user *)a[4], a[5]); 64 break; 65 case SYS_RECV: 66 err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], 67 NULL, NULL); 68 break; 69 case SYS_RECVFROM: 70 err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], 71 (struct sockaddr __user *)a[4], 72 (int __user *)a[5]); 73 break; 74 case SYS_SHUTDOWN: 75 err = __sys_shutdown(a0, a1); 76 break; 77 case SYS_SETSOCKOPT: 78 err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3], 79 a[4]); 80 break; 81 case SYS_GETSOCKOPT: 82 err = 83 __sys_getsockopt(a0, a1, a[2], (char __user *)a[3], 84 (int __user *)a[4]); 85 break; 86 case SYS_SENDMSG: 87 err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1, 88 a[2], true); 89 break; 90 case SYS_SENDMMSG: 91 err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], 92 a[3], true); 93 break; 94 case SYS_RECVMSG: 95 err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1, 96 a[2], true); 97 break; 98 case SYS_RECVMMSG: 99 if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME)) 100 err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, 101 a[2], a[3], 102 (struct __kernel_timespec __user *)a[4], 103 NULL); 104 else 105 err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1, 106 a[2], a[3], NULL, 107 (struct old_timespec32 __user *)a[4]); 108 break; 109 case SYS_ACCEPT4: 110 err = __sys_accept4(a0, (struct sockaddr __user *)a1, 111 (int __user *)a[2], a[3]); 112 break; 113 default: 114 err = -EINVAL; 115 break; 116 } 117 return err; 118 }
其代码逻辑大致与sys_socket相同,即通过switch分支进入不同的分支处理,而在各个分支处理下,我们不难发现,实际上,系统中真正调用的是形如__sys_listen这样的函数,对于这样的发现,我们同样通过gdb跟踪进行验证。
我们首先将涉及到的系统调用都打上断点,接着重复之前的步骤加以跟踪,结果如下:
结果证明我们此时的猜想并没有错误,之前的问题也得到解决:涉及到socket的系统调用都使用统一的入口sys_socketcall,再通过SYSCALL_DEFINE2进入不同的分支,调用不同的系统调用,如在本次实验中,就包括sys_socket,sys_bind,sys_listen,sys_connect,sys_accept4等等。
三,函数分析与对不同协议的封装
socket接口可以实现对与不同协议的封装,具体这一机制是如何实现的,我们可以通过根据对上文中提到的内核函数进行分析略知一二:
1 asmlinkage long sys_socket(int family, int type, int protocol) 2 { 3 int retval; 4 struct socket *sock; 5 6 /* 根据协议族、套口类型、传输层协议创建套口 */ 7 retval = sock_create(family, type, protocol, &sock); 8 if (retval < 0) 9 goto out; 10 11 /* 为创建的套接口分配一个文件描述符并进行绑定 */ 12 retval = sock_map_fd(sock); 13 if (retval < 0) 14 goto out_release; /* 根据是否成功帮顶选择是否释放 */ 15 16 out: 17 return retval; 18 19 out_release: 20 sock_release(sock); 21 return retval; 22 }
根据上述代码,我们接着找到sock_creat:
1 /** 2 * 创建一个套接口 3 * family: 套接口协议族 4 * type: 套接口类型 5 * protocol: 传输层协议 6 * res: 输出参数,创建成功的套接口指针 7 * kern: 由内核还是应用程序创建。 8 */ 9 static int __sock_create(int family, int type, int protocol, struct socket **res, int kern) 10 { 11 int err; 12 struct socket *sock; 13 14 if (family < 0 || family >= NPROTO)/* 参数合法性检测 */ 15 return -EAFNOSUPPORT; 16 if (type < 0 || type >= SOCK_MAX) 17 return -EINVAL; 18 /** 19 * IPV4协议族的SOCK_PACKET类型套接口已经不被支持 20 * 为兼容旧程序,转换为PF_PACKET 21 */ 22 if (family == PF_INET && type == SOCK_PACKET) { 23 static int warned; 24 if (!warned) { 25 warned = 1; 26 printk(KERN_INFO "%s uses obsolete (PF_INET,SOCK_PACKET)\n", current->comm); 27 } 28 family = PF_PACKET; 29 } 30 31 /* 由安全模块对创建过程进行审计 */ 32 err = security_socket_create(family, type, protocol, kern); 33 if (err) 34 return err; 35 36 #if defined(CONFIG_KMOD) 37 if (net_families[family]==NULL)/* 相应的协议族在内核中尚不存在,加载模块以支持该协议族 */ 38 { 39 request_module("net-pf-%d",family); 40 } 41 #endif 42 43 /* 等待,直到锁被释放 */ 44 net_family_read_lock(); 45 if (net_families[family] == NULL) {/* 如果协议族仍然不存在,说明不支持此协议族 */ 46 err = -EAFNOSUPPORT; 47 goto out; 48 } 49 50 51 if (!(sock = sock_alloc())) {/* 分配与inode关联的套接口 */ 52 printk(KERN_WARNING "socket: no more sockets\n"); 53 err = -ENFILE; 54 goto out; 55 } 56 57 sock->type = type;/* 设置套接口类型。 */ 58 59 err = -EAFNOSUPPORT; 60 if (!try_module_get(net_families[family]->owner))/* 增加对协议族模块的引用,如果失败则退出 */ 61 goto out_release; 62 63 /* 调用协议族的创建方法,对IPV4来说,调用的是inet_create */ 64 if ((err = net_families[family]->create(sock, protocol)) < 0) 65 goto out_module_put; 66 if (!try_module_get(sock->ops->owner)) {/* 增加传输层模块的引用计数 */ 67 sock->ops = NULL; 68 goto out_module_put; 69 } 70 /* 增加了传输层模块的引用计数后,可以释放协议族的模块引用计数 */ 71 module_put(net_families[family]->owner); 72 *res = sock; 73 /* 通知安全模块,对创建过程进行检查。 */ 74 security_socket_post_create(sock, family, type, protocol, kern); 75 76 out: 77 net_family_read_unlock(); 78 return err; 79 out_module_put: 80 module_put(net_families[family]->owner); 81 out_release: 82 sock_release(sock); 83 goto out; 84 }
高亮部分显示,在sock_creat中调用协议族的创建方法创建一个套接口,对IPV4来说,就是调用inet_create。通过这样的方法,实现了对于不同的协议的封装。