Socket与系统调用深度分析

1.系统调用概述

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

 

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

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

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

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

 

2.系统调用基本机制

前文已经提到了Linux下的系统调用是通过0x80实现的,但是显然一个操作系统有各式各样的系统调用,这样问题就出来了,对于所有系统调用我们都采用同一个软中断进入内核,而对于同一个中断号是如何处理多个不同的系统调用的?

最简单的方式是对于不同的系统调用采用不同的中断号,但是中断号明显是一种稀缺资源,Linux显然不会这么做;还有一个问题就是系统调用是需要提供参数,并且具有返回值的,这些参数又是怎么传递的?也就是说,对于系统调用我们要搞清楚两点:

 

1.系统调用的函数名称转换。

2.系统调用的参数传递。

 

首先看第一个问题。实际上,Linux中每个系统调用都有相应的系统调用号作为唯一的标识,内核维护一张系统调用表,sys_call_table,表中的元素是系统调用函数的起始地址,而系统调用号就是系统调用在调用表的偏移量。在x86上,系统调用号是通过eax寄存器传递给内核的。

系统调用表(System call Table),是一张由指向实现各种系统调用的内核函数的函数指针组成的表,该表可以基于系统调用编号进行索引,来定位函数地址,完成系统调用。

该表在linux-5.0.1/arch/x86/entry/syscalls目录下可以找到

 

系统调用的大致过程如下图:

 Socket与系统调用深度分析_第1张图片

 

 

 

系统调用表:

查询系统调用表可得,本次实验用到的系统调用大致如上图

 Socket与系统调用深度分析_第2张图片

系统调用号:

如上文所说,系统调用号记载了各个系统调用在系统调用表里的偏移量,在文件~/kernel/linux-5.0.1/arch/sh/include/uapi/asm/unistd_32.h中查询可得系统调用号,来验证是否如上文所说:

由图可知,每个系统调用确实有自己唯一的编号

 

Socket与系统调用深度分析_第3张图片

 

 

 

3.Socket使用到的系统调用分析

首先我们已经在上文中列出了socket编程可能涉及到的系统调用,在这里我们可以通过查看c文件来获悉,上次实验的功能是如何实现的,下面分别列出客户端和服务端的代码

int Replyhi()

{

       char szBuf[MAX_BUF_LEN] = "\0";

       char szReplyMsg[MAX_BUF_LEN] = "hi\0";

       InitializeService();

       while (1)

       {

              ServiceStart();

              RecvMsg(szBuf);

              SendMsg(szReplyMsg);

              ServiceStop();

       }

       ShutdownService();

       return 0;

}

 

int Hello(int argc, char *argv[])

{

       char szBuf[MAX_BUF_LEN] = "\0";

       char szMsg[MAX_BUF_LEN] = "hello\0";

       OpenRemoteService();

       SendMsg(szMsg);

       RecvMsg(szBuf);

       CloseRemoteService();

       return 0;

}

可以看出,实际上replyhi和hello程序还是比较简单的,我们进一步查看replyhi和hello函数调用的API可得:

#define InitializeService()                             \

        PrepareSocket(IP_ADDR,PORT);                    \

        InitServer();

       

#define ShutdownService()                               \

        close(sockfd);

        

#define OpenRemoteService()                             \

        PrepareSocket(IP_ADDR,PORT);                    \

        InitClient();                                   \

        int newfd = sockfd;

       

#define CloseRemoteService()                            \

        close(sockfd);

             

#define ServiceStart()                                  \

        int newfd = accept( sockfd,                     \

                    (struct sockaddr *)&clientaddr,     \

                    &addr_len);                         \

        if(newfd == -1)                                 \

        {                                               \

            fprintf(stderr,"Accept Error,%s:%d\n",      \

                            __FILE__,__LINE__);         \

        }       

#define ServiceStop()                                   \

        close(newfd);

       

#define RecvMsg(buf)                                    \

       ret = recv(newfd,buf,MAX_BUF_LEN,0);             \

       if(ret > 0)                                      \

       {                                                \

            printf("recv \"%s\" from %s:%d\n",          \

            buf,                                        \

            (char*)inet_ntoa(clientaddr.sin_addr),      \

            ntohs(clientaddr.sin_port));                \

       }

      

#define SendMsg(buf)                                    \

        ret = send(newfd,buf,strlen(buf),0);            \

        if(ret > 0)                                     \

        {                                               \

            printf("send \"hi\" to %s:%d\n",            \

            (char*)inet_ntoa(clientaddr.sin_addr),      \

            ntohs(clientaddr.sin_port));                \

        }

       

#endif /* _SYS_WRAPER_H_ */

 

 

这些只是socket API调用的宏定义,牵涉到的系统调用还是上文提及的那几种,整个流程也是经典的socket编程流程,大致过程如下:

 Socket与系统调用深度分析_第4张图片

 

 

 

 

下面进行实验的验证:

将上述涉及到的系统调用,依次打上断点,然后进行远程调试,查看分析是否正确

 Socket与系统调用深度分析_第5张图片

捕获系统调用:

 Socket与系统调用深度分析_第6张图片

 

 

 Socket与系统调用深度分析_第7张图片

 

 

 

由图可知,我们确实捕获到了设置断点的系统调用,证明socket API函数确实要用到相关1系统调用。这只是一个简单的分析,至此我们知道socket API函数需要使用相关系统调用但我们却不知道其中的细节,通过查阅资料获悉,一个socket API函数使用所依赖的系统调用大致如下图:

Socket与系统调用深度分析_第8张图片

 

 

由此可看出,使用socket 函数的第一步是使用__sys_socketcall()系统调用。下面验证假设的正确性,将__sys_socketcall()设成断点,看程序进行过程中是否进行了调用。

然后进行调试发现:

 Socket与系统调用深度分析_第9张图片

 

 

结果满足上面的假设,确实捕捉到了相应系统调用,然后根据上面的提示,查看源码可得:

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;

}

 

这主要是一个switch函数,根据call值来决定系统调用,这也进一步验证了上文的猜想

 

参考文档:

1.linux内核剖析(六)Linux系统调用详解(实现机制分析)

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

你可能感兴趣的:(Socket与系统调用深度分析)