Socket与系统调用深度分析

Posted wyy165

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Socket与系统调用深度分析相关的知识,希望对你有一定的参考价值。

实验要求


 

  • Socket API编程接口之上可以编写基于不同网络协议的应用程序;
  • Socket接口在用户态通过系统调用机制进入内核;
  • 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
  • socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;
  • 请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证;

系统调用概述


  计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源,为了更好的管理这些资源进程是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。也就是说操作系统是使用这些资源的唯一入口,而这个入口就是操作系统提供的系统调用(System Call)。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。

  一般情况下应用程序通过应用编程接口API,而不是直接通过系统调用来编程。在Unix世界,最流行的API是基于POSIX标准的。

  操作系统一般是通过中断从用户态切换到内核态。中断就是一个硬件或软件请求,要求CPU暂停当前的工作,去处理更重要的事情。比如,在x86机器上可以通过int指令进行软件中断,而在磁盘完成读写操作后会向CPU发起硬件中断。

  中断有两个重要的属性,中断号和中断处理程序。中断号用来标识不同的中断,不同的中断具有不同的中断处理程序。在操作系统内核中维护着一个中断向量表(Interrupt Vector Table),这个数组存储了所有中断处理程序的地址,而中断号就是相应中断在中断向量表中的偏移量。

 

执行态切换


应用程序 ( application program )与 库函数 ( libc )之间, 系统调用处理函数 ( system call handler )与 系统调用服务例程 ( system call service routine )之间, 均是普通函数调用,应该不难理解。 而 库函数 与 系统调用处理函数 之间,由于涉及用户态与内核态的切换,要复杂一些。

Linux 通过 软中断 实现从 用户态 到 内核态 的切换。 用户态 与 内核态 是独立的执行流,因此在切换时,需要准备 执行栈 并保存 寄存器 。

内核实现了很多不同的系统调用(提供不同功能),而 系统调用处理函数 只有一个。 因此,用户进程必须传递一个参数用于区分,这便是 系统调用号 ( system call number )。 在 Linux 中, 系统调用号 一般通过 eax 寄存器 来传递。

总结起来, 执行态切换 过程如下:

  1. 应用程序 在 用户态 准备好调用参数,执行 int 指令触发 软中断 ,中断号为 0x80 ;
  2. CPU 被软中断打断后,执行对应的 中断处理函数 ,这时便已进入 内核态 ;
  3. 系统调用处理函数 准备 内核执行栈 ,并保存所有 寄存器 (一般用汇编语言实现);
  4. 系统调用处理函数 根据 系统调用号 调用对应的 C 函数—— 系统调用服务例程 ;
  5. 系统调用处理函数 准备 返回值 并从 内核栈 中恢复 寄存器 ;
  6. 系统调用处理函数 执行 ret 指令切换回 用户态 

  一般地,系统调用都是通过软件中断实现的,x86系统上的软件中断由int $0x80指令产生,而128号异常处理程序就是系统调用处理程序system_call(),它与硬件体系有关,在entry.S中用汇编写。接下来就来看一下Linux下系统调用具体的实现过程。

调用流程


  那么,在应用程序内,调用一个系统调用的流程是怎样的呢?

  我们以一个假设的系统调用 xyz 为例,介绍一次系统调用的所有环节。

技术图片

  如上图,系统调用执行的流程如下:

    1.   应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
    2.   库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
    3.   CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
    4.   系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;

  所以如果要找到socket的系统调用,关键就是找到socket的system_call入口,在Linux-5.0.1 内核中,https://github.com/mengning/linux/blob/master/arch/x86/entry/syscalls/syscall_32.tbl 从中可以发现socket的系统调用位102号

技术图片

  那么跟踪程序如果可以跟踪到socketcall,就可以观察到系统调用,下面通过gdb调试qume中的linux源代码来跟踪系统调用。

  1.先进入menuos

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

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

 

  如下图所示:

技术图片

 

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

 1 replyhi 2 hello 

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

  如下图所示:

