linux内核启动过程跟踪
慕课18原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”
一、使用自己的Linux系统环境搭建MenuOS的过程
# 下载内核源代码编译内核
cd ~/LinuxKernel/
wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
xz -d linux-3.18.6.tar.xz
tar -xvf linux-3.18.6.tar
cd linux-3.18.6
make i386_defconfig
make # 一般要编译很长时间,少则20分钟多则数小时
# 制作根文件系统
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git # 如果被墙,可以使用附件menu.zip
cd menu
gcc -o init linktable.c menu.c test.c -m32 -static –lpthread
cd ../rootfs
cp ../menu/init ./
find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
# 启动MenuOS系统
cd ~/LinuxKernel/
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
// -initrd file Use file as initial ram disk.
二、重新配置编译Linux使之携带调试信息
在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
kernel hacking—>[*] compile the kernel with debug info
make重新编译(时间较长)
三、使用gdb跟踪调试内核
1、qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
// # -S freeze CPU at startup (use ’c’ to start execution)
//# -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项
2、另开一个shell窗口
gdb
(gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后
四、过程图示:
1、可以看到QEMU 已经运行而且被“冻结”。(有几个文件没有,不影响后续操作)
2、另外打开一个窗口,进入linuxkernel目录,输入gdb 回车
3、输入:(gdb)file linux-3.18.6/vmlinux
(gdb)target remote:1234
(gdb)break start_kernel
4、输入c 回车,可以看到内核继续启动,最后停在了start_kernel处:
更多gdb指令:
显示和查找程序源代码
(1)list :显示10行代码,但是我为什么没有显示成功呢?
(2)list 5,10:显示源文件第五行到第十行的代码
(3)list t4.c:5,10:显示源文件中第五行到第十行的代码,在跳是含有多个源文件的次序时使用;
(4)list get_sum:显示get_sum函数周围的代码//什么叫周围的代码呢?
(5)list t4.c :get_sum:显示源文件t4.c中第五行到第十行的代码,在跳是含有多个源文件的次序时使用;
(6)如果在调试中运行linux命令,则可以在gdb的提示符下输入shell命令. (gdb)shell ls
(7)search forward用来从当前行向前查找第一个匹配的字符串;
search get_sum forward get_sum
(8)reverse_search 用来从当前行想前查找第一个匹配的字符串: Example: reverse_search main
设置和管理断点:
(1)以行号设置断点:(gdb)break 7
(2)以函数名设置断点:(gdb)break get_sum
(3)以条件表达式设置断点:方法一:break 行号或者函数名 if 条件. Example: (gdb)break 7 if i==99
方法二:watch 条件表达式,下面是具体的举例:
方法三:awatch;用来给表达式设置断点,在表达式的值发生改变或者表达式的值杯读取的时候,程序暂时停止;
(4).查看当前设置的断点:info breakpoints
(5)使用“disable 断点编号”命令可以是某个断点失效,程序运行到该段点时不会停下来而是继续运行。
(6)使用“enable 断点编号”命令可以是某个断点恢复有效。
彻底的删除某个断点,可以使用clear或者delete命令。
(1)clear:删除程序中所有的断点;
(2)clear 行号:删除此行中的断点
(3)clear 函数名:删除该函数的断点
(4)delete 断点编号:删除指定编号的断点。如果一次要删除多个断点,各个断点编号以空格隔开。
控制程序的执行:
(1)continue命令:让程序继续运行,直到下一个断点或者运行完为止。格式:continue
(2)kill命令:用于结束当前程序的调试
(3)next和step命令
区别:如果遇到函数,next会把函数调用当作一条语句来执行,再次输入next会执行函数调用后的语句;
而step则会跟踪进入函数,一次一条的执行函数内的代码,直到函数内的代码执行完,在进行函数调用后的语句;
(4)nexti和stepi命令:用来单步执行一条机器指令,注意不是单步执行一条鱼据。单步执行一条语句使用next和step命令。通常一条语句有多条机器指令构成的。
注意的是:gdb的一些命令可以简写,比如list可以用li来代替,continue命令可以用cont来代替。
5、继续输入 list start_kernel 查看start_kernel函数为中心上下10行的代码
然后接着输入list 查看后续的代码(每输入一次list,向后显示10行)
我们继续输入list跟踪start_kernel以后的代码,看看后面的执行过程:
可以看到,在start_kernel之后是一大堆的初始化操作,初始化外设、时钟、寄存器等。后面还有很多操作,这里不作过多的截图展示,
6、后面进入rest_init函数:
四、总结:
在start_kernel中调用了一系列的初始化函数,已完成内核本身的设置:设置与体系结构相关的环境、进程调度器初始化、控制台初始化、系统IRQ初始化、内存初始化等。在Start_kernel函数的最后调用了rest_init()函数,在rest_init中建立了init线程,并在最后调用cpu_idle()函数。
可以这样理解:start_kernel最后clone出一个新的进程,也就是init进程,然后原来的进程就去执行cpu_idle()函数了,也就变成了idle进程,当发生一次进程调度后,init进程被调度运行。
核心进程init()主要进行一些外设初始化的工作包括SMP(Symmetric Multi-Processing 对称多处理)的初始化,以及调用do_basic_setup()完成外设及其驱动程序的加载和初始化,当do_basic_setup()函数返回init() ,init() 又打开了、dev/console设备,重定向输出文件到控制台,最后通过kernel——execve加载执行init程序。
追踪到init后可以修改文件系统,比如把显示的MENUOS修改为MYOS,修改quit指令,让其退出文件系统等:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img
// -initrd file Use file as initial ram disk.
这里将-initrd 后面的参数rootfs.img加载到内存中运行,所以,这里的rootfs.img 可以随便改名字,因为内核代码中不会直接用这个名字而是通过initrd来传递的。再者rootfs.img是由tootfs文件夹打包得来的,而rootfs文件夹中只有一个init可执行文件,而init可执行文件来自于menu文件夹,menu文件夹中执行make指令会发现生成了一个test可执行文件,运行menu文件夹中的test和init后会发现效果是一模一样的,所以显然init是由test通过(cp test init) 拷贝、重命名得来的,所以最终修改文件系统可以归结为修改menu文件夹中的main.c、test.c文件。
在init/initramfs.c文件中: