zoukankan      html  css  js  c++  java
  • 构建调试Linux内核网络代码的环境MenuOS系统

    构建调试Linux内核网络代码的环境MenuOS系统

    1.搭建linux环境

    linux内核环境指的是我们用虚拟机运行linux系统,在linux上运行我们开发的网络代码,这样做的好处就是方便调试,通过虚拟机,我们可以用gdb调试,观察内核运行到哪里了,尤其是针对网络方面的接口(如socket、bind等),调试使我们清晰的看到程序调用了什么,执行了什么,这对于我们的学习大有脾益,而为了搭建环境,我们需要1.下载并编译Linux内核,2.安装qemu,

    下载并编译linux内核

    注意,为了编译内核,我们需要在系统安装部分的编译工具:

    `sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev`
    

    用命令将它们一套带走,当然安装的过程可能会不太顺利,各种库文件的依赖最终不一定能解决,所以推荐使用ubuntu16,ubuntu18亲测不太顺利。
    内核的下载地址:https://www.kernel.org/,下载任意版本都行,我这里选择的是5.2.7
    下载完成后解压,到你的工作目录,然后就可以开始编译了。
    编译:
    由于linux内核默认编译的是x86体系对应的镜像文件,所以我们可以直接make defconfig生成配置文件,当然,如果你喜欢32位的,使用命令make i386_defconfig,
    生成配置文件后再执行一次make menuconfig以防编译报错。
    到此,配置就结束了,make -j3,用3核编译镜像文件,这个过程可能会很久大约1个小时吧!

    安装qemu

    qemu实在是太强了,好用到不行,安装也很简单,
    sudo apt-get install qemu
    等安装完成就结束了,我们可以先试试能不能运行:
    qemu-system-x86_64 -kernel bzImage
    由于之前编译的是64位的镜像,所以选择运行x86_64位的qemu,镜像文件可以从linux-5.2.7/arch/x86_64/boot/bzImage拷贝出来。
    图片2
    可以看到,qemu成功启动,但是内核并没有运行成功,报kernel panic警告,当然啦,因为我们还没有制作文件系统,而内核执行到一定步骤需要和文件系统交互的,现在没有文件系统,内核也就无法继续执行下去了。

    文件系统制作

    制作一个文件是比较麻烦的,一般要下载一个Busybox,然后编译、安装,之后添加需要的文件,不过这次实验老师已经制作好了,我们可以直接使用
    git clone https://github.com/mengning/menu.git
    进入这个文件系统的目录,执行make操作,就可以在这个文件夹得到rootfs.img的镜像
    图片4
    有了文件系统,我们再次用虚拟机加载镜像总没问题了吧

    `qemu-system-x86_64 -kernel bzImage -initrd rootfs.img`
    ![图片5](http://m.qpic.cn/psb?/V10N7dSz1VKWQ9/zAhKkD4L4kBEcmCzTJ55QHB03xe8SUuPKI9OiY7hYO8!/b/dFQBAAAAAAAA&bo=0gKVAdIClQEDGTw!&rf=viewer_4)
    

    执行一个Help操作,看一下menuos有什么功能:
    图片6
    暂时只有5个功能,后面也有这个命令的解释,到此,环境搭建成功#稳!!!,虽然成功了,但是先不急,我们看看看看这个文件系统有什么
    图片3
    暂时不知道从哪开始看,那首先看Makefile怎么写的:

    #
    # Makefile for Menu Program
    #
    
    CC_PTHREAD_FLAGS			 = -lpthread
    CC_FLAGS                     = -c 
    CC_OUTPUT_FLAGS				 = -o
    CC                           = gcc
    RM                           = rm
    RM_FLAGS                     = -f
    
    TARGET  =   test
    OBJS    =   linktable.o  menu.o test.o client.o server.o 
    
    all:	$(OBJS)
    	$(CC) $(CC_OUTPUT_FLAGS) $(TARGET) $(OBJS) 
    rootfs:
    	gcc -o init linktable.c menu.c test.c client.c server.c  -m32 -static -lpthread
    	gcc -o hello hello.c -m32 -static
    	find init hello | cpio -o -Hnewc |gzip -9 > ../rootfs.img
    .c.o:
    	$(CC) $(CC_FLAGS) $<
    
    clean:
    	$(RM) $(RM_FLAGS)  $(OBJS) $(TARGET) *.bak
    
    

    从linktable.c menu.c test.c client.c server.c几个文件找main函数,最终再test.c找到了

    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("server","socket server",server);
        MenuConfig("client","socket client 
     send infomation: ",client);
        ExecuteMenu();
    }
    

    很容易理解,printMenuos()打印了menuos的logo,menuconfig添加了menuos的功能,也就是之前执行help看到的几个命令,那excuteMenu()就是实现这个系统接收命令并执行命令的咯。那再看看这个函数再哪,找到menu.c:

    menu.c
    /* Menu Engine Execute */
    int ExecuteMenu()
    {
       /* cmd line begins */
        while(1)
        {
    		int argc = 0;
    		char *argv[CMD_MAX_ARGV_NUM];
            char cmd[CMD_MAX_LEN];
    		char *pcmd = NULL;
            printf("%s",prompt);
            /* scanf("%s", cmd); */
    		pcmd = fgets(cmd, CMD_MAX_LEN, stdin);
    		if(pcmd == NULL)
    		{
    			continue;
    		}
            /* convert cmd to argc/argv */
    		pcmd = strtok(pcmd," ");
    		while(pcmd != NULL && argc < CMD_MAX_ARGV_NUM)
    		{
    			argv[argc] = pcmd;
    			argc++;
    			pcmd = strtok(NULL," ");
    		}
            if(argc == 1)
            {
                int len = strlen(argv[0]);
                *(argv[0] + len - 1) = '';
            }
            tDataNode *p = (tDataNode*)SearchLinkTableNode(head,SearchConditon,(void*)argv[0]);
            if( p == NULL)
            {
                continue;
            }
            printf("%s - %s
    ", p->cmd, p->desc);
            if(p->handler != NULL) 
            { 
                p->handler(argc, argv);
            }
        }
    } 
    

    果然跟我们想的一样,但是这里有几个细节需要关注一下:
    第一个就是如何将命令保存,menuos会根据不同输入的命令执行对应的处理函数,那如何将这些命令存储在文件系统呢?就是靠这个结构体:

    typedef struct DataNode
    {
        tLinkTableNode * pNext;
        char*   cmd;
        char*   desc;
        int     (*handler)(int argc, char *argv[]);
    } tDataNode;
    
    typedef struct LinkTableNode
    {
        struct LinkTableNode * pNext;
    }tLinkTableNode;
    
    

    容易知道,menuos将所有的命令用链表来管理:所以这个结构体有这个命令的名字(cmd),这个命令的描述(desc),指向这个命令处理函数的指针(*handler),所有的命令通过tLinkTableNode连接起来,通过这些信息,可以想象到menuos通过命令行接收我们输入的命令,将命令和命令链表的头指针指向的结构体内的cmd字段开始比较,如果不同就和下一个比较,如果相同就执行对应的handler,命令就的到了执行。这与excuteMenu()函数也是一致的。

    2.在menuos上执行C/S的hello/hi程序

    到此为此,我们分析了menuos的文件系统,不过不要忘了,我们的任务是能够运行网络程序,最简单的就是我们之前完成的Hello/hi程序了,那如何在Menuos上添加这个程序呢?
    前面已经看到,menuos在初始化时,用MenuConfig()函数添加了系统现有的这几个服务,那我们也可以通过这个函数加入自己的命令:

    /* add cmd to menu */
    int MenuConfig(char * cmd, char * desc, int (*handler)())
    {
        tDataNode* pNode = NULL;
        if ( head == NULL)
        {
            head = CreateLinkTable();
            pNode = (tDataNode*)malloc(sizeof(tDataNode));
            pNode->cmd = "help";
            pNode->desc = "Menu List";
            pNode->handler = Help;
            AddLinkTableNode(head,(tLinkTableNode *)pNode);
        }
        pNode = (tDataNode*)malloc(sizeof(tDataNode));
        pNode->cmd = cmd;
        pNode->desc = desc;
        pNode->handler = handler; 
        AddLinkTableNode(head,(tLinkTableNode *)pNode);
        return 0; 
    }
    

    于是我们在main函数加入了这两行代码:

    MenuConfig("server","socket server",server);
    MenuConfig("client","socket client 
     send infomation: ",client);
    

    现在的main函数:

    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("server","socket server",server);
        MenuConfig("client","socket client 
     send infomation: ",client);
        ExecuteMenu();
    }
    

    server指向的是hello/hi程序的服务端程序,client指向的是hello/hi中的客户端的程序,要让hello/hi能正常运行,我们需要先运行server然后运行client,但是问题来了,目前menuoos的命令行还不能支持我们同时运行两个程序,这样一来我们就不能同时运行client和server了,如何解决这个问题呢?
    在main函数加一行代码

    if(fork())
    {
            server();
    }
    

    还好我们运行的是linux内核,内核当然支持fork创建一个进行,加上这一句话后,系统启动时会创建一个进程来执行server,也就是server是开机自启的程序了(也可以把这代码加入到client的代码内这样就只有运行client时server才会启动),然后我们再运行client就应该没问题了,于是,我们再进入文件系统的目录make rootfs重新编译一下文件系统,然后再启动menuos。
    图片7
    connet: Network is unreachable,大概是说我们的menuos无法访问网络,仔细一想还真是,我们并没有为menuos初始化网络设备,这个程序也就执行不下去了,因为socket最终是要访问网络设备的,但是menuos并没有提供,也就出错了。如何初始化网络设备呢?

    int BringUpNetInterface()
    {
        printf("Bring up interface:lo
    ");
        struct sockaddr_in sa;
        struct ifreq ifreqlo;
        int fd;
        sa.sin_family = AF_INET;
        sa.sin_addr.s_addr = inet_addr("127.0.0.1");
        fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP);
        strncpy(ifreqlo.ifr_name, "lo",sizeof("lo"));
        memcpy((char *) &ifreqlo.ifr_addr, (char *) &sa, sizeof(struct sockaddr));
        ioctl(fd, SIOCSIFADDR, &ifreqlo);
        ioctl(fd, SIOCGIFFLAGS, &ifreqlo);
        ifreqlo.ifr_flags |= IFF_UP|IFF_LOOPBACK|IFF_RUNNING;
        ioctl(fd, SIOCSIFFLAGS, &ifreqlo);
        close(fd);
        
        printf("Bring up interface:eth0
    ");
        sa.sin_family = AF_INET;
        sa.sin_addr.s_addr = inet_addr("192.168.40.254");
        fd = socket(PF_INET, SOCK_DGRAM, IPPROTO_IP);
        strncpy(ifreqlo.ifr_name, "eth0",sizeof("eth0"));
        memcpy((char *) &ifreqlo.ifr_addr, (char *) &sa, sizeof(struct sockaddr));
        ioctl(fd, SIOCSIFADDR, &ifreqlo);
        ioctl(fd, SIOCGIFFLAGS, &ifreqlo);
        ifreqlo.ifr_flags |= IFF_UP|IFF_RUNNING;
        ioctl(fd, SIOCSIFFLAGS, &ifreqlo);
        close(fd);
    
        printf("List all interfaces:
    ");
        struct ifreq *ifr, *ifend;
        struct ifreq ifreq;
        struct ifconf ifc;
        struct ifreq ifs[MAX_IFS];
        int SockFD;
     
     
        SockFD = socket(PF_INET, SOCK_DGRAM, 0);
     
     
        ifc.ifc_len = sizeof(ifs);
        ifc.ifc_req = ifs;
        if (ioctl(SockFD, SIOCGIFCONF, &ifc) < 0)
        {
            printf("ioctl(SIOCGIFCONF): %m
    ");
            return 0;
        }
     
        ifend = ifs + (ifc.ifc_len / sizeof(struct ifreq));
        for (ifr = ifc.ifc_req; ifr < ifend; ifr++)
        {
            printf("interface:%s
    ", ifr->ifr_name);
    #if 0
            if (strcmp(ifr->ifr_name, "lo") == 0)
            {
                strncpy(ifreq.ifr_name, ifr->ifr_name,sizeof(ifreq.ifr_name));
                ifreq.ifr_flags == IFF_UP;
                if (ioctl (SockFD, SIOCSIFFLAGS, &ifreq) < 0)
                {
                  printf("SIOCSIFFLAGS(%s): IFF_UP %m
    ", ifreq.ifr_name);
                  return 0;
                }			
    	    }
    #endif
    	    if (ifr->ifr_addr.sa_family == AF_INET)
            {
                strncpy(ifreq.ifr_name, ifr->ifr_name,sizeof(ifreq.ifr_name));
                if (ioctl (SockFD, SIOCGIFHWADDR, &ifreq) < 0)
                {
                  printf("SIOCGIFHWADDR(%s): %m
    ", ifreq.ifr_name);
                  return 0;
                }
     
                printf("Ip Address %s
    ", inet_ntoa( ( (struct sockaddr_in *)  &ifr->ifr_addr)->sin_addr)); 
                printf("Device %s -> Ethernet %02x:%02x:%02x:%02x:%02x:%02x
    ", ifreq.ifr_name,
                    (int) ((unsigned char *) &ifreq.ifr_hwaddr.sa_data)[0],
                    (int) ((unsigned char *) &ifreq.ifr_hwaddr.sa_data)[1],
                    (int) ((unsigned char *) &ifreq.ifr_hwaddr.sa_data)[2],
                    (int) ((unsigned char *) &ifreq.ifr_hwaddr.sa_data)[3],
                    (int) ((unsigned char *) &ifreq.ifr_hwaddr.sa_data)[4],
                    (int) ((unsigned char *) &ifreq.ifr_hwaddr.sa_data)[5]);
            }
        }
     
        return 0;
    }
    

    这个操作就是初始化网络的程序,只要我们再menuos执行了他,就能够访问本地回环网络127.0.0.1,使用的方法也分为两种,1.将这个程序添加为一个命令,需要时执行就行。2.直接在main函数加上去,启动menuos时就会自动执行了,我选择的是后者:

    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("server","socket server",server);
        MenuConfig("client","socket client 
     send infomation: ",client);
        //initialize network device
        BringUpNetInterface();
        ExecuteMenu();
    }
    

    好了,再次编译文件系统(建议先执行make clean后再编译),再启动qemu,验证我们的想法是否正确
    图片8
    总算成功了,到了这一步,我们的环境才算配置成功,总结一下步骤:
    1.编译内核
    2.编译文件系统
    3.添加网络程序命令
    4.添加网络设备初始化程序

    3.调试内核

    调试内核使用的工具为gdb,qemu已经集成了gdb server功能,这使得我们可以用qemu来实现调试内核,调试的方法也很简单——将编译器带的gdb与gdb server连接,当连接建立,我们就可以使用编译器的gdb来调试内核,注意,这里还有一个前提,如果要调试内核,肯定需要内核带有调试信息才行,调试信息就相当于告诉编译器各代码执行的逻辑,代码的位置,gdb才能跟踪并打断点,所以我们需要修改一下编译内核的配置文件,让其带上调试信息编译:
    切换到内核的文件目录(我这里是Linux-5.2.7),执行 make menuconfig
    图片10
    勾选位于Kernel hacking—>Compile-time checks and compiler options ---> [*] Compile the kernel with debug info选项
    再执行make -j3重新编译,这次编译的时间会比未勾选这个选项久很多。
    编译完成后我们就可以开始调试了:
    qemu-system-x86_64 -kernel linux-5.0.1/arch/x86_64/boot/bzImage -initrd rootfs.img -append "nokaslr" -s -S
    其中:
    -S freeze CPU at startup (use ’c’ to start execution)
    -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
    -nokaslr KASLR是kernel address space layout randomization的缩写
    可以看到,qemu的界面被打开了,但是,并没有运行,qemu像死机了一样,这是由于-S的效果,为了能够调试内核,我们还需要
    1.打开另一个终端(shell),运行gdb,运行的方式就是直接输入dgb并回车:
    图11
    2.首先加载镜像的符号表,也就是编译时附带的调试信息
    图12
    file vmlinux其中:vmlinux是未压缩的镜像文件,由编译的时候生成,位于Linux源文件的主目录。
    3.连接qemu的gdb server,输入targt remote: 1234即可,这里使用了tcp协议,1234是端口号,使用1234的原因是在启动qemu时的-s选项,当然,如果之前已经修改过端口了这里也要将端口号改为你之前启动qemu选择的端口号。
    图13
    到此为止,所有调试的准备已经结束,可以开始调试了,关于gdb的命令,可以参考:https://www.cnblogs.com/zhoug2020/p/7283169.html,本文只用了几个常见的命令:
    1.设置断点:break start_kernel,这句命令会在start_kernel建立一个断点,程序执行到这个函数就会停止。
    图片14
    图片16

    2.运行到start_kernel,执行c或者continue,执行到start_kernel
    

    图片17
    图片18
    通过list指令,能看到当前执行的位置:
    图片19
    通过step指令,可以跳入正在执行的指令:
    图片20
    再次按c,内核继续启动,直到打出Menuos的logo,
    不过可惜的是,并没有找到如何调试我们网络程序的方法,

  • 相关阅读:
    使用jvisualvm和飞行记录器分析Java程序cpu占用率过高
    Callable、Future和FutureTask
    CountDownLatch(闭锁)
    ArrayBlockingQueue和LinkedBlockingQueue分析
    并发容器之CopyOnWriteArrayList(转载)
    svn 文件夹 无法提交
    rsync 不能同不子级目录的问题
    nginx 匹配.zip .apk 结尾的文件 直接下载
    Android文件Apk下载变ZIP压缩包解决方案
    nginx: [warn] conflicting server name "locahost" on 0.0.0.0:80, ignored
  • 原文地址:https://www.cnblogs.com/myguaiguai/p/12021967.html
Copyright © 2011-2022 走看看