Socket与系统调用深度分析

一、什么是系统调用

  linux内核中设置了一组用于实现系统功能的子程序,称为系统调用。系统调用和普通库函数调用非常相似,只是系统调用由操作系统核心提供,运行于核心态,而普通的函数调用由函数库或用户自己提供,运行于用户态。

  系统调用的意义如下:

  1.把用户从底层的硬件编程中解放出来
  2.极大的提高了系统的安全性
  3.使用户程序具有可移植性

  一般进程是不能访问内核的,而系统调用是用户态进入内核态的唯一入口

 

二、系统调用与API之间的关系

  1.API和系统调用的区别:

    API只是一个函数定义

    系统调用通过软中断向内核发出一个明确的请求

  2.Libc库定义的一些API引起了封装例程(wrapper routine,唯一的目的就是发布系统调用)

    一般每个系统调用对应一个封装例程

    库再用这些封装例程定义出给用户的API

  3.不是每个API都对应一个特定的系统调用

    首先,API可能直接提供用户态的服务(比如一些数学函数)
    其次,一个单独的API可能调用几个系统调用不同的
    API可能调用了同一个系统调用

  4.返回值

    大部分封装例程返回一个整数,其值得含义依赖于相应的系统调用

    -1在多数情况下表示内核不能满足进程的请求

    Libc中定义的errno变量包含特定的出错码

 

三、系统调用的过程

  用户空间的程序无法直接执行内核代码,它们不能直接调用内核空间中的函数,因为内核驻留在受保护的地址空间上。如果进程可以直接在内核的地址空间上读写的话,系统安全就会失去控制。所以,应用程序应该以某种方式通知系统,告诉内核自己需要执行一个系统调用,希望系统切换到内核态,这样内核就可以代表应用程序来执行该系统调用了。

  通知内核的机制是靠软件中断实现的。首先,用户程序为系统调用设置参数。其中一个参数是系统调用编号。参数设置完成后,程序执行“系统调用”指令。x86系统上的软中断由int产生。这个指令会导致一个异常:产生一个事件,这个事件会致使处理器切换到内核态并跳转到一个新的地址,并开始执行那里的异常处理程序。此时的异常处理程序实际上就是系统调用处理程序。它与硬件体系结构紧密相关。

  新地址的指令会保存程序的状态,计算出应该调用哪个系统调用,调用内核中实现那个系统调用的函数,恢复用户程序状态,然后将控制权返还给用户程序。系统调用是设备驱动程序中定义的函数最终被调用的一种方式。

  系统调用处理程序也其他异常处理程序的结构类似,执行下列操作
  1.在进程的内核态堆栈中保存大多数寄存器的内容(即保存恢复进程到用户态执行所需要的上下文)
  2.根据用户态传递的系统调用号,确定系统
  3.调用名为系统调用服务例程的相应的C函数来处理系统调用
  4.从系统调用返回

 

  以下图为例:

  

 

   用户态中的xyz()函数是API,当中封装了系统调用。系统调用中触发了int 0x80的中断,这个中断向量对应着systemcall这个内核代码的起点)。system中调用对应的系统调用服务程序sys_xyz(),进入服务程序。进行完毕后ret_from_sys_call返回。此时是一个进程调度的时机,如果没有发生进程调度则直接iret返回用户态。

 

四、系统调用号和传参

1、内核通过自己的系统调用分派表sys_call_table(可以理解为一个系统调用号,对应一个函数入口地址)找到这个具体的系统调用服务例程对应的函数入口地址,如上面sys_read,sys_write等。
2、传递的参数规则:
  在发起系统调用前,eax寄存器里面存储了系统调用号。如用户程序fork()函数,glibc 发出int 0x80或sysenter指令前,eax寄存器就会设置好内核的sys_fork函数对应的系统调用号,这是glibc里面的封装例程会自动设置好的,程序员无需关心。 有些系统调用可能调用很多参数(除了系统调用号之外),普通c函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的。因为系统调用是一种跨用户态和内核态的特殊函数,所以这两个栈都不能用。在发出系统调用之前,系统调用的参数写入了cpu的寄存器(如glibc去写好这些寄存器),然后发出系统调用之后,而在内核调用服务例程(如sys_fork()服务例程)之前,内核再把存放在cpu中的参数拷贝的内核态的堆栈中(因为sys_fork只是普通的c函数,前面说过普通c函数的参数传递是通过把参数值写入活动的程序栈(用户态栈或者内核态栈)实现的)。内核为什么不直接把用户态的栈拷贝到内核态的栈而要去通过寄存器来传呢?首先,同事操作两个栈是比较复杂的,其次,寄存器的使用使得系统调用处理程序的结构与其它异常处理程序的结构类似。
使用寄存器传递参数,必须满足两个条件:
        每个参数的长度不能超过寄存器的长度(比如寄存器长度32位,那参数长度就不能超过32位);
        参数的个数不能超过6个(除了eax中传递的系统调用号),因为80x86处理器的寄存器的数量是有限的。
        第一个条件总能成立,因为POSIX标准规定,如果寄存器里面装不下那个长度的参数,那么必须改用参数的地址来传递。
        第二个条件有的系统调用参数大于6个,这种情况下,必须用一个单独的寄存器执行进程地址空间的这些参数所在的一个内存区。
 

五、通过gdb跟踪reply的系统调用

1.先进入menuos

2.打开另一个终端,输入以下命令:

gdb
file ~/LinuxKernel/linux-5.0.1/vmlinux
target remote:1234
break sys_socketcall     //设置断点
c                                //查看断点信息

如下图所示:

 

3.在menuos中输入终端命令:

replyhi
hello

此时已经成功捕捉到了sys_socketcall,对应的内核处理函数为SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)

如下图所示:

4.根据这些系统调用返回的系统调用号,可以查看这些系统调用实现了哪些功能,我们找到socket.c文件,其源码如下:

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;
}

#此函数根据不同的call来进入不同的分支,从而调用不同的内核处理函数。
#在replyhi/hello的执行过程中,涉及到了socket的建立、recv、send等,这些不同的系统调用传给SYSCALL_DEFINE2()的参数call是不同的。

posted @ 2019-12-19 14:12  浩翔Zz  阅读(265)  评论(0编辑  收藏  举报