跟踪分析Linux内核的启动过程

这次作业不需要编译,于是使用实验楼的环境。

在shell中使用下面的命令启动qemu

qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

qemu 启动内核_运维

可以看到成功的进入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继续运行

qemu 启动内核_初始化_02

可以看到IP指针目前指向的是x86的reset vector,还没有执行任何指令。

然后在内核的入口start_kernel函数设置断点,并且让qemu开始运行。

(gdb)break start_kernel
(gdb)c

qemu 启动内核_初始化_03

当断点触发的时候,可以看到BIOS已经运行完成,并且将控制交给了Linux。内核的解压缩也已经完成,开始启动内核。

为了便于浏览大量内核源码,推荐LXR项目,全称是Linux Cross Reference,提供了web界面浏览源码,并且可以直接点击看到的任何符号进行查询。我用了free-electrons.com的LXR,这里提供了各个版本的内核源码在线浏览,我直接选择了作业中使用的3.18.6版本

首先找到init/main.c文件里面的start_kernel函数,此时qemu正停在入口处。

下面只能挑看的懂的说了_(:з」∠)_

qemu 启动内核_qemu 启动内核_04

510行在栈底防止了一个MagicNumber,这样可以检查到堆栈溢出。

511行为多核处理器的各个核设置ID

517行初始化了防止栈溢出攻击的安全机制——金丝雀。金丝雀本质是一个随机的MagicNumber,可以从这里了解更多。

521行关闭了中断,确保后面的步骤不会被中断打断。

 

接下来进行了CPU和页表的初始化

qemu 启动内核_运维_05

 

 

解析了内核的启动参数

qemu 启动内核_shell_06

 

接下来是各种关键模块的初始化过程。。。。屏幕上输出了很多信息。。。。

qemu 启动内核_运维_07

 

最后在start_kernel函数的末尾,调用了rest_init函数,继续初始化其余部分。

qemu 启动内核_操作系统_08

rest_init首先启动了调度器,然后创建了1号进程,最后进入idle进程。

 

而1号进程的入口是kernel_init函数,于是继续跟踪该函数

qemu 启动内核_运维_09

首先调用了kernel_init_freeable函数,然后简单看一下kernel_init_freeable函数

qemu 启动内核_初始化_10

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,就会尝试其他默认的程序。

qemu 启动内核_qemu 启动内核_11

 

至此,Linux就启动了init进程。经过若干次调度,MenuOS的main函数开始运行。

 

总结

Linux的启动过程先是初始化各种重要的模块,然后启动两个重要进程init和idle。其中idle是内核启动完成后主动进入的;而init进程是内核fork出来的,最终运行了initrd中的程序。