• 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如何在内核中变化定义也有了一些眉目。
  • 相关阅读:
    python学习笔记(4):面向对象
    PowerShell中hashtable排序问题
    PowerShell DSC Resource更新问题
    移除PowerShell InvokeCommand返回值ConvertToJSON后PSComputerName,RunspaceId,PSShowComputerName
    This File Came From Another Computer And Might be Blocked
    查看.NET Framework版本
    SQLServer backup and restore
    Winrm HTTPS
    PowerShell中异步方法的使用
    System32下的一些快捷应用
  • 原文地址:https://www.cnblogs.com/xshun/p/12069643.html
Copyright © 2020-2023  润新知