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通信流程完全相同,至此,整个追踪过程结束。

posted @ 2019-12-19 16:14  温冷  阅读(313)  评论(0编辑  收藏  举报