zoukankan      html  css  js  c++  java
  • Socket与系统调用深度分析

    Socket与系统调用深度分析

    实验环境:Linux-5.0.1 内核 32位系统的MenuOS

    本文主要解决两个问题

    • 用户态如何通过中断进入socket的系统调用
    • socket抽象层如何通过多态的机制,来支持不同的传输层的协议。也就是socket作为父类,TCP/UDP为子类,父类指向子类对象,实现多态。

    主要问题有下图的红色字体标出

    #include<socket.h>
    int main(int argc, char *argv[])
    {  ...
       socket(...)
       ...
       return 0;
    }
    
    

    该函数会调用socket,socket内核提供给我们的函数,要通过系统调用来使用。

    调用流程

    那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

    我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

    如上图,系统调用执行的流程如下:

    1. 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数
    2. 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
    3. CPU软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
    4. 系统调用处理函数 调用 系统调用服务例程 ( 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号

    那么跟踪程序如果可以跟踪到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
    

    可以看到程序停止在_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语句中。

    可以看到,调用了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中对对相应函数指针实现的初始化。调用时就会通过这个数据结构查到相应的函数。

    不同的协议,就痛过不同的数据结构,写入不同的函数指针,从而实现了多态的机制。

    参考资料

    https://www.cnblogs.com/fasionchan/p/9431784.html

  • 相关阅读:
    (转)Python之路,Day6
    (转)函数作用域,匿名函数,函数式编程,面向过程,面向对象
    (转)面向对象编程初步
    day26-多态、封装、反射
    (转)面向对象进阶
    MySql-Mysql技术内幕~SQL编程学习笔记(1)
    Spring MVC-学习笔记(4)数据绑定流程
    Mybatis-学习笔记(10)调用存储过程、存储函数
    Mybatis-学习笔记(9)Mybatis3+spring4+springMVC
    Mybatis-学习笔记(8)常用的注解
  • 原文地址:https://www.cnblogs.com/HMYaa/p/12068287.html
Copyright © 2011-2022 走看看