Socket与系统调用深度分析

本次实验要求:

请将Socket API编程接口、系统调用机制及内核中系统调用相关源代码、 socket相关系统调用的内核处理函数结合起来分析,并在X86 64环境下Linux5.0以上的内核中进一步跟踪验证。

Socket API编程接口:

 C语言中的Socket API就是一种涉及系统调用的API,常用的函数如下:

int socket(int domain, int type, int protocol)
//创建一个新的套接字,返回套接字描述符


int connect(int sockfd, struct sockaddr *server_addr, int sockaddr_len)
//同远程服务器主动连接,成功时返回0,失败时返回1


int bind(int sockfd, struct sockaddr* my_addr, int addrlen)
//为套接字指明一个本地端点地址,TCP/IP协议使用sockaddr_in结构,包含IP地址和端口号,服务器使用它来指明熟悉的端口号,然后等待连接


int listen(int sockfd, int input_queue_size)
//面向连接的服务器指明某个套接字,将其置为被动模式,并准备接收传入连接


int accept(int sockfd, void* addr, int* addrlen)
//获取传入连接请求,返回新的连接套接字描述符,为每个新连接请求创建一个新的套接字,服务器只对新的连接使用该套接字,原来的监听套接字接受其他的连接请求。新的连接上传输数据使用新的套接字


int sendto(int sockfd, const void* data, int data_len, unsigned int flags, struct sockaddr* remaddr,int remaddr_len)
//基于UDP发送数据报,返回实际发送的数据长度,出错时返回1


int send(int sockfd, const void* data, int data_len, unsigned int flags)
//在TCP连接上发送数据,返回成功传送数据的长度,出错时返回-1,将外发数据复制到OS内核中


int recvfrom(int sockfd, void *buf, int buf_len,unsigned int flags,struct sockaddr *from,int *fromlen);
//从UDP接收数据,返回实际接收的字节数,失败时返回-1


int recv(int sockfd, void* buf, int buf_len,unsigned int flags) 
//从TCP接收数据,返回实际接收的数据长度,出错时返回-1。服务器使用其接收客户请求,客户使用它接受服务器的应答。如果没有数据,将阻塞,如果收到的数据大于缓存的大小,多余的数据将丢弃

close(int sockfd)
//撤销套接字,如果只有一个进程使用,立即终止连接并撤销该套接字,如果多个进程共享该套接字,将引用数减一,如果引用数降到零,则撤销它

  网络程序调用基本流程如下图所示:

socket api和系统调用关系

系统调用:

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

系统态(也称为管态或核心态),操作系统在系统态运行
用户态(也称为目态),应用程序只能在用户态运行。
在实际运行过程中,处理机会在系统态和用户态间切换。相应地,现代多数操作系统将 CPU 的指令集分为特权指令和非特权指令两类。

执行态切换过程:

应用程序在用户态准备好调用参数,执行 int 指令触发软中断 ,中断号为 0x80 ;

CPU 被软中断打断后,执行对应的中断处理函数 ,这时便已进入内核态 ;

系统调用处理函数准备内核执行栈 ,并保存所有寄存器 (一般用汇编语言实现);

系统调用处理函数根据系统调用号调用对应的 C 函数—— 系统调用服务例程 ;

系统调用处理函数准备返回值并从内核栈中恢复 寄存器 ;

系统调用处理函数执行 ret 指令切换回用户态 ;

socket系统调用发生流程

当用户进程使用socket API 的时候,会产生向量为0x80的编程异常,系统执行系统调用。

进程传递系统调用号到寄存器eax,指明需要哪个系统调用,同时会将系统调用需要的参数存入相关寄存器。

系统调用处理函数system_call是Linux中所有系统调用的入口点,通过进程存在eax寄存器中的系统调用号决定调用哪个系统调用。

其中,socket api有两种系统调用方式:(1)所有的socket系统调用的总入口是sys_socketcall(系统调用号102)  (2)每一个独立的socket api都对应一个单独的系统调用。

