Socket与系统调用深度分析
Socket与系统调用关系
- Socket API编程接口之上可以编写基于不同网络协议的应用程序;
- Socket接口在用户态通过系统调用机制进入内核;
- 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
- socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;
本博文将从系统调用机制、Socket API编程接口及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核下进行跟踪验证。
系统调用
(1)系统调用就是为了让应用程序可以访问系统资源,每个操作系统都会提供一套接口供应用程序使用。这些接口通常通过中断来实现,例如在windows中是0x2E号中断作为系统调用入口,linux使用0x80号中断作为系统的调用的入口。
(2)系统调用的弊端:各个操作系统的系统调用不兼容,使用不方便,系统调用比较原始。运行库解决这两个问题,它的使用统一不会随着操作系统或编译器的变化而变化。
(3)系统调用的原理
系统调用是运行在内核态的,而用户程序一般是运行在用户态的,操作系统一般通过中断从用户态切换到内核态。中断具有两个属性一个是中断号,一个是中断向量表,是一个数组,包含中断处理程序。一个中断号对应一个中断处理程序。中断分为硬件中断和软件中断,软件中断通常是一条指令,带有一个参数代表中断号。在linux中使用0x80来触发所有的系统调用。和中断一样系统调用都有一个系统调用号,系统调用号代表在系统调用表中的位置。
系统调用过程
(1)以sys开始的函数(如下)
/*
无参的系统调用
type:系统调用的返回值类型
name:系统调用的名称
*/
_syscall0(type,name);
/*
带一个参数的系统调用
type1:参数的类型
arg1:参数的值
*/
_syscall2(type,name,type1,arg1);
(2)参数传递:系统调用用寄存器来传递参数,eax寄存器用于表示系统的调用的接口号,在系统调用时又做为返回值。
系统调用的参数通过寄存器来传递(EBX,ECX,EDX,ESI,EDI,EBP),在进入系统调用函数时,会有SAVEALL函数保存这些所有的寄存器的值。
(3)堆栈切换:从用户栈到内核栈
找到当前进程的内核栈 --》保存当前的ESP和SS(将用户态的寄存器SS,ESP,EFLAGSS,CS压入内核栈中)--》设置ESP和SS为内核栈的值。
(4)调用中断处理程序:找到0x80对应的中断处理程序system_call并执行
(5)系统调用返回iret (内核态到用户态)
当内核从系统调用返回时,调用iret指令,则会从内核栈里弹出寄存器SS,ESP,EFLAGS,CS,EIP的值,返回值用EAX返回,恢复到用户态的状态
(6)返回值的处理
函数返回:_syscall_return (type,res)
用于检查系统调用的返回值,并把它相应的转换为c语言的errno错误返回,系统调用返回值传递错误码,而c语言里大多数都以返回-1表示调用失败,而将错误信息储存在errno的局部变量中。
流程如下图所示:
系统调用与一般函数调用的区别
一般的函数调用均是在用户态进行的,函数调用是通过栈来传递参数。而系统调用是通过寄存器来传递参数。
函数返回:一般函数调用的返回值如果是小字节返回则用eax带出,如果是8~15字节则用edx和eax带出。对于大字节,他会在调用方的栈帧中开辟一个空间,作为返回的临时对象,在调用函数时会将这块地址隐式的传入,调用函数的返回值拷贝到临时对象中,并将临时对象的地址用eax带出。
跟踪socket相关系统调用内核处理函数
我们可以先查看内核对于socket相关的系统调用接口,以及对应的系统调用号和内核的socket接口。
现在进行内核跟踪,我们首先在 sys_socketcall 处建立断点
捕获到sys_socketcall,对应的内核处理函数为SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
在net/socket.c中查看SYSCALL_DEFINE2相关的源代码如下:
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;
}
SYSCALL_DEFINE2根据不同的call来进入不同的分支,从而调用不同的内核处理函数,如__sys_bind, __sys_listen等等内核处理函数。
在SYSCALL_DEFINE2调用的与socket相关的函数们都打上断点,再进行调试
在Qemu中输入replyhi,发现gdb捕获到以上断点,一直到sys_accept4函数停止。说明此时服务器处于阻塞状态,一直在等待客户端连接,在Qemu中输入hello,同样捕获断点,可以看到客户端发起连接,发送接收数据,整个过程与TCP socket通信流程完全相同,至此,整个追踪过程结束。