Socket与系统调用深度分析
学习一下对Socket与系统调用的分析分析
一、介绍
我们都知道高级语言的网络编程最终的实现都是调用了系统的Socket API编程接口,在操作系统提供的socket系统接口之上可以建立不同端口之间的网络连接,从而使我们可以编写各基于不同网络协议的应用程序。而用户程序一般都是运行在用户态,依靠的Socket接口也是在在用户态,我们都知道socket接口是通过系统调用机制进入内核,从而从内核的层面提高服务。本次实验主要需要分析出socketAPI函数是如何进行系统调用的。
首先我们再明确一下系统调用和socketAPI之间的关系:也就是说系统调用是使得API能获得内核支持的途径。
二、实验步骤
首先从参考了大佬的博客之后获悉,在linux系统中,与socket API有关的系统调用包括所有的与socketapi都使用的sys_socketcall系统调用和每一个socketapi都对应特定的系统调用,因此我主要通过这两类来进行分析。
1、首先使用gdb连接menu系统,这个系统是上次实验制作的简易系统
注意:这里需要先用下面的命令启动tcp server,否则会显示连接超时
执行 qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S
然后新开一个终端,打开gdb,并且使用target连接上menu系统
复制代码
gdb
file ~/net/linux-5.0.1/vmlinux
target remote:1234
复制代码
连接以后就可以打断点进行调试了。
2、对特殊的系统调用进行测试:给sys_bind和sys_listen分析
接着按c即继续运行程序,此时menu系统继续启动。
根据提示,输入replyhi以后,发现gdb的终端如下所示:
说明并没有停在断点,也就是使用replyhi的时候并没有调用sys_listen和sys_bind这两个内核函数。
3、接下来对所有socketapi都使用的sys_socketcall系统调用进行测试,使用 b sys_socketcall打断点,接着按c继续执行
然后在mune系统进行测试,输入replyhi以后,发现gdb终端有反馈了,如下所示:
说明停在了SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args),也就是此时使用了这个内核函数
查看源码如下:
下面我们简单分析一下这个函数:发现这段c语言很大一部分都是switch语句,说明这个函数的主要作用是根据不同的输入来选择不同的操作,调用不同的内核处理函数,传入是SYS_BIND则调用__sys_bind,传入SYS_LISTEN则调用 __sys_listen等内核函数进行相应的处理。
复制代码
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) {
case 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;
}
复制代码
此时我们查看一下replyhi的源码,看看究竟是如何调用的。
从下面的程序可以看到,replyhi调用其他函数的顺序InitializeService,ServiceStart,RecvMsg,SendMsg、ServiceStop。
接下来我们需要做的是找出InitializeService,ServiceStart,RecvMsg,SendMsg和ServiceStop,看看这些函数是如何使用内核处理函数的。
(1)首先是InitializeService,可以看到这个函数的主要就是调用了 PrepareSocket(IP_ADDR,PORT)和 InitServer()
所以只能继续追踪,首先看 PrepareSocket(IP_ADDR,PORT),可以看到这个函数的主要作用是分配内存空间,并调用socket函数,创建套接字
接着是InitServer()函数,从程序中看到这个函数的主要作用调用bind函数,服务器使用bind来指明熟知的端口号,然后等待连接
并进行监听
所以InitializeService函数主要作用在于建立socket,绑定socket并进行端口的监听。
(2)ServiceStart函数,显然,这个函数的作用只是调用accept,获取传入连接请求。
(3) SendMsg,顾名思义,这个函数主要就是进行信息的发送,依靠的内核函数是send函数
(4)RecvMsg 这个函数主要就是进行信息的接受,依靠的内核函数是recv函数
(5)ServiceStop,这个函数很简单,就是调用close函数进行撤销套接字,结束当前的连接。
至此,我们终于分析完了replyhi这个函数是如何依靠socketAPI以及内核服务程序进行网络通信的。
回顾总结一下:
reply函数通过调用InitializeService,ServiceStart,RecvMsg,SendMsg、ServiceStop子函数,这些函数进行的系统调用分别是socket、bind、accept、recv、send、close。然后在内核的sockct函数中,通过传入的系统调用,使用switch语句调用不同的内核处理函数,完成网络通信。
三、总结
这次的实验确实挺难的,做了很久,也只是做出了点皮毛,但是通过这个实现,进一步了解了linux内核网络通信部分的机制,对写内核的人更加钦佩,只能说,大佬太强了!!!
实验过程参考同学博客:https://www.cnblogs.com/hhssqq9999/p/12048964.htmlhttps://www.cnblogs.com/iyuanyuan/p/12069677.html