基于linux5.0.1内核的网络代码环境的构建及内核函数的跟踪
本次带着大家完成一个关于linux内核及网络的有趣的实验,即在linux内核的基础上构建一个只有很少命令的Menu OS系统,然后在Menu OS的终端上完成TCP的C/S通信程序,最后通过linux系统下自带的调试器gdb跟踪关于网络内核的函数,看看它们的原理,通过这次实验,相信大家对内核函数调用及网络程序的工作原理能有更深一步的了解!
实验目标
- 通过本地Linux系统完成构建调试Linux内核网络代码的环境MenuOS系统
- 截图证实已经在MenuOS上能够完成TCP客户端和服务器发送和接收hello/hi,也就是MenuOS的网络可以正常工作
- 截图证实通过gdb可以跟踪到内核代码,比如start_kernel、sys_socketcall等内核函数
实验环境
内核代码:Linux 5.0.1;
Ubuntu虚拟机:运行环境,本次使用VM Ware15.5安装的Ubuntu16.04;
qemu:本次使用的模拟器,运行内核代码;
gdb:linux下的断点调试工具;
实验过程
一、构建MenuOS系统
1、下载linux内核到kernel目录并解压:
mkdir kernel wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.0.1.tar mv linux-5.0.1.tar ./kernel cd kernel tar -xvf linux-5.0.1.tar.gz
2、在进行内核配置之前,首先安装一些必要的编译工具:
sudo apt install build-essential flex bison libssl-dev libelf-dev libncurses-dev
3、配置内核并编译:
make x86_64_defconfig //选择默认的x86_64方式配置
make menuconfig
选中compile the kernel with debug info,以产生调试信息供gdb调试:
ok,保存好生成.config信息,然后开始编译内核:
make
编译时间是真的有点长,所以耐心等待啊,最后终于成功了。
4、安装强大的模拟器qemu:
sudo apt-get install qemu
成功后在shell上输入qemu再按tab,可以看到qemu支持的机器架构:
我们待会会用到qemu-system-x86_64的,别着急,一步步来。
5、制作根文件系统:
cd ~/kernel/ mkdir rootfs git clone https://github.com/mengning/menu.git cd menu gcc -pthread -o init linktable.c menu.c test.c -m32 -static cd ../rootfs cp ../menu/init ./ find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
从 https://github.com/mengning/menu.git上克隆menu目录到kernel下,里面有很多测试程序用来制作img根文件系统,如下图:
通过上面的命令,现在已经制作好rootfs.img了,如下图:
6、用qemu启动MenuOS系统:
cd .. qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
出现以下界面:
好了,至此,大功告成,现在已经成功构建调试Linux内核网络代码的环境MenuOS系统,可以再往下走了。
在Menu OS上敲入help可以看到提供的命令:
可以从上图看到只有5个命令,现在打开test.c看看:
1 #include <stdio.h> 2 #include <stdlib.h> 3 #include <time.h> 4 #include "menu.h" 5 6 #define FONTSIZE 10 7 int PrintMenuOS() 8 { 9 int i, j; 10 char data_M[FONTSIZE][FONTSIZE] = 11 { 12 " ", 13 " * * ", 14 " *** *** ", 15 " * * * * ", 16 " * * * * ", 17 " * ** * ", 18 " * * ", 19 " * * ", 20 " * * ", 21 " " 22 }; 23 char data_e[FONTSIZE][FONTSIZE] = 24 { 25 " ", 26 " ", 27 " ** ", 28 " * * ", 29 " * * ", 30 " ****** ", 31 " * ", 32 " * ", 33 " *** ", 34 " " 35 }; 36 char data_n[FONTSIZE][FONTSIZE] = 37 { 38 " ", 39 " ", 40 " ** ", 41 " * * ", 42 " * * ", 43 " * * ", 44 " * * ", 45 " * * ", 46 " * * ", 47 " " 48 }; 49 char data_u[FONTSIZE][FONTSIZE] = 50 { 51 " ", 52 " ", 53 " * * ", 54 " * * ", 55 " * * ", 56 " * * ", 57 " * * ", 58 " * ** ", 59 " ** * ", 60 " " 61 }; 62 char data_O[FONTSIZE][FONTSIZE] = 63 { 64 " ", 65 " **** ", 66 " * * ", 67 " * * ", 68 " * * ", 69 " * * ", 70 " * * ", 71 " * * ", 72 " **** ", 73 " " 74 }; 75 char data_S[FONTSIZE][FONTSIZE] = 76 { 77 " ", 78 " **** ", 79 " ** ", 80 " ** ", 81 " *** ", 82 " ** ", 83 " ** ", 84 " ** ", 85 " **** ", 86 " " 87 }; 88 89 for(i=0; i<FONTSIZE; i++) 90 { 91 for(j=0; j<FONTSIZE; j++) 92 { 93 printf("%c", data_M[i][j]); 94 } 95 for(j=0; j<FONTSIZE; j++) 96 { 97 printf("%c", data_e[i][j]); 98 } 99 for(j=0; j<FONTSIZE; j++) 100 { 101 printf("%c", data_n[i][j]); 102 } 103 for(j=0; j<FONTSIZE; j++) 104 { 105 printf("%c", data_u[i][j]); 106 } 107 for(j=0; j<FONTSIZE; j++) 108 { 109 printf("%c", data_O[i][j]); 110 } 111 for(j=0; j<FONTSIZE; j++) 112 { 113 printf("%c", data_S[i][j]); 114 } 115 printf("\n"); 116 } 117 return 0; 118 } 119 120 int Quit(int argc, char *argv[]) 121 { 122 /* add XXX clean ops */ 123 } 124 125 int Time(int argc, char *argv[]) 126 { 127 time_t tt; 128 struct tm *t; 129 tt = time(NULL); 130 t = localtime(&tt); 131 printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); 132 return 0; 133 } 134 135 int TimeAsm(int argc, char *argv[]) 136 { 137 time_t tt; 138 struct tm *t; 139 asm volatile( 140 "mov $0,%%ebx\n\t" 141 "mov $0xd,%%eax\n\t" 142 "int $0x80\n\t" 143 "mov %%eax,%0\n\t" 144 : "=m" (tt) 145 ); 146 t = localtime(&tt); 147 printf("time:%d:%d:%d:%d:%d:%d\n",t->tm_year+1900, t->tm_mon, t->tm_mday, t->tm_hour, t->tm_min, t->tm_sec); 148 return 0; 149 } 150 int main() 151 { 152 PrintMenuOS(); 153 SetPrompt("MenuOS>>"); 154 MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL); 155 MenuConfig("quit","Quit from MenuOS",Quit); 156 MenuConfig("time","Show System Time",Time); 157 MenuConfig("time-asm","Show System Time(asm)",TimeAsm); 158 ExecuteMenu(); 159 }
现在大家应该明白了Menu OS系统的界面以及提供的命令是怎么出现的了,没错主要就是根据test.c来的。
顺便给大家提一点关于qemu常见启动和调试选项的解释:
Linux启动选项
当我们使用这些选项时,我们可以使用一个指定的内核,而没有将它安装在磁盘镜像中。这对于简单的测试各种内核是相当有用的。
- `-kernel bzImage'
- 使用bzImage作为内核映像。
- `-append cmdline'
- 使用cmdline作为内核的命令行。
- `-initrd file'
- 使用file作为初始的ram磁盘。
调试选项
- `-serial dev'
- 重定向虚拟串到主机的设备dev。可用的设备如下:
- vc
- 虚拟终端
- pty
- (Linux)伪TTY(自动分配一个新的TTY)
- null
- 空设备
- /dev/XXX"
- (Linux)使用主机的tty。例如,'/dev/ttyS0'。主机的串口参数通过模拟进行设置。
- /dev/parportN
- (Linux)使用主机的并口N。当前只可以使用SPP的并口特征。
- file:filename
- 将输出写入到文件filename中。没有字符可读。
- stdio
- (Unix)标准输入/输出
- pipe:filename
- (Unix)有名管道filename
在图形模式下的默认设备为vc,而在非图形模式下为stdio.这个选项 可以被多次使用,最多可以模拟4个串口。
- '-parallel dev'
-
重定向虚拟并口到主机的设备dev(与串口相同的设备)。在Linux主 机上,`/dev/parportN'可以被用来使用与相应的并口相连的硬件设 备。这个选项可以使用多次,最多可以模拟3个并口。
- `-monitor dev'
-
重定向临视器到主机的设备dev(与串口相同的设备)。在图形模式 下的默认设备为vc,而在非图形模式下为stdio。
- '-s'
- 等待gdb连接到端口1234.
- `-p port'
- 改变gdb连接端口。
- `-S'
- 在启动时并不启动CPU(我们必须在监视器中输入'c')
'-d'
- 输出日志到/tmp/qemu.log
- `-hdachs c,h,s,[,t]'
- 强制硬盘0的物理参数(1 <= c <= 16383, 1 <= h <= 16, 1 <= s <=63),并且可以选择强制BIOS的转换模式(t=none, lba or auto).通 常QEMU可以检测这些参数.这个选项对于老的MS-DOS磁盘映像是相当 有用的.
- `-std-vga'
- 模拟一个Bochs VBE扩展的标准VGA显卡(默认情况下为Cirrus Logic GD5446 PCI VGA)
- `-loadvm file'
- 从一个保存状态启动.
- 当然还有很多其他的选项,这里就不写那么多,想知道更多可以直接去查看qemu的所有选项。
- 上面我们构建的Menu OS系统还无法使用gdb调试,使用以下命令重新跑一下就可以进行调试了:
-
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S
默认的TCP端口是1234,想换的话上面有选项说明。
二、完成TCP客户端和服务器通信
1、用test_reply.c程序制作系统rootfs.img:
test_reply.c程序较长,可以在 https://github.com/mengning/linuxnet/tree/master/lab2上看到,将test_reply.c部署到Menu OS系统中:
gcc -pthread -o init linktable.c menu.c test_reply.c -m32 -static cd ../rootfs cp ../menu/init ./ find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
跑起来吧:
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
如下图:
完美,部署成功,可以看到Menu OS系统又多了两个命令replyhi和hello,果然和test_reply.c一致。
2、测试Menu OS系统可以完成hello/hi的网络通信过程:
在Menu OS系统上先开启服务端即输入replyhi,再打开客户端即输入hello,如下图:
哎呀失败了,这是为什么呢,别慌,这是因为启动的 TCP 服务并不能正常对外提供服务,因为 MenuOS 没有初始化网络设备(包括本地回环 loopback 设备),因此它无法接收到任何网络请求。现在我们将激活 Linux 网络设备,并将 MenuOS 系统的网络设备用简便的方式配置好,然后再次部署我们的网络通信程序,程序已经写好,只需要从 https://github.com/mengning/linuxnet/tree/master/lab3上拷贝即可,主要有两个文件即main.c和syswrapper.h,由于代码过程就不再展示了,现在将这两个文件放在 ~/kernel/menu下,然后重新制作rootfs.img:
在menu目录下依次输入以下命令:
gcc -pthread -o init linktable.c menu.c main.c -m32 -static cd ../rootfs cp ../menu/init ./ find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img cd .. qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img
新的Menu OS系统界面及提供的命令:
可以看到使用main.c制作的Menu OS系统共有5条命令,现在试试TCP通信能否成功,依次输入replyhi和hello:
哈哈,Menu OS系统已成功实现基于TCP的客户端、服务端通信,我们可以愉快地进入下一步了,即关于系统内核函数的调试。
三、使用gdb跟踪调试内核函数
使用gdb跟踪start_kernel内核函数:
先重新跑起来可以调试的Menu OS系统:
qemu-system-x86_64 -kernel linux-5.0.1/arch/x86/boot/bzImage -initrd rootfs.img -s -S -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选项
# 不加-append nokaslr可能断点挂不住
现在已经启动并冻结了Menu OS系统:
重新打开一个终端:
cd kernel gdb (gdb)file linux-5.0.1/vmlinux # 在gdb界面中targe remote之前加载符号表 (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行 (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
如下图:
关于linux内核目录的说明:
1、arch目录包括了所有和体系结构相关的核心代码。它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel CPU及与之相兼容体系结构的子目录。PC机一般都基于此目录。
2、COPYING目录下是GPL版权申明。对具有GPL版权的源代码改动而形成的程序,或使用GPL工具产生的程序,具有使用GPL发表的义务,如公开源代码。
3、CREDITS目录下是光荣榜。对Linux做出过很大贡献的一些人的信息。
4、documentation目录下是一些文档,没有内核代码,可惜都是English的,是对每个目录作用的具体说明。
5、drivers目录中是系统中所有的设备驱动程序。它又进一步划分成几类设备驱动,每一种有对应的子目录,如声卡的驱动对应于drivers/sound; block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化,因为安装nfs文件系统的时候需要网络其他: 如, Lib放置核心的库代码; Net,核心与网络相关的代码; Ipc,这个目录包含核心的进程间通讯的代码; Fs,所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext2。
6、fs目录存放Linux支持的文件系统代码和各种类型的文件操作代码。每一个子目录支持一个文件系统,如ext3文件系统对应的就是ext3子目录。
7、include目录包括编译核心所需要的大部分头文件,例如与平台无关的头文件在include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录。
8、init目录包含核心的初始化代码(不是系统的引导代码),有main.c和Version.c两个文件。这是研究核心如何工作的好起点。
9、ipc目录包含了核心进程间的通信代码。
10、Kernel内核管理的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同时与处理器结构相关代码都放在archlib/目录下。
11、MAINTAINERS目录存放了维护人员列表,对当前版本的内核各部分都有谁负责。
12、Makefile目录第一个Makefile文件。用来组织内核的各模块,记录了个模块间的相互这间的联系和依托关系,编译时使用;仔细阅读各子目录下的Makefile文件对弄清各个文件这间的联系和依托关系很有帮助。
13、mm目录包含了所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下,例如arch/i386/mm/Fault.c 。
14、modules目录存放了已建好的、可动态加载的模块文件目录,是个空目录,用于存放编译时产生的模块目标文件。
15、net目录里是核心的网络部分代码,其每个子目录对应于网络的一个方面。
16、ReadMe目录里是核心及其编译配置方法简单介绍
17、REPORTING-BUGS目录里是有关报告Bug 的一些内容
18、Rules.make目录里是各种Makefilemake所使用的一些共同规则
19、scripts目录包含用于配置核心的脚本文件等。
一般在每个目录下都有一个.depend文件和一个Makefile文件。这两个文件都是编译时使用的辅助文件。仔细阅读这两个文件对弄清各个文件之间的联系和依托关系很有帮助。另外有的目录下还有Readme文件,它是对该目录下文件的一些说明,同样有利于对内核源码的理解。
好了,go on!在gdb中按c键使系统开始执行到断点处暂停,如下图:
使用list命令可以查看可以看到start_kernel函数的上下文,如下图:
start_kernel()是内核的汇编代码和c代码的交接处,在此之前,全是由汇编代码完成各种环境的初始化工作,包括将内核代码载入内存,设置C运行环境等等。
当运行到start_kernel()的时候,我们可以大致分析如下:
1.手工创建0号进程init_task(),最终变成idle进程
set_task_stack_end_magic(&init_task);
init_idle()函数会把init_task加入到cpu的运行队列中去,在没有其他进程加入cpu队列的时候,init_task会一直运行,当其他进程加入进来的时候,init_task就会被设置成idle,并使用调度函数将切换到新加入进来的进程上。
2.初始化各个模块
模块如下:内存管理模块 中断 调度模块等等
3.运行到rest_init(),初始化进程
其中rest_init()是内核初始化的最后一步,它也是所有进程的祖先。
注意这段代码:
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); //初始化第一个用户态进程,也就是1号进程
OK,接下来我们继续分析
static void noinline rest_init(void) 417 __releases(kernel_lock) 418 { 419 kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND); 420 numa_default_policy(); 421 unlock_kernel();423 /* 424 * The boot idle thread must execute schedule() 425 * at least one to get things moving: 426 */ 427 preempt_enable_no_resched(); 428 schedule(); 429 preempt_disable(); 430 431 /* Call into cpu_idle with preempt disabled */ 432 cpu_idle(); 433 }
在这段代码中,我们只需要分析下面两句就行了,其他的可以暂时忽略。
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
cpu_idle(); //cpu队列进程的切换,将0号进程设置idle
kernel_thread中传入的函数init需要进行分析一下,我们截取部分代码如下:
run_init_process("/sbin/init"); run_init_process()实际上是通过嵌入汇编构建一个类似用户态代码
run_init_process("/etc/init"); 一样的 sys_execve()调用,其参数就是要执行的可执行文件名,也就
run_init_process("/bin/init"); 是这里的 init process 在磁盘上的文件。
run_init_process("/bin/sh");
在run_init_procrss()处断点调试,run_init_process 就是通过 execve()来运行 init 程序。
到这里idle_task()的任务完成; 将会被调度函数设置为空闲的进程。
——————————————————————————————————————————————————————————————————————
本次实验到这里就要和大家说再见了,希望大家能亲自动手试试看,绝对能收获不少关于linux内核和网络程序设计的新知识啦,就这样了,回见!!!