小甜、
Published on 2017-09-02 11:31 in 暂未分类 with 小甜、

Socket与系统调用深层分析

实验背景:

  • Socket API编程接口之上可以编写基于不同网络协议的应用程序;
  • Socket接口在用户态通过系统调用机制进入内核;
  • 内核中将系统调用作为一个特殊的中断来处理,以socket相关系统调用为例进行分析;
  • socket相关系统调用的内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法;

前言

之前我们简单分析了用户态下封装的Socket工具与底层Socket的关系详情见这里,本次实验将针对Socket的调用过程,基于Linux提供的Socket相关接口进行其用户态到系统态的原理及过程分析,包括对Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数的详细分析。 本次将首先从简单Socket调用原理入手,讲解Socket函数调用链关系,再进行底层调用的探究实验。
首先抛出问题,用户态下的Socket怎么与底层内核建立连接的呢?

系统调用

在计算机系统中,通常运行着两类程序:系统程序和应用程序,为了保证系统程序不被应用程序有意或无意地破坏,为计算机设置了两种状态:

  • 系统态(也称为管态或核心态),操作系统在系统态运行
  • 用户态(也称为目态),应用程序只能在用户态运行。

正常情况下,应用程序工作在用户态下,出于保护系统安全性的目的,用户态留给用户可用功能有限,所以就预留给用户一些可用内核空间,使应用程序可以通过系统调用的方法,间接调用操作系统的相关过程,取得相应的服务。当需要执行内核操作时就需要进行向内核态的转换,可以称之为系统调用。

状态的转换通过软中断进入,中断一般有两个属性,一个是中断号,一个是中断处理程序。不同的中断有不同的中断号,每个中断号都对应了一个中断处理程序。在内核中通过维护中断向量表维护这一关系。当中断到来时,cpu会暂停正在执行的代码,根据中断号去中断向量表找出对应的中断处理程序并调用。中断处理程序执行完成后,会继续执行之前的代码。这里涉及状态保存及返回问题,不做过多描述,嵌套的调用过程如下:

我们这里说的软中断通常是一条指令,使用这条指令用户可以手动触发某个中断。例如在i386下,对应的指令是int,在int指令后指定对应的中断号,如int 0x80代表调用第0x80号的中断处理程序。
在此,我们以一个经典的xyz函数系统调用为例进行还原以上系统调用过程

  1. 应用程序 代码调用系统调用( xyz ),该函数是一个包装系统调用的 库函数 ;
  2. 库函数 ( xyz )负责准备向内核传递的参数,并触发 软中断 以切换到内核;
  3. CPU 被 软中断 打断后,执行 中断处理函数 ,即 系统调用处理函数 ( system_call);
  4. 系统调用处理函数 调用 系统调用服务例程 ( sys_xyz ),真正开始处理该系统调用;
    总结下来就是用户执行带有中断指令的程序时,执行到中断调用指令int 0x80会跳转到中断处理函数,这也就是系统中断调用的接入口,通过这个介入口获取到进入内核态所需的资源,当现场保存完成、返回地址保存完成后cpu进入到内核态,并从system_call处开始指令执行(同时sys_call_table也就是上面说到的系统调用表),返回用户态时类似,具体函数调用过程如下:
  5. start_kernel
  6. trap_init
  7. idt_setup_traps

跟踪系统调用

对系统调用有了大致了解后我们进入正题,基于上次实验qumu模拟器和gdb调试观察系统调用过程。
首先观察Replyhi函数

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 StartReplyhi(int argc, char *argv[])
{
    int pid;
    /* fork another process */
    pid = fork();
    if (pid < 0)
    {
        /* error occurred */
        fprintf(stderr, "Fork Failed!");
        exit(-1);
    }
    else if (pid == 0)
    {
        /*   child process  */
        Replyhi();
        printf("Reply hi TCP Service Started!\n");
    }
    else
    {
        /*  parent process   */
        printf("Please input hello...\n");
    }
}
 
 
int main()
{
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("time","Show System Time",Time);
    MenuConfig("time-asm","Show System Time(asm)",TimeAsm);
    MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
    ExecuteMenu();
}

我们发现Replyhi函数中,依次调用了InitializeService()、ServiceStart()、RecvMsg()、SendMsg()、ServiceStop()以及最后的ShutdownService()函数,我们依次来看这些函数究竟是如何调用socket API的。

#ifndef _SYS_WRAPER_H_
#define _SYS_WRAPER_H_
 
#include<stdio.h>
#include<arpa/inet.h> /* internet socket */
#include<string.h>
//#define NDEBUG
#include<assert.h>
 
#define PORT                5001
#define IP_ADDR             "127.0.0.1"
#define MAX_BUF_LEN         1024
 
/* private macro */
#define PrepareSocket(addr,port)                        \
        int sockfd = -1;                                \
        struct sockaddr_in serveraddr;                  \
        struct sockaddr_in clientaddr;                  \
        socklen_t addr_len = sizeof(struct sockaddr);   \
        serveraddr.sin_family = AF_INET;                \
        serveraddr.sin_port = htons(port);              \
        serveraddr.sin_addr.s_addr = inet_addr(addr);   \
        memset(&serveraddr.sin_zero, 0, 8);             \
        sockfd = socket(PF_INET,SOCK_STREAM,0);
         