技术图片

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

  1 SYSCALL_DEFINE2(socketcall, int, call, unsigned long __user *, args)
  2 {
  3     unsigned long a[AUDITSC_ARGS];
  4     unsigned long a0, a1;
  5     int err;
  6     unsigned int len;
  7 
  8     if (call < 1 || call > SYS_SENDMMSG)
  9         return -EINVAL;
 10     call = array_index_nospec(call, SYS_SENDMMSG + 1);
 11 
 12     len = nargs[call];
 13     if (len > sizeof(a))
 14         return -EINVAL;
 15 
 16     /* copy_from_user should be SMP safe. */
 17     if (copy_from_user(a, args, len))
 18         return -EFAULT;
 19 
 20     err = audit_socketcall(nargs[call] / sizeof(unsigned long), a);
 21     if (err)
 22         return err;
 23 
 24     a0 = a[0];
 25     a1 = a[1];
 26 
 27     switch (call) {
 28     case SYS_SOCKET:
 29         err = __sys_socket(a0, a1, a[2]);
 30         break;
 31     case SYS_BIND:
 32         err = __sys_bind(a0, (struct sockaddr __user *)a1, a[2]);
 33         break;
 34     case SYS_CONNECT:
 35         err = __sys_connect(a0, (struct sockaddr __user *)a1, a[2]);
 36         break;
 37     case SYS_LISTEN:
 38         err = __sys_listen(a0, a1);
 39         break;
 40     case SYS_ACCEPT:
 41         err = __sys_accept4(a0, (struct sockaddr __user *)a1,
 42                     (int __user *)a[2], 0);
 43         break;
 44     case SYS_GETSOCKNAME:
 45         err =
 46             __sys_getsockname(a0, (struct sockaddr __user *)a1,
 47                       (int __user *)a[2]);
 48         break;
 49     case SYS_GETPEERNAME:
 50         err =
 51             __sys_getpeername(a0, (struct sockaddr __user *)a1,
 52                       (int __user *)a[2]);
 53         break;
 54     case SYS_SOCKETPAIR:
 55         err = __sys_socketpair(a0, a1, a[2], (int __user *)a[3]);
 56         break;
 57     case SYS_SEND:
 58         err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
 59                    NULL, 0);
 60         break;
 61     case SYS_SENDTO:
 62         err = __sys_sendto(a0, (void __user *)a1, a[2], a[3],
 63                    (struct sockaddr __user *)a[4], a[5]);
 64         break;
 65     case SYS_RECV:
 66         err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
 67                      NULL, NULL);
 68         break;
 69     case SYS_RECVFROM:
 70         err = __sys_recvfrom(a0, (void __user *)a1, a[2], a[3],
 71                      (struct sockaddr __user *)a[4],
 72                      (int __user *)a[5]);
 73         break;
 74     case SYS_SHUTDOWN:
 75         err = __sys_shutdown(a0, a1);
 76         break;
 77     case SYS_SETSOCKOPT:
 78         err = __sys_setsockopt(a0, a1, a[2], (char __user *)a[3],
 79                        a[4]);
 80         break;
 81     case SYS_GETSOCKOPT:
 82         err =
 83             __sys_getsockopt(a0, a1, a[2], (char __user *)a[3],
 84                      (int __user *)a[4]);
 85         break;
 86     case SYS_SENDMSG:
 87         err = __sys_sendmsg(a0, (struct user_msghdr __user *)a1,
 88                     a[2], true);
 89         break;
 90     case SYS_SENDMMSG:
 91         err = __sys_sendmmsg(a0, (struct mmsghdr __user *)a1, a[2],
 92                      a[3], true);
 93         break;
 94     case SYS_RECVMSG:
 95         err = __sys_recvmsg(a0, (struct user_msghdr __user *)a1,
 96                     a[2], true);
 97         break;
 98     case SYS_RECVMMSG:
 99         if (IS_ENABLED(CONFIG_64BIT) || !IS_ENABLED(CONFIG_64BIT_TIME))
100             err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
101                          a[2], a[3],
102                          (struct __kernel_timespec __user *)a[4],
103                          NULL);
104         else
105             err = __sys_recvmmsg(a0, (struct mmsghdr __user *)a1,
106                          a[2], a[3], NULL,
107                          (struct old_timespec32 __user *)a[4]);
108         break;
109     case SYS_ACCEPT4:
110         err = __sys_accept4(a0, (struct sockaddr __user *)a1,
111                     (int __user *)a[2], a[3]);
112         break;
113     default:
114         err = -EINVAL;
115         break;
116     }
117     return err;
118 }
119 
120 #此函数根据不同的call来进入不同的分支,从而调用不同的内核处理函数。
121 #在replyhi/hello的执行过程中,涉及到了socket的建立、recv、send等,这些不同的系统调用传给SYSCALL_DEFINE2()的参数call是不同的。

以上是关于Socket与系统调用深度分析的主要内容,如果未能解决你的问题,请参考以下文章

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析

Socket系统调用Socket与系统调用深度分析

Socket与系统调用深度分析

Socket与系统调用深度分析