咏月东南 2019-12-11
linux内核环境指的是我们用虚拟机运行linux系统,在linux上运行我们开发的网络代码,这样做的好处就是方便调试,通过虚拟机,我们可以用gdb调试,观察内核运行到哪里了,尤其是针对网络方面的接口(如socket、bind等),调试使我们清晰的看到程序调用了什么,执行了什么,这对于我们的学习大有脾益,而为了搭建环境,我们需要1.下载并编译Linux内核,2.安装qemu,
注意,为了编译内核,我们需要在系统安装部分的编译工具:
`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实在是太强了,好用到不行,安装也很简单,sudo apt-get install qemu
等安装完成就结束了,我们可以先试试能不能运行:qemu-system-x86_64 -kernel bzImage
由于之前编译的是64位的镜像,所以选择运行x86_64位的qemu,镜像文件可以从linux-5.2.7/arch/x86_64/boot/bzImage拷贝出来。
可以看到,qemu成功启动,但是内核并没有运行成功,报kernel panic警告,当然啦,因为我们还没有制作文件系统,而内核执行到一定步骤需要和文件系统交互的,现在没有文件系统,内核也就无法继续执行下去了。
制作一个文件是比较麻烦的,一般要下载一个Busybox,然后编译、安装,之后添加需要的文件,不过这次实验老师已经制作好了,我们可以直接使用git clone https://github.com/mengning/menu.git
进入这个文件系统的目录,执行make操作,就可以在这个文件夹得到rootfs.img的镜像
有了文件系统,我们再次用虚拟机加载镜像总没问题了吧
`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有什么功能:
暂时只有5个功能,后面也有这个命令的解释,到此,环境搭建成功#稳!!!,虽然成功了,但是先不急,我们看看看看这个文件系统有什么
暂时不知道从哪开始看,那首先看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()函数也是一致的。
到此为此,我们分析了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。
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,验证我们的想法是否正确
总算成功了,到了这一步,我们的环境才算配置成功,总结一下步骤:
1.编译内核
2.编译文件系统
3.添加网络程序命令
4.添加网络设备初始化程序
调试内核使用的工具为gdb,qemu已经集成了gdb server功能,这使得我们可以用qemu来实现调试内核,调试的方法也很简单——将编译器带的gdb与gdb server连接,当连接建立,我们就可以使用编译器的gdb来调试内核,注意,这里还有一个前提,如果要调试内核,肯定需要内核带有调试信息才行,调试信息就相当于告诉编译器各代码执行的逻辑,代码的位置,gdb才能跟踪并打断点,所以我们需要修改一下编译内核的配置文件,让其带上调试信息编译:
切换到内核的文件目录(我这里是Linux-5.2.7),执行 make menuconfig
勾选位于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并回车:
2.首先加载镜像的符号表,也就是编译时附带的调试信息file vmlinux
其中:vmlinux
是未压缩的镜像文件,由编译的时候生成,位于Linux源文件的主目录。
3.连接qemu的gdb server,输入targt remote: 1234
即可,这里使用了tcp协议,1234是端口号,使用1234的原因是在启动qemu时的-s选项,当然,如果之前已经修改过端口了这里也要将端口号改为你之前启动qemu选择的端口号。
到此为止,所有调试的准备已经结束,可以开始调试了,关于gdb的命令,可以参考:https://www.cnblogs.com/zhoug2020/p/7283169.html,本文只用了几个常见的命令:
1.设置断点:break start_kernel
,这句命令会在start_kernel建立一个断点,程序执行到这个函数就会停止。
2.运行到start_kernel,执行c或者continue,执行到start_kernel
通过list指令,能看到当前执行的位置:
通过step指令,可以跳入正在执行的指令:
再次按c,内核继续启动,直到打出Menuos的logo,
不过可惜的是,并没有找到如何调试我们网络程序的方法,