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

    Socket与系统调用深度分析

    可以想象的是,当应用程序调用socket()接口,请求操作系统提供服务时,必然会系统调用,内核根据发起系统调用时传递的系统调用号,判断要提供何种服务,具体来讲,若为socket对应的调用号,则执行socket对应的中断服务程序。当服务程序执行结束,便中断返回,从内核态再回到用户态,socket()系统调用也就执行完毕了。

    本次实验,我们关心三个问题:
    1.应用程序如何如何请求系统调用,或者说,如何进入内核态。
    2.中断服务程序之间的调用关系,他是如何跳转到我们需要的服务程序。
    3.socket为了完成我们的调用,在初始化时做了哪些事。

    应用程序调用socket

    还是使用我们之前编写的hello/hi聊天程序,在ubunu上,用客户端client来调试,观察socket()的执行过程。

    准备:

    为了能够调试libc库的内容需要下载libc库的源码,还有hello/hi聊天程序,具体步骤如下
    1.首先安装glibc的符号表,安装方法:
    sudo apt-get install libc6-dbg
    2.调试libc需要转到对应的源文件,借助了libc的开源,我们可以下载libc的源码,在调试时就能看到执行的位置:
    sudo apt-get source libc6-dev
    注意你下载的libc源码路径,后面调试过程会用到。
    3.新建源文件: clinet.c

    #include <stdio.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <string.h>
    #include <sys/socket.h>
    #include <netinet/in.h>
    #include <arpa/inet.h>
    #define MAX_len 1024
    int sock_fd;
    struct sockaddr_in add;
    int main()
    {
            int ret;
            char buf[MAX_len]={0};
            char buf_rec[MAX_len]={0};
            char buf_p[5]={"0"};
            memset(&add,0,sizeof(add));
            add.sin_family=AF_INET;
            add.sin_port=htons(8000);
            add.sin_addr.s_addr=inet_addr("127.0.0.1");
    
            if((sock_fd=socket(PF_INET,SOCK_STREAM,0))<=0)
            {
                    perror("socket");
                    return 1;
            }
            if((ret=connect(sock_fd,(struct sockaddr*)& add,sizeof(struct sockaddr)))<0)
            {
                    perror("connet");
                    return 1;
            }
            if((ret=send(sock_fd,(void*)buf_p,strlen(buf),0))<0)
            {
                    perror("recvfrom");
                    return 1;
            }
            while (1)
            {
                    scanf("%s",buf);
                    if((ret=send(sock_fd,(void*)buf,sizeof(buf),0))<0)
                    {
                            perror("sendfrom1");
                            return 1;
                    }
                    if((ret=recv(sock_fd,(void*)buf_rec,sizeof(buf_rec),0))<0)
                    {
                            perror("recvfrom1");
                            return 1;
    
                    }
                    printf("%s
    ",buf_rec);
            }
            return 0;
    }
    
    

    开始调试:

    1.编译文件并生成调试信息:
    gcc -o - g client client.c
    2.执行gdb命令并调试: gdb client

    (gdb) file client
    Load new symbol table from "client"? (y or n) y
    Reading symbols from client...done.
    (gdb) b 23
    Breakpoint 1 at 0x40091d: file client.c, line 23.
    (gdb) c
    The program is not being run.
    (gdb) run 
    Starting program: /home/netlab/netlab/systemcall/client 
    
    Breakpoint 1, main () at client.c:23
    23	        if((sock_fd=socket(PF_INET,SOCK_STREAM,0))<=0)
    

    将断点设在了23行,也就是第一次执行socket()的那一行,然后运行程序,使程序在23行停住,接下来使用step指令进入socket()内部,分析socket内部如何实现的。

    (gdb) s
    socket () at ../sysdeps/unix/syscall-template.S:84
    84	../sysdeps/unix/syscall-template.S: No such file or directory.
    

    但是这里提示了我们将要跳转的程序不存在,这是由于我们的libc上并没有源代码,这也是我们准备时要下载源代码的原因,根据他提示的目录,我们使用directory glibc-2.23/sysdeps/unix/命令将下载的libc的源代码装载到gdb,然后再次调试:

    (gdb) directory glibc-2.23/sysdeps/unix/
    Source directories searched: /home/netlab/netlab/systemcall/glibc-2.23/sysdeps/unix:$cdir:$cwd
    (gdb) s
    socket () at ../sysdeps/unix/syscall-template.S:85
    85		ret
    (gdb) l
    80	
    81	/* This is a "normal" system call stub: if there is an error,
    82	   it returns -1 and sets errno.  */
    83	
    84	T_PSEUDO (SYSCALL_SYMBOL, SYSCALL_NAME, SYSCALL_NARGS)
    85		ret
    86	T_PSEUDO_END (SYSCALL_SYMBOL)
    87	
    88	#endif
    89	
    (gdb) 
    

    程序跳入了systemcall-template.s就返回了,从这两句都是宏定义,并看不出什么内容,实际上,这是系统调用生成的模板,从名字也大致能猜出来,这里规定了常规的系统调用的格式。
    所以目前看来通过gdb调试到系统调用是不能实现了,而且,32位与64位在这里遇到的情况都一致,所以我们跳过调试,分析一下libc的源码。

    socket glibc库实现:

    首先通过一个重定位将socket重定位为__socket

    #define __socket socket
    #define __recvmsg recvmsg
    #define __bind bind
    #define __sendto sendto
    

    然后在库文件实现了__socket():

    int __socket (int fd, int type, int domain)
    {
    	#ifdef __ASSUME_SOCKET_SYSCALL
    	  return INLINE_SYSCALL (socket, 3, fd, type, domain);
    	#else
    	  return SOCKETCALL (socket, fd, type, domain);
    	#endif
    }
    libc_hidden_def (__socket)
    weak_alias (__socket, socket)
    

    在__socket()的内部调用了SOCKETCALL或INLINE_SYSCALL,最终它们都会转换为INLINE_SYSCALL,INLINE_SYSCALL与体系结构紧密相关,对应于x86_的架构,实现如下:

    # define INLINE_SYSCALL(name, nr, args...) 
      ({									      
        unsigned long int resultvar = INTERNAL_SYSCALL (name, , nr, args);	      
        if (__glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (resultvar, )))	      
          {									      
    	__set_errno (INTERNAL_SYSCALL_ERRNO (resultvar, ));		      
    	resultvar = (unsigned long int) -1;				      
          }									      
        (long int) resultvar; })
    
    #undef INTERNAL_SYSCALL
    #define INTERNAL_SYSCALL(name, err, nr, args...)			
    	internal_syscall##nr (SYS_ify (name), err, args)
    

    这里根据参数的数量,又会转换为:

    #define internal_syscall3(number, err, arg1, arg2, arg3)		
    ({									
        unsigned long int resultvar;					
        TYPEFY (arg3, __arg3) = ARGIFY (arg3);			 	
        TYPEFY (arg2, __arg2) = ARGIFY (arg2);			 	
        TYPEFY (arg1, __arg1) = ARGIFY (arg1);			 	
        register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;			
        register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;			
        register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;			
        asm volatile (							
        "syscall
    	"							
        : "=a" (resultvar)							
        : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3)			
        : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);			
        (long int) resultvar;						
    })
    

    这里采用了内嵌汇编的形式,将参数用rdx、rsi、rdi来存储,中断号用eax存储,发起软中断内核也就会相应中断,进入中断处理程序,不仅是socket,bind、listen、accept等都是如此,到此,应用程序的调试部分结束。

    内核响应中断:

    为了能看到内核如何响应应用程序的socket请求,我们用qemu+gdb调试内核linux-5.0.1,观察socket请求时内核响应的过程。
    1.以调试状态运行menuos,注意要添加上client程序,使其称为一个menuos的命令,方便调试
    2.分析一下断点的位置,为了能观察到内核对socket的响应,显然应该在响应的函数调用路径上打上断点,以方便调试,但是断点不能设在所有中断的入口,那样我们很难得到我们想要的中断响应,最好的位置就是socket系统调用处理程序的入口,在这个位置,只有socket请求能触发,保证了我们能直接分析,那么如何能找到系统调用的入口呢?
    内核的arch/x86/entry/syscalls内就有x86体系下的所有中断入口的描述,为了向前兼容,分为32与64位的中断入口:
    32位:

    99	i386	statfs			sys_statfs			__ia32_compat_sys_statfs
    100	i386	fstatfs			sys_fstatfs			__ia32_compat_sys_fstatfs
    101	i386	ioperm			sys_ioperm			__ia32_sys_ioperm
    102	i386	socketcall		sys_socketcall			__ia32_compat_sys_socketcall
    103	i386	syslog			sys_syslog			__ia32_sys_syslog
    104	i386	setitimer		sys_setitimer			__ia32_compat_sys_setitimer
    105	i386	getitimer		sys_getitimer			__ia32_compat_sys_getitimer
    

    64位:

    ......
    40	common	sendfile		__x64_sys_sendfile64
    41	common	socket			__x64_sys_socket
    42	common	connect			__x64_sys_connect
    43	common	accept			__x64_sys_accept
    44	common	sendto			__x64_sys_sendto
    
    ......
    

    对于32程序,显然应该定位于32位的系统调用入口,64位的程序,应该定位于64位的入口。我们将分别看着两种程序对应的中断入口:
    首先是32位,只有一个socket的系统调用,并没有bind、listen等的系统调用,通过之前的实验我们知道,这是因为socket系统调用会在系统调用的服务程序中实现分流,后面的调试也会证实这一点,因此我们将断点设置为__ia32_compat_sys_socketcall,运行menuos,并运行client程序,gdb会在进入__ia32_compat_sys_socketcall时停住:

    (gdb) b __ia32_compat_sys_socketcall
    Breakpoint 3 at 0xffffffff818474b0: file net/compat.c, line 718.
    (gdb) c
    Continuing.
    
    Breakpoint 3, __ia32_compat_sys_socketcall (regs=0xffffc900001eff58)
        at net/compat.c:718
    718	COMPAT_SYSCALL_DEFINE2(socketcall, int, call, u32 __user *, args)
    
    

    看一下这个函数的内容:

    718	COMPAT_SYSCALL_DEFINE2(socketcall, int, call, u32 __user *, args)
    719	{
             ......
    723		int ret;
    725		if (call < SYS_SOCKET || call > SYS_SENDMMSG)
    726			return -EINVAL;
    727		len = nas[call];
    728		if (len > sizeof(a))
    729			return -EINVAL;
    730	
    731		if (copy_from_user(a, args, len))
    733	
    734		ret = audit_socketcall_compat(len / sizeof(a[0]), a);
    735		if (ret)
    736			return ret;
    
    738		a0 = a[0];
    739		a1 = a[1];
    740	
    741		switch (call) {
    742		case SYS_SOCKET:
            		ret = __sys_socket(a0, a1, a[2]);
    744			break;
    745		case SYS_BIND:
    746			ret = __sys_bind(a0, compat_ptr(a1), a[2]);
    747			break;
    748		case SYS_CONNECT:
    749			ret = __sys_connect(a0, compat_ptr(a1), a[2]);
    750			break;
    751		case SYS_LISTEN:
    752			ret = __sys_listen(a0, a1);
        ......
    

    显然,这个处理程序是socket一类操作的总入口,它首先获取了系统调用的参数,然后根据请求服务的类型,跳转到不同的处理程序,实现了分发,继续观察函数的调用:

    (gdb) b __sys_socket
    Breakpoint 4 at 0xffffffff817eea40: file net/socket.c, line 1498.
    (gdb) c
    Continuing.
    
    Breakpoint 4, __sys_socket (family=2, type=1, protocol=0) at net/socket.c:1498
    1498	{
    1499		int retval;
    1500		struct socket *sock;
    1501		int flags;
    1503		/* Check the SOCK_* constants for consistency.  */
    1504		BUILD_BUG_ON(SOCK_CLOEXEC != O_CLOEXEC);
    1505		BUILD_BUG_ON((SOCK_MAX | SOCK_TYPE_MASK) != SOCK_TYPE_MASK);
    1506		BUILD_BUG_ON(SOCK_CLOEXEC & SOCK_TYPE_MASK);
    1507		BUILD_BUG_ON(SOCK_NONBLOCK & SOCK_TYPE_MASK);
    1508	
    1509		flags = type & ~SOCK_TYPE_MASK;
    1510		if (flags & ~(SOCK_CLOEXEC | SOCK_NONBLOCK))
    1511			return -EINVAL;
    1512		type &= SOCK_TYPE_MASK;
    1514		if (SOCK_NONBLOCK != O_NONBLOCK && (flags & SOCK_NONBLOCK))
    1515			flags = (flags & ~SOCK_NONBLOCK) | O_NONBLOCK;
    1516	
    1517		retval = sock_create(family, type, protocol, &sock);
    1518		if (retval < 0)
    1519			return retval;
    1520	
    1521		return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    1522	}
    

    在__sys_socket()函数内部只检查了一下参数,就跳转到sock_creat()执行了

    (gdb) b __sock_create
    Breakpoint 7 at 0xffffffff817ec9a0: file net/socket.c, line 1363.
    (gdb) c
    Continuing.
    
    Breakpoint 5, __sys_socket (family=2, type=1, protocol=0) at net/socket.c:1517
    1517		retval = sock_create(family, type, protocol, &sock);
    (gdb) c
    Continuing.
    
    Breakpoint 7, __sock_create (net=0xffffffff824e94c0 <init_net>, family=2, type=1, 
        protocol=0, res=0xffffc90000047e98, kern=0) at net/socket.c:1363
    1363		if (family < 0 || family >= NPROTO)
    (gdb) l
    1358		const struct net_proto_family *pf;
    1359	
    1360		/*
    1361		 *      Check protocol is in range
    1362		 */
    1363		if (family < 0 || family >= NPROTO)
    1364			return -EAFNOSUPPORT;
    1365		if (type < 0 || type >= SOCK_MAX)
    1366			return -EINVAL;
    			......
    1373		if (family == PF_INET && type == SOCK_PACKET) {
    1374			pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)
    ",
    1375				     current->comm);
    1376			family = PF_PACKET;
    1377		}	
    1379		err = security_socket_create(family, type, protocol, kern);
    1388		sock = sock_alloc();
    			......
    			
    1389		if (!sock) {
    1390			net_warn_ratelimited("socket: no more sockets
    ");
    1391			return -ENFILE;	/* Not exactly a match, but its the
    1392					   closest posix thing */
    1393		}
    1394	
    1395		sock->type = type;
    1396	
    (gdb) l
    1408		rcu_read_lock();
    1409		pf = rcu_dereference(net_families[family]);
    1410		err = -EAFNOSUPPORT;
    1411		if (!pf)
    1412			goto out_release;
    (gdb) l
    1418		if (!try_module_get(pf->owner))
    1419			goto out_release;
    1422		rcu_read_unlock();
    1423	
    1424		err = pf->create(net, sock, protocol, kern);
    		......
    1443		*res = sock;
    注:代码有所删减
    

    最初设断点在sock_reate发现到达不了,查看内核代码后,发现要设断点在__sock_create才行,可能是sock_creat被重定向了。继续看__sock_create:
    err = security_socket_create(family, type, protocol, kern);先检查了一下是否合法,然后就执行了最关键的函数
    sock = sock_alloc();
    为了理解这行代码,我们需要知道,sock是struct socket的一个变量,这个接口体是socket的核心,他的内容如下:

    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;
    };
    

    State是当前socket的状态,用于表示连接和未连接,type表示socket服务的类型,如TCP服务的SOCK_STREAM型,flags表示标志,如SOCK_ASYNC_NOSPACE,wq是等待队列,因为一个socket可能会有多个请求,file是指的文件,因为socket也可以被当作文件看待,所有会有这个指针,兼容文件的操作。Sk是非常重要的,也是非常大的,负责记录协议相关内容。这样的设置使得socket具有很好的协议无关性,可以通用,ops是socket与服务相关的基本操作的指针,这是linux的通常用法,将一个对象的操作用集合子啊一个函数指针的结构体中。

    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);
    	int		(*getname)   (struct socket *sock,
    				      struct sockaddr *addr,
    				      int peer);
    	__poll_t	(*poll)	     (struct file *file, struct socket *sock,
    				      struct poll_table_struct *wait);
    	int		(*ioctl)     (struct socket *sock, unsigned int cmd,
    				      unsigned long arg);
    	int		(*listen)    (struct socket *sock, int len);
    	......
    };
    

    再回到调试的程序,sock_alloc()分配了一个socket结构体,内部又是如何实现的呢?继续设断点观察:

    (gdb) b sock_alloc
    Breakpoint 8 at 0xffffffff817ec230: file net/socket.c, line 569.
    (gdb) c
    Continuing.
    
    Breakpoint 8, sock_alloc () at net/socket.c:569
    569		inode = new_inode_pseudo(sock_mnt->mnt_sb);
    (gdb) l
    564	struct socket *sock_alloc(void)
    565	{
    566		struct inode *inode;
    567		struct socket *sock;
    568	
    569		inode = new_inode_pseudo(sock_mnt->mnt_sb);
    570		if (!inode)
    571			return NULL;
    572	
    573		sock = SOCKET_I(inode);
    (gdb) l
    574	
    575		inode->i_ino = get_next_ino();
    576		inode->i_mode = S_IFSOCK | S_IRWXUGO;
    577		inode->i_uid = current_fsuid();
    578		inode->i_gid = current_fsgid();
    579		inode->i_op = &sockfs_inode_ops;
    580	
    581		return sock;
    582	}
    

    sock_alloc内部实现了两个结构的创建,磁盘文件inode、struct socket结构,除此之外,还为inode赋值,此时,问题问题又聚集在了SOCKET_I(),按照这里来看,SOCKET_I应该是创建socket的位置,将inode作为参数传递,确实有点难以理解内部是如何创建struct socket的,想继续深入看看,但遗憾的是,SOCKET_I函数是内联的,所以并不能跳转到函数内部,只能通过源码分析了。

    static inline struct socket *SOCKET_I(struct inode *inode)
    {
    	return &container_of(inode, struct socket_alloc, vfs_inode)->socket;
    }
    

    整个函数只有一行代码,container_of()是一个非常经典的宏,在这里,对于container_of(A,B,C);得到的就是位于结构体A中排在的第一个类型为B的域。即inode->socket_alloc->socket,也就是在inode节点中第一个域为socket_alloc,而socket_alloc有socket域,socket_alloc域如下:

    struct socket_alloc {
    	struct socket socket;
    	struct inode vfs_inode;
    };
    

    那这个inode节点如何创建的呢?在前面提到的sock_alloc函数中,调用了new_inode_pseudo函数来实现的,他实现如下:

    struct inode *new_inode_pseudo(struct super_block *sb)
    {
    	struct inode *inode = alloc_inode(sb);
    
    	if (inode) {
    		spin_lock(&inode->i_lock);
    		inode->i_state = 0;
    		spin_unlock(&inode->i_lock);
    		INIT_LIST_HEAD(&inode->i_sb_list);
    	}
    	return inode;
    }
    

    这里调用了alloc_inode函数:

    static struct inode *alloc_inode(struct super_block *sb)
    {
    	struct inode *inode;
    
    	if (sb->s_op->alloc_inode)
    		inode = sb->s_op->alloc_inode(sb);
    	else
    		inode = kmem_cache_alloc(inode_cachep, GFP_KERNEL);
    
    	if (!inode)
    		return NULL;
    
    	if (unlikely(inode_init_always(sb, inode))) {
    		if (inode->i_sb->s_op->destroy_inode)
    			inode->i_sb->s_op->destroy_inode(inode);
    		else
    			kmem_cache_free(inode_cachep, inode);
    		return NULL;
    	}
    
    	return inode;
    }
    

    这一下变得明白多了,inode最终是调用超级块中的s_op->alloc_inode来实现的,又涉及到了文件系统的内容,linux中,用一个超级块来代表一个文件系统,每个文件系统有创建磁盘文件、删除磁盘文件等方法,显然,socket也被当作了一个文件系统,所以这里调用的也是soket文件系统的创建节点函数,在文件系统的创建节点函数s_op->alloc_inode中,并非直接创建一个inode节点,而是创建了一个sock_alloc结构,这个结构里面有既有struct inode又有struct socket,最后,将这个socket初始化并返回,但这里还有一个细节,socket系统调用的返回值为一个套接字描述符(文件描述符),但这里并没有出现文件描述符,原因在这里__sys_socket函数的sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));,之前提到,socket被当作文件系统看待,也正是如此,在socket的系统调用中,创建了struct filestruct inodestruct socket,而这个函数正是将文件描述符与struct socketstruct file相绑定的函数,他使得文件描述也可以表示一个struct socket,实现如下:

    static int sock_map_fd(struct socket *sock, int flags)
    {
    	struct file *newfile;
    	int fd = get_unused_fd_flags(flags);
    	if (unlikely(fd < 0)) {
    		sock_release(sock);
    		return fd;
    	}
    
    	newfile = sock_alloc_file(sock, flags, NULL);
    	if (likely(!IS_ERR(newfile))) {
    		fd_install(fd, newfile);
    		return fd;
    	}
    
    	put_unused_fd(fd);
    	return PTR_ERR(newfile);
    }
    

    在这里面,通过sock_alloc_file(sock, flags, NULL);得到了要返回的文件描述符fd,并创建了一个struct file的对象,创建的过程如下:

    struct file *sock_alloc_file(struct socket *sock, int flags, const char *dname)
    {
    	struct file *file;
    	if (!dname)
    		dname = sock->sk ? sock->sk->sk_prot_creator->name : "";
    	file = alloc_file_pseudo(SOCK_INODE(sock), sock_mnt, dname,
    				O_RDWR | (flags & O_NONBLOCK),
    				&socket_file_ops);
    	if (IS_ERR(file)) {
    		sock_release(sock);
    		return file;
    	}
    	sock->file = file;
    	file->private_data = sock;
    	return file;
    }
    

    这里又调用了alloc_file_pseudo,注意,这里有一个关键的结构体就是socket_file_ops,他定义了一些socket基础的文件操作,所以这一步又将这些文件操作与文件绑定在一起了。定义如下:

    static const struct file_operations socket_file_ops = {
    	.owner =	THIS_MODULE,
    	.llseek =	no_llseek,
    	.read_iter =	sock_read_iter,
    	.write_iter =	sock_write_iter,
    	.poll =		sock_poll,
    	.unlocked_ioctl = sock_ioctl,
    #ifdef CONFIG_COMPAT
    	.compat_ioctl = compat_sock_ioctl,
    #endif
    	.mmap =		sock_mmap,
    	.release =	sock_close,
    	.fasync =	sock_fasync,
    	.sendpage =	sock_sendpage,
    	.splice_write = generic_splice_sendpage,
    	.splice_read =	sock_splice_read,
    };
    

    到此,socket的前两部我们走完了,由于关系错综复杂,我们捋一下调用关系:
    __ia32_compat_sys_socketcall->__sys_socket->sock_create->sock_alloc->alloc_file_pseudo->||sb->s_op->alloc_inode;
    通过这样一个流程,核心就是创建了结构体socket_alloc,因为这个结构里面既有socket又有inode,然后调用sock_map_fd()创建了struct file,并将struct file与·struct socket绑定,到此,三个结构体创建完成,它们都为应用程序的一个socket连接提供服务。
    32位socket调用就到这里结束了,那64位的呢?
    将制作文件系统的Makefile改一下,去掉-m32选项,使其编译为64位的程序,尝试跟踪在64位socket()下,会有何区别。第一个不同就是断电的设置,从前面的系统调用表可以看出,64位应用程序调用的中断服务接口为__x64_sys_socket,并且不同的socket类服务,都有自己的系统调用号。按照与之前相同的方法,开始调试

    (gdb) file vmlinux
    Reading symbols from vmlinux...done.
    warning: File "/home/netlab/netlab/linux-5.2.7/scripts/gdb/vmlinux-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
    To enable execution of this file add
    	add-auto-load-safe-path /home/netlab/netlab/linux-5.2.7/scripts/gdb/vmlinux-gdb.py
    (gdb) target remote: 1234
    Remote debugging using : 1234
    0x0000000000000000 in fixed_percpu_data ()
    (gdb) b __x64_sys_socket
    Breakpoint 1 at 0xffffffff817eeb10: file net/socket.c, line 1526.
    (gdb) c
    Continuing.
    Breakpoint 1, __x64_sys_socket (regs=0xffffc90000047f58) at net/socket.c:1524
    1524	SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    (gdb) l
    1519			return retval;
    1520	
    1521		return sock_map_fd(sock, flags & (O_CLOEXEC | O_NONBLOCK));
    1522	}
    1523	
    1524	SYSCALL_DEFINE3(socket, int, family, int, type, int, protocol)
    1525	{
    1526		return __sys_socket(family, type, protocol);
    1527	}
    1528	
    (gdb) 
    

    可以看到,__x64_sys_socket就是SYSCALL_DEFINE3,他实际上就是做了一个转换,调用了我们之前分析的__sys_socket,后面的执行步骤也就一样了,与程序的位数无关。
    那bind函数呢?我们也可以分析一下,重启gdb和menuos,将断点打在_x64_sys_bind,然后执行client命令

    (gdb) file vmlinux
    Reading symbols from vmlinux...done.
    warning: File "/home/netlab/netlab/linux-5.2.7/scripts/gdb/vmlinux-gdb.py" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load".
    To enable execution of this file add
    	add-auto-load-safe-path /home/netlab/netlab/linux-5.2.7/scripts/gdb/vmlinux-gdb.py
    line to your configuration file "/home/netlab/.gdbinit".
    To completely disable this security protection add
    	set auto-load safe-path /
    line to your configuration file "/home/netlab/.gdbinit".
    For more information about this security protection see the
    "Auto-loading safe path" section in the GDB manual.  E.g., run from the shell:
    	info "(gdb)Auto-loading safe path"
    (gdb) b __x64_sys_bind
    Breakpoint 1 at 0xffffffff817eeee0: file net/socket.c, line 1664.
    (gdb) c
    The program is not being run.
    (gdb) target remote: 1234
    Remote debugging using : 1234
    0x0000000000000000 in fixed_percpu_data ()
    (gdb) c
    Continuing.
    Breakpoint 1, __x64_sys_bind (regs=0xffffc90000047f58) at net/socket.c:1662
    1662	SYSCALL_DEFINE3(bind, int, fd, struct sockaddr __user *, umyaddr, int, addrlen)
    1663	{
    1664		return __sys_bind(fd, umyaddr, addrlen);
    1665	}
    1666	
    (gdb) 
    

    可以发现,32位与64位的程序除了入口不一样,其他的执行过程没什么区别,继续观察他的执行,也会发现,最终调用的是sock->ops->bind(sock,(struct sockaddr *)&address, addrlen);,sock的类型是struct socket,这与我们分析的socket()内容也是一致的。

    (gdb) b __sys_bind
    Breakpoint 2 at 0xffffffff817eee00: file net/socket.c, line 1640.
    (gdb) c
    Continuing.
    Breakpoint 2, __sys_bind (fd=4, umyaddr=0x7fffe4a9b1b0, addrlen=16)
        at net/socket.c:1640
    1639	int __sys_bind(int fd, struct sockaddr __user *umyaddr, int addrlen)
    1640	{
    		......
    1652				if (!err)
    1653					err = sock->ops->bind(sock,
    1654							      (struct sockaddr *)
    (gdb) 
    1655							      &address, addrlen);
    		......
    1659		return err;
    1660	}
    

    还有最后一个问题:socket初始化,在前面socket struct的介绍中,proto_ops域有不同的服务函数指针,但这些指针在什么时候赋值,如何赋值的我们还未分析,这一步,我们主要分析这个问题。

    socket的初始化

    同样,通过gdb来观察linux内核的启动过程,观察socket以何种顺序,何种方式被初始化:
    重新打开qemu,加载menuos,用gdb调试内核的启动:
    首先将断点打在start_kernel,并观察有无初始化网络的代码:

    (gdb) target remote: 1234
    Remote debugging using : 1234
    0x0000000000000000 in fixed_percpu_data ()
    (gdb) b start_kernel 
    Breakpoint 1 at 0xffffffff82997b05: file init/main.c, line 552.
    (gdb) c
    Continuing.
    
    Breakpoint 1, start_kernel () at init/main.c:552
    warning: Source file is more recent than executable.
    552	asmlinkage __visible void __init start_kernel(void)
    553	{
    554		char *command_line;
    555		char *after_dashes;
    556	
    
    557		set_task_stack_end_magic(&init_task);
    558		smp_setup_processor_id();
    559		debug_objects_early_init();
    560	
    561		cgroup_init_early();
    562	
    563		local_irq_disable();
    564		early_boot_irqs_disabled = true;
    
    570		boot_cpu_init();
    571		page_address_init();
    572		pr_notice("%s", linux_banner);
    573		setup_arch(&command_line);
    574		mm_init_cpumask(&init_mm);
    575		setup_command_line(command_line);
    576		setup_nr_cpu_ids();
     
    

    并未看到网络初始化相关代码,arch_call_rest_init();注意到这个函数执行的应该是除了这里列出来的其他部分的初始化,将断点设在arch_call_rest_init();

    Breakpoint 2, arch_call_rest_init () at init/main.c:548
    546	
    547	void __init __weak arch_call_rest_init(void)
    548	{
    549		rest_init();
    550	}
    551	
    552	asmlinkage __visible void __init start_kernel(void)
    (gdb) b rest_init
    Breakpoint 4, rest_init () at init/main.c:411
    411	
    (gdb) l
    406	
    407	noinline void __ref rest_init(void)
    408	{
    409		struct task_struct *tsk;
    410		int pid;
    411	
    412		rcu_scheduler_starting();
    413		/*
    414		 * We need to spawn init first so that it obtains pid 1, however
    415		 * the init task will end up wanting to create kthreads, which, if
    (gdb) 
    
    

    arch_rest_init()只有一行,那就是调用rest_init(),继续追踪,得到rest_init的完整代码:

    	noinline void __ref rest_init(void)
    408	{
    409		struct task_struct *tsk;
    410		int pid;
    411	
    412		rcu_scheduler_starting();
    		......
    418		pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    		......
    424		rcu_read_lock();
    425		tsk = find_task_by_pid_ns(pid, &init_pid_ns);
    426		set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
    427		rcu_read_unlock();
    428	
    429		numa_default_policy();
    430		pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    431		rcu_read_lock();
    432		kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    433		rcu_read_unlock();
    		...
    442		system_state = SYSTEM_SCHEDULING;
    443	
    444		complete(&kthreadd_done);
    450		schedule_preempt_disabled();
    451		/* Call into cpu_idle with preempt disabled */
    452		cpu_startup_entry(CPUHP_ONLINE);
    453	}
    

    这里创建了两个线程kernel_init和kthread,实际的初始化是由它们完成的,那我们将断点分别设在这两个函数:

    1086	static int __ref kernel_init(void *unused)
    1087	{
    1088		int ret;
    1089	
    1090		kernel_init_freeable();
    1091		/* need to finish all async __init code before freeing the memory */
    (gdb) 
    1092		async_synchronize_full();
    1093		ftrace_free_init_mem();
    1094		free_initmem();
    1095		mark_readonly();
    			......
    1101		pti_finalize();
    (gdb) 
    1102	
    1103		system_state = SYSTEM_RUNNING;
    1104		numa_default_policy();
    1105	
    1106		rcu_end_inkernel_boot();
    1107	
    1108		if (ramdisk_execute_command) {
    1109			ret = run_init_process(ramdisk_execute_command);
    1110			if (!ret)
    1111				return 0;
    (gdb) 
    1112			pr_err("Failed to execute %s (error %d)
    ",
    1113			       ramdisk_execute_command, ret);
    1114		}
    (gdb) 
    1122		if (execute_command) {
    1123			ret = run_init_process(execute_command);
    1124			if (!ret)
    1125				return 0;
    1126			panic("Requested init %s failed (error %d).",
    1127			      execute_command, ret);
    1128		}
    1129		if (!try_to_run_init_process("/sbin/init") ||
    1130		    !try_to_run_init_process("/etc/init") ||
    1131		    !try_to_run_init_process("/bin/init") ||
    (gdb) 
    1132		    !try_to_run_init_process("/bin/sh"))
    1133			return 0;
    1134	
    1135		panic("No working init found.  Try passing init= option to kernel. "
    1136		      "See Linux Documentation/admin-guide/init.rst for guidance.");
    1137	}
    

    首先执行的是kernel_init,函数内部负责判断应该执行哪个位置的init文件,并最终跳转执行,但是在加载init用户程序前通过kernel_init_freeable函数进一步做了一些初始化的工作,所以跳转到kernel_init_freeable()。

    static noinline void __init kernel_init_freeable(void)
    {
    	/*
    	 * Wait until kthreadd is all set-up.
    	 */
    	wait_for_completion(&kthreadd_done);
    	smp_prepare_cpus(setup_max_cpus);
    
    	workqueue_init();
    
    	init_mm_internals();
    
    	do_pre_smp_initcalls();
    	lockup_detector_init();
            ......
    	smp_init();
    	sched_init_smp();
    
    	page_alloc_init_late();
    	page_ext_init();
    
    	do_basic_setup();
            ......
    }
    

    函数内部除了do_basic_setup外,并未执行与网络相关的初始化。所以我们在do_basic_setup打上断点,但是,程序首先来到了kthreadd

    568	int kthreadd(void *unused)
    569	{
    570		struct task_struct *tsk = current;
    571	
    572		/* Setup a clean context for our children to inherit. */
    573		set_task_comm(tsk, "kthreadd");
    (gdb) l
    574		ignore_signals(tsk);
    575		set_cpus_allowed_ptr(tsk, cpu_all_mask);
    576		set_mems_allowed(node_states[N_MEMORY]);
    577	
    578		current->flags |= PF_NOFREEZE;
    579		cgroup_init_kthreadd();
    580	
    581		for (;;) {
    582			set_current_state(TASK_INTERRUPTIBLE);
    583			if (list_empty(&kthread_create_list))
    (gdb) 
    584				schedule();
    585			__set_current_state(TASK_RUNNING);
    586	
    587			spin_lock(&kthread_create_lock);
    588			while (!list_empty(&kthread_create_list)) {
    589				struct kthread_create_info *create;
    590	
    591				create = list_entry(kthread_create_list.next,
    592						    struct kthread_create_info, list);
    593				list_del_init(&create->list);
    (gdb) 
    594				spin_unlock(&kthread_create_lock);
    595	
    596				create_kthread(create);
    597	
    598				spin_lock(&kthread_create_lock);
    599			}
    600			spin_unlock(&kthread_create_lock);
    601		}
    602	
    603		return 0;
    (gdb) 
    604	}
    

    kthreadd内部负责根据kthread_create_list创建一系列的线程,这显然与我们要的网络初始化无关,继续观察do_basic_setup;

    static void __init do_basic_setup(void)
    {
    	cpuset_init_smp();
    	shmem_init();
    	driver_init();
    	init_irq_proc();
    	do_ctors();
    	usermodehelper_enable();
    	do_initcalls();
    }
    
    859static void __init do_initcalls(void)
    860{
    861	int level;
    862
    863	for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
    864		do_initcall_level(level);
    865}
    

    do_initcalls会根据init_levels不断执行do_initcall_level(level),那首先我们需要看看do_initcall_level是什么

    static void __init do_initcall_level(int level)
    {
    	initcall_entry_t *fn;
    
    	strcpy(initcall_command_line, saved_command_line);
    	parse_args(initcall_level_names[level],
    		   initcall_command_line, __start___param,
    		   __stop___param - __start___param,
    		   level, level,
    		   NULL, &repair_env_string);
    
    	trace_initcall_level(initcall_level_names[level]);
    	for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
    		do_one_initcall(initcall_from_entry(fn));
    }
    

    initcall_levels为一个表,从而可以对每一个注册进来的初始化项目进行初始化,initcall_from_entry返回的就是fn的地址,然后根据这个地址,执行do_one_initcall,而至于这个表是如何来的,可以从网络初始化程序inet_init得到解答

    略去了很多无关代码
    static int __init inet_init(void)
    {
            ......
    	rc = proto_register(&tcp_prot, 1);
    	if (rc)
    		goto out;
    
    	rc = proto_register(&udp_prot, 1);
    	if (rc)
    		goto out_unregister_tcp_proto;
    
    	rc = proto_register(&raw_prot, 1);
    	if (rc)
    		goto out_unregister_udp_proto;
    
    	rc = proto_register(&ping_prot, 1);
    	if (rc)
    		goto out_unregister_raw_proto;
    
    	(void)sock_register(&inet_family_ops);
    	ip_static_sysctl_init();
    	if (inet_add_protocol(&icmp_protocol, IPPROTO_ICMP) < 0)
    		pr_crit("%s: Cannot add ICMP protocol
    ", __func__);
    	if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
    		pr_crit("%s: Cannot add UDP protocol
    ", __func__);
    	if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
    		pr_crit("%s: Cannot add TCP protocol
    ", __func__);
    	/* Register the socket-side information for inet_create. */
    	for (r = &inetsw[0]; r < &inetsw[SOCK_MAX]; ++r)
    		INIT_LIST_HEAD(r);
    
    	for (q = inetsw_array; q < &inetsw_array[INETSW_ARRAY_LEN]; ++q)
    		inet_register_protosw(q);
    
    	arp_init();
    	ip_init();
    	tcp_init();
    	udp_init();
    	udplite4_register();
    	raw_init();
    	ping_init();
    	ipv4_proc_init();
    	ipfrag_init();
    
    	dev_add_pack(&ip_packet_type);
    	ip_tunnel_core_init();
    	rc = 0;
    }
    fs_initcall(inet_init);
    

    所以通过fs_initcall(inet_init)将inet_init函数注册进initcalls的initcall_levels,最终得到初始化,为了验证,最好的办法就是重新启动,将断点打在inet_init,观察这个函数是否会调用即可。

    (gdb) b inet_init
    Breakpoint 1 at 0xffffffff829f49fe: file net/ipv4/af_inet.c, line 1906.
    (gdb) c
    Continuing.
    
    Breakpoint 1, inet_init () at net/ipv4/af_inet.c:1906
    1906	{
    
    

    接下来仔细看看inet_init的代码:这里面包括了几乎所有的网络协议——TCP、UDP、ICMP等,流程是先注册端口号,然后添加对应的协议,最后是初始化,追踪到这里也就告一段落了,但是我们并没有看到socket系统的基础操作如alloc_inode是如何初始化的,这是由在定义socket超级块的时候就直接定义了,并未在初始化的流程中。

    static const struct super_operations sockfs_ops = {
    	.alloc_inode	= sock_alloc_inode,
    	.destroy_inode	= sock_destroy_inode,
    	.statfs		= simple_statfs,
    };
    

    显然,位于struct socket结构体中的proto_ops域,也就是特定协议的函数处理指针就是在这里初始化的,对于不同的协议,初始化了不同的proto_ops。

  • 相关阅读:
    golang 数据结构 优先队列(堆)
    leetcode刷题笔记5210题 球会落何处
    leetcode刷题笔记5638题 吃苹果的最大数目
    leetcode刷题笔记5637题 判断字符串的两半是否相似
    剑指 Offer 28. 对称的二叉树
    剑指 Offer 27. 二叉树的镜像
    剑指 Offer 26. 树的子结构
    剑指 Offer 25. 合并两个排序的链表
    剑指 Offer 24. 反转链表
    剑指 Offer 22. 链表中倒数第k个节点
  • 原文地址:https://www.cnblogs.com/myguaiguai/p/12041354.html
Copyright © 2011-2022 走看看