这次作业不需要编译,于是使用实验楼的环境。
在shell中使用下面的命令启动qemu
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
可以看到成功的进入MenuOS。
因为没有学过《软件工程C编码实践篇》,所以首先简略看一下MenuOS的代码,main函数在test.c里面
1 int main()
2 {
3 PrintMenuOS(); //用*号显示MenoOS的Logo
4 SetPrompt("MenuOS>>"); //设置命令行提示符
5 MenuConfig("version","MenuOS V1.0(Based on Linux 3.18.6)",NULL); //注册命令
6 MenuConfig("quit","Quit from MenuOS",Quit); //注册命令
7 MenuConfig("time","Show System Time",Time); //注册命令
8 MenuConfig("time-asm","Show System Time(asm)",TimeAsm); //注册命令
9 ExecuteMenu(); //显示提示符,并接受命令输入
10 }
初步了解这些就够了。从qemu的显示画面来看,已经将main函数运行完成。
main函数是如何开始运行的?从Makefile来看,MenuOS编译出了init文件,然后init和另一个helloworld小程序通过cpio和gzip打包压缩成了rootfs.img。而这个rootfs.img正是qemu中通过-initrd参数指定的。
那么initrd是什么东西?initrd是Linux初始RAM磁盘,Linux刚启动的时候,实际的文件系统还没有初始化,而初始化实际文件系统的程序需要一个运行环境,就是由initrd提供的。initrd中包含了各种可执行程序和驱动程序,它们可以用来挂载实际的根文件系统。Linux内核自动将initrd装载在内存中,运行文件系统初始化程序,然后再将这个RAM磁盘卸载,并释放内存。在很多嵌入式Linux系统中,因为无需复杂的文件系统,initrd甚至本身就作为最终的根文件系统。
下面就用gdb跟踪内核的启动流程,看看内核是如何一步一步的运行到MenuOS的main函数。启动qemu的时候添加两个参数
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S
其中-s表示启动远程调试,否则gdb无法调式虚拟机中的程序。而-S表示启动时立刻冻结虚拟机,否则内核很快运行结束了,没有跟踪的机会。
等到qemu启动以后,再打开一个shell启动gdb,载入符号表,并连接到qemu的调试端口。
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
可以看到IP指针目前指向的是x86的reset vector,还没有执行任何指令。
然后在内核的入口start_kernel函数设置断点,并且让qemu开始运行。
(gdb)break start_kernel
(gdb)c
当断点触发的时候,可以看到BIOS已经运行完成,并且将控制交给了Linux。内核的解压缩也已经完成,开始启动内核。
为了便于浏览大量内核源码,推荐LXR项目,全称是Linux Cross Reference,提供了web界面浏览源码,并且可以直接点击看到的任何符号进行查询。我用了free-electrons.com的LXR,这里提供了各个版本的内核源码在线浏览,我直接选择了作业中使用的3.18.6版本
首先找到init/main.c文件里面的start_kernel函数,此时qemu正停在入口处。
下面只能挑看的懂的说了_(:з」∠)_
510行在栈底防止了一个MagicNumber,这样可以检查到堆栈溢出。
511行为多核处理器的各个核设置ID
517行初始化了防止栈溢出攻击的安全机制——金丝雀。金丝雀本质是一个随机的MagicNumber,可以从这里了解更多。
521行关闭了中断,确保后面的步骤不会被中断打断。
接下来进行了CPU和页表的初始化
解析了内核的启动参数
接下来是各种关键模块的初始化过程。。。。屏幕上输出了很多信息。。。。
最后在start_kernel函数的末尾,调用了rest_init函数,继续初始化其余部分。
rest_init首先启动了调度器,然后创建了1号进程,最后进入idle进程。
而1号进程的入口是kernel_init函数,于是继续跟踪该函数
首先调用了kernel_init_freeable函数,然后简单看一下kernel_init_freeable函数
1010行和1011行,复制了两个文件号,1号被当作标准输出流,2号被当作标准错误流。
1018行指定了默认的ramdisk_execute_command——init,这也就是MenoOS的可执行文件名。
1022行的prepare_namespace函数就不展开了,这个函数载入了ramdisk作为根磁盘系统,也就是启动参数中指定的rootfs.img。
回到kernel_init函数,就会运行ramdisk_execute_command,即init程序。如果没有ramdisk_execute_command,就会尝试其他默认的程序。
至此,Linux就启动了init进程。经过若干次调度,MenuOS的main函数开始运行。
总结
Linux的启动过程先是初始化各种重要的模块,然后启动两个重要进程init和idle。其中idle是内核启动完成后主动进入的;而init进程是内核fork出来的,最终运行了initrd中的程序。