2020-2021-1 20209317《Linux内核原理与分析》第四周作业

《linux内核原理与分析》第三周作业

一、实验相关

实验内容

使用qemu虚拟机运行内核,并从内核入口start_kernel开始单步分析start_kernel函数执行过程
实验楼环境一直崩溃,所以改为使用自己的虚拟机。

下载源码并解压编译

mkdir LinuxKernel
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_defcongig
make

编译时出现书中提到gcc编译器版本问题,问题原因在于现在的linux编译器gcc版本过高,解决方法是将内核中存在的头文件重命名为所需的文件。

制作根文件系统

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

对内核进行跟踪调试

使用sudo apt-get install qemu命令下载安装qemu虚拟机,进行书中配置编译Linux内核操作,执行以下操作

make menuconfig
make

执行该命令时,发现目录中并不存在menuconfig文件,经查阅资料后了解到menuconfig是基于文本选择的配置界面,字符终端下推荐使用,进行该命令后会出现如下图

该窗口是为了配置内核编译的选项,同时出于好奇,查阅了相关资料,该窗口是由cripts/kconfig目录下面的配置文件生成,该目录下比较重要的文件有mconf.c和lxdialog目录,使用cat命令查看后其中存放的为编译时的配置文件。
按照书中的操作选择编译时携带调试信息,为了后面使用gdb进行调试,之后进行make操作
后面进行调试时发现由于linux版本问题和qemu版本问题,进行该操作之后并不能添加调试信息,解决方法是在制作根文件系统和镜像的时候,使用

gcc -pthread -o init linktable.c menu.c test.c -m32 -static -g

提前将调试信息加入到镜像中,后面惊醒gdb单步查看时就能够查看到调试的符号表信息了。

跟踪调试Linux内核启动过程

首先在配置好的qemu虚拟机中启动内核,使用以下命令

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

其中-S是为了将CPU在初始化之前将其冻结起来,-s是在1234端口上创建了一个gdb-server,方便通过另一个窗口使用gdb把带有符号表的内核镜像加载,然后连接gdbserver,并进行调试。效果图如下:

打开另一个窗口使用gdb,使用如下命令

file linux-3.18.6/vmlinux
target remote:1234
break start_kernel

加载内核并通过端口1234进行调试,在内核开始的start_kernel函数入口处设置断点,如下图:

之后开始单步执行,如下图:

分析:该图为start_kernel函数执行的第一行,改行初始化了一个数据结构init_task,该数据结构类型是PCB,即该行代码的作用为初始化第一个PCB,该PCB指向了内核的0号进程即init_task(),该进程初始化了系统的所有进程和线程,在内核初始化完成之后就会演变为idle进程,在后面内核调度进程中发挥作用。
继续执行,如下图:

分析:接下来按行分析各个函数的作用,主要还是通过搜索各种资源,了解每个函数的作用。
smp_setup_processor_id(),这个函数主要作用是获取当前正在执行初始化的处理器ID,
debug_objects_early_init(),这个函数主要作用是对调试对象进行早期的初始化,其实就是HASH锁和静态对象池进行初始化。
boot_init_stack_canary(),初始化stack_canary,用来防止栈溢出攻击保护的堆栈。
cgroup_init_early(),这个函数主要作用是控制组进行早期的初始化。控制组的作用是为了给进程分配不同的cpu、IO等资源。
local_irq_disable(),这个函数主要作用是关闭当前CPU的所有中断响应,方便后面对各类资源初始化的时候屏蔽多余中断。
early_boot_irqs_off(),这个函数主要作用是标记内核还在早期初始化代码阶段,并且中断在关闭状态,如果有任何中断打开或请求中断的事情出现,都是会提出警告,以便跟踪代码错误情况。
boot_cpu_init(),这个函数主要作用是设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好。
page_address_init(),这个函数主要作用是初始化高端内存的映射表。
pr_notice("%s", linux_banner),这行代码主要作用是在输出终端上显示版本信息、编译的电脑用户名称、编译器版本、编译时间。
接下来继续单步执行,调试代码和结果如下图:



分析:从输出版本之后,start_kernel函数进行的操作就是进行内核架构、页表、内存、命令行、以及各种参数的初始化,这些可以暂时不用详细了解,其中需要我们了解的是:
vfs_caches_init_early(),这个函数是虚拟文件系统的缓存初始化
mm_init(),这个函数是标记那些内存可以使用,并且告诉系统有多少内存可以使用,当然是除了内核使用的内存以外。
sched_init(),这个函数主要作用是对进程调度器进行初始化,比如分配调度器占用的内存,初始化任务队列,设置当前任务的空线程,当前任务的调度策略为CFS调度器
init_IRQ(),这个函数是初始化中断相关的工作,主要初始化中断描述数组,然后调用每个CPU架构中断初始化。
softirq_init(),这个函数是初始化软件中断,软件中断与硬件中断区别就是中断发生时,软件中断是使用线程来监视中断信号,而硬件中断是使用CPU硬件来监视中断。
console_init(),这个函数是用来初始化控制台,从这个函数之后就可以输出内容到控制台了。如下图,可以看到qemu虚拟机中出现了控制台启用的信息:

接下来进行的操作就是继续进行各种初始化,不过在控制台初始化之后,相应的信息都会在qemu虚拟机的界面上显示出来,看起来就很直观了。我们继续单步执行:

执行到最后一步rest_init()函数,这个函数会进行后续的初始化工作,并开始正式执行内核进程和用户级进程。

实验收获

通过该次实验,单步执行linux内核的入口函数,对内核的初始化有了个大概的了解。其中我觉得比较重要的是,了解到了Linux系统0号进程,1号进程,2号进程的产生过程以及他们分别的作用。
0号进程来源于init_task()函数,该函数会演变为idle进程,是唯一一个没有通过fork()产生的进程,该进程的作用是当没有其它进程能够执行时,会调度执行idle。在调度器发现执行队列为空的时候执行,将其调入执行。
1号进程来源于kernel_init()1号内核线程,该进程由0号进程创建,是内核启动的第一个用户态进程,该进程是其他所有用户进程的祖先进程
2号进程是kthreadd()2号内河县城,始终运行在内核空间,是所有内核态其他守护线程的父线程。

原文地址:https://www.cnblogs.com/lms20209317/p/13888463.html