Socket与系统调用深度分析
实验环境:Linux-5.0.1 内核 32位系统的MenuOS
本文主要解决两个问题
- 用户态如何通过中断进入socket的系统调用
- socket抽象层如何通过多态的机制,来支持不同的传输层的协议。也就是socket作为父类,TCP/UDP为子类,父类指向子类对象,实现多态。
主要问题有下图的红色字体标出
<img src="https://img2018.cnblogs.com/blog/957240/201912/957240-20191219153934931-1462062478.png" width="500" height="400">#include<socket.h> int main(int argc, char *argv[]) { ... socket(...) ... return 0; }
该函数会调用socket,socket内核提供给我们的函数,要通过系统调用来使用。
调用流程
那么,在应用程序内,调用一个系统调用的流程是怎样的呢?
我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。
<img src="https://img2018.cnblogs.com/blog/957240/201912/957240-20191219154609677-335600228.png" width="500" height="300">如上图,系统调用执行的流程如下:
- 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
- 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
- CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
- 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
所以如果要找到socket的系统调用,关键就是找到socket的system_call入口,在Linux-5.0.1 内核中
https://github.com/mengning/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl 从中可以发现socket的系统调用位102号
<img src="https://img2018.cnblogs.com/blog/957240/201912/957240-20191219154304762-164213308.png" width="500" height="100">那么跟踪程序如果可以跟踪到socketcall,就可以观察到系统调用,下面通过gdb调试qume中的linux源代码来跟踪系统调用。
跟踪socketcall系统调用
#启动qemu 加载内核,不加-S,因为跟踪的系统调用,不是启动过程 qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s #打开一个新的终端 gdb file ~/linux-5.0.1/vmlinux b sys_socketcall target remote:1234 c #在qume中输入 replyhi
<img src="https://img2018.cnblogs.com/blog/957240/201912/957240-20191219154409452-901642349.png" width="600" height="200"> <img src="https://img2018.cnblogs.com/blog/957240/201912/957240-20191219154502167-2084708389.png" width="600" height="400">
可以看到程序停止在_sys_socketcall,该韩式纸SYSCALL_DEFINE2,也即是系统调用后,通过软中断,跳转到了该函数,查看该函数源码如下,gdb中的call = 1 ,正是传递到swich case语句中
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) { //根据call的不同之,从而执行不同的系统系统调用 当前call = 1 也就是执行 case SYS_SOCKET: // SYS_SOCKET的系统调用。 err = __sys_socket(a0, a1, a[2]); break; case SYS_BIND: err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_CONNECT: err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]); break; case SYS_LISTEN: err = __sys_listen(a0, a1); break; case SYS_ACCEPT: err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], 0); break; case SYS_GETSOCKNAME: err = __sys_getsockname(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_GETPEERNAME: err = __sys_getpeername(a0, (struct sockaddr __user *)a1, (int __user *)a[2]); break; case SYS_SOCKETPAIR: err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]); break; case SYS_SEND: err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], NULL, 0); break; case SYS_SENDTO: err = __sys_sendto(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], a[5]); break; case SYS_RECV: err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], NULL, NULL); break; case SYS_RECVFROM: err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3], (struct sockaddr __user *)a[4], (int __user *)a[5]); break; case SYS_SHUTDOWN: err = __sys_shutdown(a0, a1); break; case SYS_SETSOCKOPT: err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3], a[4]); break; case SYS_GETSOCKOPT: err = __sys_getsockopt(a0, a1, a[2], (char __user *)a[3], (int __user *)a[4]); break; case SYS_SENDMSG: err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_SENDMMSG: err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2], a[3], true); break; case SYS_RECVMSG: err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1, a[2], true); break; case SYS_RECVMMSG: 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: err = __sys_accept4(a0, (struct sockaddr __user *)a1, (int __user *)a[2], a[3]); break; default: err = -EINVAL; break; } return err; }
在linuxnet的lab3中查看replyhi源码,可以发现其实就是基本的socket - bind - listen - accept 过程这就不详细列出,结合上面的_sys_socketcall,可以猜想到会调用多次_sys_socketcall,进入switch case语句中。
<img src="https://img2018.cnblogs.com/blog/957240/201912/957240-20191219155159722-576966221.png" width="600" height="400">可以看到,调用了4次,分别call为 1 2 4 5,也对用服务器开始动作 socket - bind - listen -accept
客户端类似,这里不做演示。
###socket抽象层的多态的机制
从上上面的SYSCALL_DEFINE2中可以看到__sys_socket()函数
SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args){ 、、、 switch (call) { case SYS_SOCKET: err = __sys_socket(a0, a1, a[2]); break; 、、、 } //进入该函数 int __sys_socket(int family, int type, int protocol) { int retval; struct socket *sock; // socket位关键的数据结构 int flags; /* Check the SOCK_* constants for consistency. */ BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC); BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK); BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK); flags = type & ~SOCK_TYPE_MASK; if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK)) return -EINVAL; type &= SOCK_TYPE_MASK; if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK)) flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK; retval = sock_create(family, type, protocol, &sock); if (retval < 0) return retval; return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK)); } <net.h> struct socket { socket_state state; short type; unsigned long flags; struct socket_wq *wq; struct file *file; struct sock *sk; const struct proto_ops *ops; // *** };
其中最为核心的就是_struct proto_ops_,查看它的定义可以看到
<net.h> struct proto_ops { int family; struct module *owner; int (*release) (struct socket *sock); int (*bind) (struct socket *sock, struct sockaddr *myaddr, int sockaddr_len); int (*connect) (struct socket *sock, struct sockaddr *vaddr, int sockaddr_len, int flags); int (*socketpair)(struct socket *sock1, struct socket *sock2); int (*accept) (struct socket *sock, struct socket *newsock, int flags, bool kern); //...
可以这个结构体中有很多的函数指针,查看SYSCALL_DEFINE2中的__sys_connect
int __sys_connect(int fd, struct sockaddr __user *uservaddr, int addrlen) { struct socket *sock; struct sockaddr_storage address; int err, fput_needed; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; err = move_addr_to_kernel(uservaddr, addrlen, &address); if (err < 0) goto out_put; err = security_socket_connect(sock, (struct sockaddr *)&address, addrlen); if (err) goto out_put; err = sock->ops->connect(sock, (struct sockaddr *)&address, addrlen, sock->file->f_flags) //***
可以看到有一个sock->ops->connect函数指针的调用,我们这是调用的是TCP,查看ipv4/tcp_ipv4.c
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect, .connect = tcp_v4_connect, .disconnect = tcp_disconnect, .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, //...
这就是tcp中对对相应函数指针实现的初始化。调用时就会通过这个数据结构查到相应的函数。
不同的协议,就痛过不同的数据结构,写入不同的函数指针,从而实现了多态的机制。
参考资料