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\n",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\n\t"							\
    : "=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)\n",
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\n");
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)\n",
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\n", __func__);
	if (inet_add_protocol(&udp_protocol, IPPROTO_UDP) < 0)
		pr_crit("%s: Cannot add UDP protocol\n", __func__);
	if (inet_add_protocol(&tcp_protocol, IPPROTO_TCP) < 0)
		pr_crit("%s: Cannot add TCP protocol\n", __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。

posted @ 2019-12-15 11:26  zhqian  阅读(1137)  评论(0编辑  收藏  举报