初始化 MenuOS 系统的网络功能,跟踪分析 TCP 协议:

在本次实验中,我们创建的是一个利用socket的基于TCP的连接,接下来我们结合源码,接口来进行整个hello/hi的实现过程的调用分析与追踪。

上次的课程实验实现了menuos的调试环境的配置主要是在menuos中增加了replyhi和hello两条命令,其结果大致如下:

我们首先以最常用的listen()函数为例进行分析:

进入上次的MenuOS目录,更改makefile,将-S参数去除。(否则将会挂起CPU)

之后在终端编译:

make rootfs

  结果如下所示:

在 gdb中给__sys_linsten打上断点:

gdb
file ./vmlinux
target remote:1234
break __sys_listen 

结果如下所示: 

 

说明gdb已找到这两条系统调用的函数定义所在。

我们接下来看一下接下来要分析的replyhi和hello的源码

int main()
{
    BringUpNetInterface();
    PrintMenuOS();
    SetPrompt("MenuOS>>");
    MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL);
    MenuConfig("quit","Quit from MenuOS",Quit);
    MenuConfig("replyhi", "Reply hi TCP Service", StartReplyhi);
    MenuConfig("hello", "Hello TCP Client", Hello);
    ExecuteMenu();
}

从上面代码中看到replyhi和hello命令需要分别调用 StartReplyhi 和 Hello函数。

下面是StartReplyhi的源码:

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...
");
    }
}

  从上面看出StartReplyhi又调用了

{
    char szBuf[MAX_BUF_LEN] = "";
    char szReplyMsg[MAX_BUF_LEN] = "hi";
    InitializeService();
    while (1)
    {
        ServiceStart();
        RecvMsg(szBuf);
        SendMsg(szReplyMsg);
        ServiceStop();
    }
    ShutdownService();
    return 0;
}
#InitializeService(),ServiceStart()这些函数均定义在头文件syswrapper.h中
/* public macro */ 
#define InitializeService()                             
        PrepareSocket(IP_ADDR,PORT);                    
        InitServer();
#InitializeService()中分别调用了PrepareSocket和InitServer()
/* 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);
#显然我们可以发现到这一步,InitializeService这个宏已经完成了socket的创建,绑定和listen
#我们继续看ServiceStart(),RecvMsg(szBuf),SendMsg(szReplyMsg),ServiceStop(),ServiceStop()
#define ServiceStart() 
 int newfd = accept( sockfd, 
                    (struct sockaddr *)&clientaddr,     
                    &addr_len);                         
        if(newfd == -1)                                 
        {                                               
            fprintf(stderr,"Accept Error,%s:%d
",      
                            __FILE__,__LINE__);         
        }
#这一步完成了accept
 
#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));                
       }
       #这一步完成了recv
#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));                
        }
#这一步完成了send
#define ServiceStop() 
 close(newfd);
#这一步完成了close

  

hello的过程与其类似,通过源码我们可以看出replayhi函数涉及到的系统调用按照调用顺序为

socket,bind,listen,accept,recv,send

 于是我们在一个终端打开qemu启动MenuOS(指令中去掉 -S,在另一个终端用gdb读入linux-5.0.1的vmlinux,

通过端口1234与qemu建立连接,在相关的系统调用内核处理函数处设置断点,结果如下图所示:

接着在gdb中按c运行Menu OS

 

上图中可以看到,首先就等待了。然后我们在Menu OS中打开服务器,即输入replyhi:

不停地按回车,发现捕获到如下断点,一直到sys_accept4函数停止。说明此时服务器处于阻塞状态,一直在等待客户端连接。

 

打开qemu发现指令已经完整运行:

通过实验可知,在replyhi中,分别调用了socket、bind、listen、accept。

结果与预想一致,至此完成对replyhi/hello的追踪。

原文地址:https://www.cnblogs.com/zzydexiaowu/p/12067150.html