#define InitServer()                                    \
        int ret = bind( sockfd,                         \
                        (struct sockaddr *)&serveraddr, \
                        sizeof(struct sockaddr));       \
        if(ret == -1)                                   \
        {                                               \
            fprintf(stderr,"Bind Error,%s:%d\n",        \
                            __FILE__,__LINE__);         \
            close(sockfd);                              \
            return -1;                                  \
        }                                               \
        listen(sockfd,MAX_CONNECT_QUEUE);
 
#define InitClient()                                    \
        int ret = connect(sockfd,                       \
            (struct sockaddr *)&serveraddr,             \
            sizeof(struct sockaddr));                   \
        if(ret == -1)                                   \
        {                                               \
            fprintf(stderr,"Connect Error,%s:%d\n",     \
                __FILE__,__LINE__);                     \
            return -1;                                  \
        }
/* public macro */              
#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("rely \"hi\" to %s:%d\n",            \
            (char*)inet_ntoa(clientaddr.sin_addr),      \
            ntohs(clientaddr.sin_port));                \
        }
         
#endif /* _SYS_WRAPER_H_ */

综合以上代码,我们能够看到系统定义的函数首先调用InitializeService(),根据定义,依次调用socket()--->bind()--->listen(),这些是socket编程的一般步骤。然后调用ServiceStart()函数,通过宏定义,调用了accept()函数。然后是RecvMsg()和SendMsg()函数,根据宏定义,调用了recv和send函数

当我们查看socket.c源代码,能够发现,Socket的第一步,socket()函数首先进行了系统调用,也就是对入口函数sys_scoketcall的调用,通过传入用户定义的参数地址,进行系统调用的传参。
接下来我们在开始gdb跟踪之前找到系统自定义的函数宏定义标准,其结果如下(用于后面跟踪调试时查看具体是什么调用过程):

#define SYS_SOCKET  1       /* sys_socket(2)        */
#define SYS_BIND    2       /* sys_bind(2)          */
#define SYS_CONNECT 3       /* sys_connect(2)       */
#define SYS_LISTEN  4       /* sys_listen(2)        */
#define SYS_ACCEPT  5       /* sys_accept(2)        */
#define SYS_GETSOCKNAME 6       /* sys_getsockname(2)       */
#define SYS_GETPEERNAME 7       /* sys_getpeername(2)       */
#define SYS_SOCKETPAIR  8       /* sys_socketpair(2)        */
#define SYS_SEND    9       /* sys_send(2)          */
#define SYS_RECV    10      /* sys_recv(2)          */
#define SYS_SENDTO  11      /* sys_sendto(2)        */
#define SYS_RECVFROM    12      /* sys_recvfrom(2)      */
#define SYS_SHUTDOWN    13      /* sys_shutdown(2)      */
#define SYS_SETSOCKOPT  14      /* sys_setsockopt(2)        */
#define SYS_GETSOCKOPT  15      /* sys_getsockopt(2)        */
#define SYS_SENDMSG 16      /* sys_sendmsg(2)       */
#define SYS_RECVMSG 17      /* sys_recvmsg(2)       */
#define SYS_ACCEPT4 18      /* sys_accept4(2)       */
#define SYS_RECVMMSG    19      /* sys_recvmmsg(2)      */
#define SYS_SENDMMSG    20      /* sys_sendmmsg(2)      */其中 

所以接下来针对sys_scoketcall函数监视,观察系统调用过程。
首先开启qemu模拟器,执行
qemu -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img -append nokaslr -s -S
打开新的终端窗口,进入gdb调试,执行
file ~/LinuxKernel/linux-5.0.1/vmlinux
b sys_socketcall
target remote:1234

在qemu模拟器中继续执行,键入replyhi,观察断点监视情况如下:

能够看到此次过程调用了4次sys_socketcall函数,其中调用的编号分别为 1、2、4、5至此我们查看sys_define中的具体定义,在此忽略。以上过程调用过程依次对应了,__sys_socket、__sys_bind、__sys_listen、__sys_accept函数调用,至此Socket所需资源初始化成功,我们继续进行跟踪,在qemu中键入hello,其结果如下:


能够看到这次hello回应结束后,继续执行断点,看到调用编号分别为1、3、10、9、10、9、10、9、10、9、5查看上面的函数宏定义分别对应函数sys_socket(2) sys_connect(2) sys_recv(2) sys_send(2) sys_recv(2) sys_send(2) sys_recv(2) sys_send(2) sys_recv(2) sys_send(2) sys_accept(2)
这也完全对应上了上述过程,描述如下:

  • 服务端创建socket
  • 建立tcp连接
  • 进行hello hi的四次通信过程
  • 继续回到accpet状态接收消息
    至此,基于qemu及gdb调试过程结束,socket如何在内核中变化定义也有了一些眉目。
posted @ 2019-12-19 19:20  小甜、  阅读(346)  评论(0编辑  收藏  举报