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

咏月东南 2019-12-11

构建调试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拷贝出来。
构建调试Linux内核网络代码的环境MenuOS系统
可以看到,qemu成功启动,但是内核并没有运行成功,报kernel panic警告,当然啦,因为我们还没有制作文件系统,而内核执行到一定步骤需要和文件系统交互的,现在没有文件系统,内核也就无法继续执行下去了。

文件系统制作

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

`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有什么功能:
构建调试Linux内核网络代码的环境MenuOS系统
暂时只有5个功能,后面也有这个命令的解释,到此,环境搭建成功#稳!!!,虽然成功了,但是先不急,我们看看看看这个文件系统有什么
构建调试Linux内核网络代码的环境MenuOS系统
暂时不知道从哪开始看,那首先看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 \n 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) = '\0';
        }
        tDataNode *p = (tDataNode*)SearchLinkTableNode(head,SearchConditon,(void*)argv[0]);
        if( p == NULL)
        {
            continue;
        }
        printf("%s - %s\n", 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 \n 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 \n 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。
构建调试Linux内核网络代码的环境MenuOS系统
connet: Network is unreachable,大概是说我们的menuos无法访问网络,仔细一想还真是,我们并没有为menuos初始化网络设备,这个程序也就执行不下去了,因为socket最终是要访问网络设备的,但是menuos并没有提供,也就出错了。如何初始化网络设备呢?

int BringUpNetInterface()
{
    printf("Bring up interface:lo\n");
    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\n");
    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:\n");
    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\n");
        return 0;
    }
 
    ifend = ifs + (ifc.ifc_len / sizeof(struct ifreq));
    for (ifr = ifc.ifc_req; ifr < ifend; ifr++)
    {
        printf("interface:%s\n", 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\n", 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\n", ifreq.ifr_name);
              return 0;
            }
 
            printf("Ip Address %s\n", inet_ntoa( ( (struct sockaddr_in *)  &ifr->ifr_addr)->sin_addr)); 
            printf("Device %s -> Ethernet %02x:%02x:%02x:%02x:%02x:%02x\n", 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 \n send infomation: ",client);
    //initialize network device
    BringUpNetInterface();
    ExecuteMenu();
}

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

3.调试内核

调试内核使用的工具为gdb,qemu已经集成了gdb server功能,这使得我们可以用qemu来实现调试内核,调试的方法也很简单——将编译器带的gdb与gdb server连接,当连接建立,我们就可以使用编译器的gdb来调试内核,注意,这里还有一个前提,如果要调试内核,肯定需要内核带有调试信息才行,调试信息就相当于告诉编译器各代码执行的逻辑,代码的位置,gdb才能跟踪并打断点,所以我们需要修改一下编译内核的配置文件,让其带上调试信息编译:
切换到内核的文件目录(我这里是Linux-5.2.7),执行 make menuconfig
构建调试Linux内核网络代码的环境MenuOS系统
勾选位于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并回车:
构建调试Linux内核网络代码的环境MenuOS系统
2.首先加载镜像的符号表,也就是编译时附带的调试信息
构建调试Linux内核网络代码的环境MenuOS系统
file vmlinux其中:vmlinux是未压缩的镜像文件,由编译的时候生成,位于Linux源文件的主目录。
3.连接qemu的gdb server,输入targt remote: 1234即可,这里使用了tcp协议,1234是端口号,使用1234的原因是在启动qemu时的-s选项,当然,如果之前已经修改过端口了这里也要将端口号改为你之前启动qemu选择的端口号。
构建调试Linux内核网络代码的环境MenuOS系统
到此为止,所有调试的准备已经结束,可以开始调试了,关于gdb的命令,可以参考:https://www.cnblogs.com/zhoug2020/p/7283169.html,本文只用了几个常见的命令
1.设置断点:break start_kernel,这句命令会在start_kernel建立一个断点,程序执行到这个函数就会停止。
构建调试Linux内核网络代码的环境MenuOS系统
构建调试Linux内核网络代码的环境MenuOS系统

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

构建调试Linux内核网络代码的环境MenuOS系统
构建调试Linux内核网络代码的环境MenuOS系统
通过list指令,能看到当前执行的位置:
构建调试Linux内核网络代码的环境MenuOS系统
通过step指令,可以跳入正在执行的指令:
构建调试Linux内核网络代码的环境MenuOS系统
再次按c,内核继续启动,直到打出Menuos的logo,
不过可惜的是,并没有找到如何调试我们网络程序的方法,

相关推荐