linux内核启动过程分析
gdb的基本调试命令
- r(run) : 开始运行程序;
- c(continue) : 继续运行一直到断点停止
- b(break) : 设置程序断点;
- p(print) : 打印出变量值;如 p var,会把var变量的值输出
- s(step) : 单步跟踪,会进入函数内部
- n(next) : 单步跟踪,不进入函数
- finish : 跳出函数调试,并打印返回时的信息
- u(until) : 跳出循环体
- q(quit) : 退出gdb
- l(list) : 显示当前行后面的源程序
- bt (backtrace) : 查看堆栈信息
- info : 查看各类gdb信息以及环境信息,比如:info break 可以查看断点信息
- clear : 清除全部已定义的断点
- delete : 删除指点的断点号,后面接断点号
- gdb -tui main或者在启动gdb后,输入命令focus或layout: 能够在运行时间的同时显示代码
实验环境搭建
下载内核源代码编译内核
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 # 一般要编译很长时间
制作根文件系统
cd ~/LinuxKernel/
mkdir rootfs
git clone https://github.com/mengning/menu.git
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
重新配置编译Linux使之携带调试信息
- 在原来配置的基础上,make menuconfig选中如下选项重新配置Linux,使之携带调试信息
- kernel hacking—>
[*] compile the kernel with debug info - make重新编译
使用gdb跟踪调试内核
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选项
使用gdb调试
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之前,也可以在之后
运行启动
调试代码并分析
总的流程图:
首先在start_kernel下断点,运行后,在断点停下:
set_task_stack_end_magic
start_kernel是内核启动阶段的入口,通过单步调试,可以发现它是linux内核执行的第一个init,我们单步进入看看它做了哪些操作:
其中end_of_stack在include/linux/sched.h中,它的意思是获取栈边界地址。然后把栈底地址设置为STACK_END_MAGIC,这个作为栈溢出的标记。STACK_END_MAGIC就是设置在thread_info结构的上面。比如如果你写了一个无限循环,导致栈使用不断增长了,那么,一旦把这个标记未修改了,就导致了栈溢出的错误
local_irq_disable函数
上面两条指令修改了中断寄存器中的IF标志位,sti是中断标志置1指令,使IF=1,cli是中断标志置0指令,使IF=0。所以这里native_irq_disable是关中断,屏蔽中断,接下来的操作就不允许中断对其产生影响。并设置标志位
early_boot_irqs_disabled = true;
page_address_init
初始化高端内存的映射表函数,高端内存是相对于低端内存而存在的,那么先要理解一下低端内存了。在32位的系统里,最多能访问的总内存是4G,其中3G空间给应用程序,而内核只占用1G的空间。因此,内核能映射的内存空间,只有1G大小,但实际上比这个还要小一些,大概是896M,另外128M空间是用来映射高端内存使用的。因此0到896M的内存空间,就叫做低端内存,而高于896M的内存,就叫高端内存了。如果系统是64位系统,当然就没未必要有高端内存存在了,因为64位有足够多的地址空间给内核使用,访问的内存可以达到10G都没有问题。在32位系统里,内核为了访问超过1G的物理内存空间,需要使用高端内存映射表。比如当内核需要读取1G的缓存数据时,就需要分配高端内存来使用,这样才可以管理起来。使用高端内存之后,32位的系统也可以访问达到64G内存。
linux_banner变量保存着linux内核的版本号:
trap_init函数
该函数作用是构建中断描述符表
set_intr_gate(X86_TRAP_DE, divide_error); //除零错误
set_intr_gate_ist(X86_TRAP_NMI, &nmi, NMI_STACK);//不可屏蔽中断
/* int4 can be called from all */
set_system_intr_gate(X86_TRAP_OF, &overflow);//溢出
set_intr_gate(X86_TRAP_BR, bounds);//边界检查错误
set_intr_gate(X86_TRAP_UD, invalid_op);//无效指令 set_intr_gate(X86_TRAP_NM, device_not_available);//无效设备
……
我们可以参照linux-0.11版本的内核来看这段代码:
Linux-0.11
#set_trap_gate(0,÷_error)//除零错误
……
#define set_trap_gate(n,addr) \
_set_gate(&idt[n],15,0,addr)//0表示特权级
sched_init函数
对系统的调度机制进行初始化。先是对每个可用CPU上的runqueque进行初始化
然后初始化0号进程
用__sched_fork产生0号进程,并把0号进程的状态设置为TASK_RUNNING,设为系统的idle进程,即系统空闲时占据CPU进程
console_init函数
在窗口输出信息,之前的内存分配信息也打印出来了。
rest_init
这里具体函数分析见上面的流程图,这里主要是fork了一个新进程,并发生进程调度和切换。