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 file
、struct inode
、struct socket
,而这个函数正是将文件描述符与struct socket
与struct 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。