Socket与系统调用深度分析

本实验讲述的是一个简单的socket网络应用程序,如何一步步的执行到内核。基于这个实验,来大概分析一下,socket网络程序从用户态到内核态的流程。

 

实验环境是ubuntu18.04,但是用qemu加载linux-5.0.1内核,内核配置是采用x86_64defconfig,并分别制作简易的32位menusOS和64位menuOS,用gdb跟踪内核代码,看看执行过程是怎样的。

 

我们从上层到下层来看:

一、应用层

menuOS是一个简单的菜单系统,里面加入了简单网络应用,我们主要跟踪网络应用程序。首先来看一下简单网络程序代码:

完整代码在这,可以下载下来看看

https://github.com/mengning/menu.git

本实验用的是lab3代码。

下面来简单看下网络程序代码

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();
}
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 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;
}
#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("send \"hi\" to %s:%d\n",            \
            (char*)inet_ntoa(clientaddr.sin_addr),      \
            ntohs(clientaddr.sin_port));                \
        }

上面其实就是一个简单的TCP网络程序,实现的是一个Hello/hi的功能,当在命令行输入replyhi,hello,会执行相关socketAPI接口,主要就是socket,bind,listen,accept,send,recv,connect,close这几个函数。

应用层写的socke函数,先是调用了libc库中对应的函数,32位libc库中对应函数有一句汇编指令 int $0x80,这是陷入内核的指令,0x80是系统调用的中断号,64位libc库中对应函数的汇编指令是system_call,我们都来跑一下。

 

先不用qemu加载内核,先在ubuntu上用gdb跟踪一下程序在应用层如何陷入内核。

为了简单,我们不修改Makefile,直接编译,先来编译成32位程序,如下:

gcc -o main *.c -g -m32 -lpthread -static
gdb main

 

先在ubuntu上调试main函数,跟踪由应用层陷入内核过程。

先在socket加断点,然后打开汇编代码窗口,逐步跟踪汇编,一直输入si,直到出现int $0x80

 

 

再来看下本机ubuntu的内核版本

这就说明了32位程序在64位内核版本下陷入内核的指令是int $0x80

 

下面来看一下64位程序在64位内核版本下陷入内核的指令是什么?

只需要将编译命令改一下

gcc -o main *.c -g -m64 -lpthread -static
gdb main

 

其他步骤一致,结果如下

64位程序陷入内核的汇编指令是syscall。

 

二、内核处理过程

接下来跟踪内核,跟踪socket,bind,listen等函数对应的内核处理函数是什么?

同样,分别跟踪32位应用程序和64位程序分别对应的内核处理函数。

首先来跟踪32位应用程序对应的内核处理函数。

make rootfs
cd ..
qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S

 

 

 再打开一个终端,进入linux-5.0.1目录

gdb
file vmlinux
target remote:1234

 

 

 查看32位系统调用列表

 

 

b sys_socketcall

 提示未定义,于是尝试下后面的函数名,看意思是兼容32的socket函数。

__ia32_compat_sys_socketcall

 接下来,输入c

 

过程中提示遇到断点,输入c,或者等到MenuOS启动再加断点也可以。

我们在MenuOS输入replyhi

replyhi

 

 查看net/compat.c源码

 

 找到处理各种socket接口代码,再加个断点,加在859行,然后看看调用了哪些接口

 说明首先调用的是__sys_socket函数

 

 接着又调用__sys_bind函数

 

 接着又调用__sys_listen,后面的跟踪方法类似,并且跟应用代码调用顺序保持一致。

 

下面再来看下,64位应用程序socket API对应的内核处理函数是什么?

查看64位系统调用列表

 

 

 重新制作rootfs,修改Makefile,生成64位的MenuOS

#
# Makefile for linuxnet/lab3
#

CC_PTHREAD_FLAGS             = -lpthread
CC_FLAGS                     = -c 
CC_OUTPUT_FLAGS                 = -o
CC                           = gcc
RM                           = rm
RM_FLAGS                     = -f

TARGET  =   main
OBJS    =   linktable.o  menu.o main.o

all:    $(OBJS)
    $(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) 
rootfs:
    gcc -o init linktable.c menu.c main.c -m64 -static -lpthread #将-m32改成-m64
    find init | cpio -o -Hnewc |gzip -9 > ../rootfs.img
    #qemu-system-x86_64 -kernel ../../linux-5.0.1/arch/x86/boot/bzImage -initrd ../rootfs.img
.c.o:
    $(CC) $(CC_FLAGS) $<

clean:
    $(RM) $(RM_FLAGS)  $(OBJS) $(TARGET) *.bak init
make rootfs
cd ..
qemu-system-x86_64 -kernel ../linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -append "root=/dev/sda init=/init nokaslr" -s -S

 

其他操作基本一致,还在另外一个终端输入同样的命令

file vmlinux
target remote:1234
c

然后 Ctrl+C中断,进入gdb

按照64位系统调用列表,在__x64_sys_socket,__x64_sys_bind,__x64_sys_listen等函数加断点

b __x64_sys_socket
b __x64_sys_bind
b __x64_sys_listen
b __x64_sys_accept
b __x64_sys_sendmsg
b __x64_sys_recvmsg
c

 

然后在MenuOS里输入replyhi,在gdb调试窗口输入c

 

 

 

 

 

 但是却没有在__x64_sys_sendmsg和__x64_sys_recvmsg停止,猜测可能内核读写调的不是这两个函数,于是在__x64_sys_read和__x64_sys_write两个函数加断点,再跟踪看看

b __x64_sys_read
b __x64_sys_write

 

 

 结果在__x64_sys_read和__x64_sys_write停止了,说明recv和send对应的内核函数是__x64_sys_read和__x64_sys_write。

这样对32位和64位应用程序在64位内核上,从应用层陷入内核,以及socket相关API对应的内核处理函数都跟踪完了。

 

 接下来来看一下稍微深层的函数

 

 由代码可知,应用层的socket,bind,listen等函数,在内核中对应的函数是__sys_socket,__sys_bind,__sys_listen等函数,再来看看__sys_socket,__sys_bind,__sys_listen这三个函数的定义,在socket.c文件里定义了

 

 

 

 

 

由代码可以看出,在__sys_socket函数中,根据参数初始化了sock结构体,在__sys_bind和__sys_listen等函数中调用了sock->ops->bind函数和sock->op->listen函数。

 

可以看出这是多态,在__sys_socket函数中根据输入参数初始化了sock结构体,然后在__sys_bind和__sys_listen函数里调用sock->ops->bind和sock->ops->listen函数,这是内核处理函数内部通过“多态机制”对不同的网络协议进行的封装方法。

 

以上就是简述一下socketAPI调用流程的大致过程,都是手敲,如有错误还请指教!


posted @ 2019-12-18 18:20  gang.w  阅读(474)  评论(0编辑  收藏  举报