zoukankan      html  css  js  c++  java
  • 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] = "";
        char szReplyMsg[MAX_BUF_LEN] = "hi";
        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!
    ");
        }
        else
        {
            /*  parent process   */
            printf("Please input hello...
    ");
        }
    }
     
     
    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
    ",        
                                __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
    ",     
                    __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
    ",      
                                __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
    ",          
                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
    ",            
                (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如何在内核中变化定义也有了一些眉目。
  • 相关阅读:
    vmware ubuntu 异常关机无法连接到网络
    Speed up GCC link
    常用的一些解压命令
    Log4j 漏洞复现
    Test Case Design method Boundary value analysis and Equivalence partitioning
    CCA (Citrix Certified Administrator) exam of “Implementing Citrix XenDesktop 4”
    What is Key Word driven Testing?
    SAP AGS面试小结
    腾讯2013终端实习生一面
    指针的引用
  • 原文地址:https://www.cnblogs.com/xshun/p/12069643.html
Copyright © 2011-2022 走看看