Socket与系统调用深度分析
系统调用
在一开始,应用程序是可以直接控制硬件的,这就需要程序员有很高的编程能力,否则一旦程序出了问题,会将整个系统Crash。
在现在的操作系统中,用户程序运行在用户态,而要进行诸如Socket
、磁盘I/O
这样的一些操作,这需要切换到内核态,再进行进行相应的操作,而这一过程则是系统调用system call
。有了操作系统分离了内核和用户态,应用程序就无法直接进行硬件资源的访问,需要经过系统调用来进行。
每次的系统调用,都会从用户态转换到内核态,运行完任务后,回到用户态。这中间的过程需要上下文切换(保存寄存器信息),也就是切换状态是需要消耗资源和时间的。
Socket
Socket是程序实现端到端通信的地址,互联网中每台设备有自己的IP地址,但每台设备上运行着许多程序。同时不同的程序都可能有通信的需求,这就需要一个套接字(Socket)来区分不同的程序。
一个套接字由IP地址和端口号组成。
IP address: port
实验
开启上次实验编译好的MenuOS系统
上次实验编译了一个带调试功能,且带有TCP服务器和客户端的MenuOS系统
进入LinuxKernel目录,启动虚拟机。
jett@ubuntu:~$ cd LinuxKernel
jett@ubuntu:~/LinuxKernel$ qemu-system-i386 -kernel linux-5.4.2/arch/x86/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S
进入调试
这时候虚拟机进入停止在一个黑屏界面,等待gdb的接入和下一步指令。
新开一个终端窗口,进入gdb调试。
接着分别
- 导入符号表
- 连接调试服务器
- 设置断点
jett@ubuntu:~/LinuxKernel$ gdb
(gdb) file ~/LinuxKernel/linux-5.4.2/vmlinux
Reading symbols from ~/LinuxKernel/linux-5.4.2/vmlinux...done.
(gdb) target remote:1234
Remote debugging using :1234
0x0000fff0 in ?? ()
(gdb) break start_kernel
Breakpoint 1 at 0xc1db5885: file init/main.c, line 576.
然后输入c
让系统继续执行,执行到断点start_kernel ()
则说明成功。
(gdb) c
Continuing.
Breakpoint 1, start_kernel () at init/main.c:576
576 {
添加新断点sys_bind
, sys_listen
, sys_socketcall
(gdb) break sys_bind
Breakpoint 2 at 0xc179beb0: file net/socket.c, line 1656.
(gdb) break sys_listen
Breakpoint 3 at 0xc179bf60: file net/socket.c, line 1688.
(gdb) break sys_socketcall
Breakpoint 4 at 0xc179ce00: file net/socket.c, line 2818.
c
让系统继续执行
(gdb) c
Continuing.
Breakpoint 4, __se_sys_socketcall (call=1, args=-1075909056) at net/socket.c:2818
2818 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
系统进入sys_socketcall
断点,查看虚拟机窗口,可以看到虚拟机正在启动本地换回接口
再次继续执行,可以看到总共进了三次sys_socketcall
,另外两次分别是启动以太网口和建立连接,这个在上周就已经发现了。
接着成功进入系统,可以查看之前编译进系统的TCP服务端和客户端
在MenuOS中输入replyhi
启动TCP服务端
sys_socketcall
可以在gdb中发现再次进入sys_socketcall
断点
Breakpoint 4, __se_sys_socketcall (call=1, args=-1075910240) at net/socket.c:2818
2818 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
可以看到,断点停在__se_sys_socketcall
中,位置是net/socket.c
的2818行,找到函数如下:
2818 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
2819 {
2820 unsigned long a[AUDITSC_ARGS];
2821 unsigned long a0, a1;
2822 int err;
2823 unsigned int len;
2824
2825 if (call < 1 || call > SYS_SENDMMSG)
2826 return -EINVAL;
2827 call = array_index_nospec(call, SYS_SENDMMSG + 1);
2828
2829 len = nargs[call];
2830 if (len > sizeof(a))
2831 return -EINVAL;
2832
2833 /* copy_from_user should be SMP safe. */
2834 if (copy_from_user(a, args, len))
2835 return -EFAULT;
2836
2837 err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
2838 if (err)
2839 return err;
2840
2841 a0 = a[0];
2842 a1 = a[1];
2843
2844 switch (call) {
2845 case SYS_SOCKET:
2846 err = __sys_socket(a0, a1, a[2]);
2847 break;
2848 case SYS_BIND:
2849 err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
2850 break;
2851 case SYS_CONNECT:
2852 err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
2853 break;
2854 case SYS_LISTEN:
2855 err = __sys_listen(a0, a1);
2856 break;
2857 case SYS_ACCEPT:
2858 err = __sys_accept4(a0, (struct sockaddr __user *)a1,
2859 (int __user *)a[2], 0);
2860 break;
2861 ...
2930 default:
2931 err = -EINVAL;
2932 break;
2933 }
2934 return err;
2935 }
函数内部主体是一个switch
语句,根据我们的call
参数来进行选择,通过gdb我们可以看到,这时(call=1, args=-1075910240)
,具体要根据1是哪个case
来追踪调用。
可以在gdb
中输入n
(next)来继续往下执行,跟踪这次调用进入到哪个case
,也可以去找case
的定义。
而这些case
的定义并不在socket.c
中。我们可以在/LinuxKernel/linux-5.4.2/include/linux
下找到socket.h
文件。
#define SYS_SOCKET 1 /* sys_socket(2) */
#define SYS_BIND 2 /* sys_bind(2) */
#define SYS_CONNECT 3 /* sys_connect(2) */
#define SYS_LISTEN 4 /* sys_listen(2) */
#define SYS_ACCEPT 5 /* sys_accept(2) */
#define SYS_GETSOCKNAME 6 /* sys_getsockname(2) */
#define SYS_GETPEERNAME 7 /* sys_getpeername(2) */
#define SYS_SOCKETPAIR 8 /* sys_socketpair(2) */
#define SYS_SEND 9 /* sys_send(2) */
#define SYS_RECV 10 /* sys_recv(2) */
#define SYS_SENDTO 11 /* sys_sendto(2) */
#define SYS_RECVFROM 12 /* sys_recvfrom(2) */
#define SYS_SHUTDOWN 13 /* sys_shutdown(2) */
#define SYS_SETSOCKOPT 14 /* sys_setsockopt(2) */
#define SYS_GETSOCKOPT 15 /* sys_getsockopt(2) */
#define SYS_SENDMSG 16 /* sys_sendmsg(2) */
#define SYS_RECVMSG 17 /* sys_recvmsg(2) */
所以实质则是进入了__sys_socket(a0, a1, a[2]);
函数内。
__sys_socket
Continuing
Breakpoint 5, __sys_socket (family=2, type=1, protocol=0) at net/socket.c:1492
1492 {
输入s
(step in)进入函数调用,或者直接查看net/socket.c
的1492行。
int __sys_socket(int family, int type, int protocol)
{
int retval;
struct socket *sock;
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));
}
函数sock_create
创建了一个对应类型的套接字,sock_map_fd
将套接字连接到一个文件描述符中返回。
socket_create
继续进入socket_create
函数:
(gdb) step
sock_create (res=<optimized out>, protocol=<optimized out>, type=<optimized out>,
family=<optimized out>) at net/socket.c:1511
1511 retval = sock_create(family, type, protocol, &sock);
函数调用了__sock_create
函数
int sock_create(int family, int type, int protocol, struct socket **res)
{
return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);
}
__sock_create
设置断点,进入函数查看
Breakpoint 7, __sock_create (net=0xc1d60dc0 <init_net>, family=2, type=1, protocol=0,
res=0xc71e7f40, kern=0) at net/socket.c:1349
1349 {
__sock_create
函数定义如下:
int __sock_create(struct net *net, int family, int type, int protocol,
struct socket **res, int kern)
{
int err;
struct socket *sock;
const struct net_proto_family *pf;
// 检查协议地址和socket类型
if (family < 0 || family >= NPROTO)
return -EAFNOSUPPORT;
if (type < 0 || type >= SOCK_MAX)
return -EINVAL;
if (family == PF_INET && type == SOCK_PACKET) {
pr_info_once("%s uses obsolete (PF_INET,SOCK_PACKET)
",
current->comm);
family = PF_PACKET;
}
err = security_socket_create(family, type, protocol, kern);
if (err)
return err;
// 分配socket对象
sock = sock_alloc();
if (!sock) {
net_warn_ratelimited("socket: no more sockets
");
return -ENFILE;
}
sock->type = type;
#ifdef CONFIG_MODULES
if (rcu_access_pointer(net_families[family]) == NULL)
request_module("net-pf-%d", family);
#endif
rcu_read_lock();
// 得到相应的协议族的操作
pf = rcu_dereference(net_families[family]);
err = -EAFNOSUPPORT;
if (!pf)
goto out_release;
if (!try_module_get(pf->owner))
goto out_release;
rcu_read_unlock();
// 使用指针函数创建套接字
err = pf->create(net, sock, protocol, kern);
if (err < 0)
goto out_module_put;
if (!try_module_get(sock->ops->owner))
goto out_module_busy;
module_put(pf->owner);
err = security_socket_post_create(sock, family, type, protocol, kern);
if (err)
goto out_sock_release;
*res = sock;
return 0;
out_module_busy:
err = -EAFNOSUPPORT;
out_module_put:
sock->ops = NULL;
module_put(pf->owner);
out_sock_release:
sock_release(sock);
return err;
out_release:
rcu_read_unlock();
goto out_sock_release;
}
其中sock_alloc
函数:该函数分配一个新的inode与其绑定的套接字对象
struct socket *sock_alloc(void)
{
struct inode *inode;
struct socket *sock;
inode = new_inode_pseudo(sock_mnt->mnt_sb);
if (!inode)
return NULL;
sock = SOCKET_I(inode);
inode->i_ino = get_next_ino();
inode->i_mode = S_IFSOCK | S_IRWXUGO;
inode->i_uid = current_fsuid();
inode->i_gid = current_fsgid();
inode->i_op = &sockfs_inode_ops;
return sock;
}
接着pf = rcu_dereference(net_families[family]);
得到相应的协议族操作net_family_ops
。
这里的指针函数指向inet_family_ops
,相当于调用inet_family_ops.create
所以综上__sock_create
函数进行了
- 协议类型的检查
- 创建套接字对象
- 根据不同的协议族类型,使用函数指针初始化对应的套接字
- 做了各种错误处理
- 返回套接字
整个sys_socketcall
调用的主要过程
sys_socketcall
|
switch:
case SYS_SOCKET:
__sys_socket
|
+---sock_create
| |
| +---__sock_create
| |
| +---sock_alloc()
| +---rcu_dereference(net_families[family])
| +---pf->create(net, sock, protocol, kern)
+---sock_map_fd
case SYS_BIND:
...
继续执行
对下列case
中的函数调用打上断点,这里还可以发现,系统调用中的send
和sendto
都是通过__sys_sendto
系统调用来实现的,recv
同样。
(gdb) b sys_socketcall
Breakpoint 10 at 0xc179ce00: file net/socket.c, line 2818.
(gdb) b __sys_socket
Breakpoint 11 at 0xc179ba70: file net/socket.c, line 1492.
(gdb) b __sys_bind
Breakpoint 12 at 0xc179bde0: file net/socket.c, line 1634.
(gdb) b __sys_connect
Breakpoint 13 at 0xc179c1c0: file net/socket.c, line 1811.
(gdb) b __sys_listen
Breakpoint 14 at 0xc179bed0: file net/socket.c, line 1668.
(gdb) b __sys_accept4
Breakpoint 15 at 0xc179bf70: file net/socket.c, line 1707.
(gdb) b __sys_sendto
Breakpoint 16 at 0xc179c480: file net/socket.c, line 1923.
(gdb) b __sys_recvfrom
Breakpoint 17 at 0xc179c5e0: file net/socket.c, line 1984.
gdb捕获了4次sys_socketcall
,分别是
- SYS_SOCKET
- SYS_BIND
- SYS_LISTEN
- SYS_ACCEPT
正好对应TCP服务端中应用程序的4个过程,然后阻塞等待客户端请求
在MenuOS中,启动客户端hello
gdb捕获了6次sys_socketcall
,分别是
- SYS_SOCKET(客户端)
- SYS_CONNECT(客户端)
- SYS_SEND(客户端)
- SYS_RECV(服务端)
- SYS_SEND(服务端)
- SYS_RECV(客户端)
作者:SA19225176,万有引力丶
参考资料来源:USTC Socket与系统调用深度分析