内核准备
内核和相关环境
wget https://raw.github.com/mengning/mykernel/master/mykernel-2.0_for_linux-5.4.34.patch
sudo apt install axel
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz
xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar
cd linux-5.4.34
patch -p1 < ../mykernel-2.0_for_linux-5.4.34.patch
sudo apt install build-essential gcc-multilib
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
内核配置
make defconfig # Default configuration is based on 'x86_64_defconfig'
make menuconfig
打开debug相关选项 输入y和n进行选中或取消选中
Kernel hacking --->
Compile-time checks and compiler options --->
[*] Compile the kernel with debug info
[*] Provide GDB scripts for kernel debugging
[*] Kernel debugging
关闭KASLR,否则会导致打断点失败
Processor type and features ---->
[] Randomize the address of the kernel image (KASLR)
编译内核
make -j$(nproc) # nproc gives the number of CPU cores/threads available
制作根文件系统
电脑加电启动首先由bootloader加载内核,内核紧接着需要挂载内存根文件系统,其中包含必要的设备驱动和工具,bootloader加载根文件系统到内存中,内核会将其挂载到根目录/下,然后运行根文件系统中init脚本执行一些启动任务,最后才挂载真正的磁盘根文件系统。我们这里为了简化实验环境,仅制作内存根文件系统。这里借助BusyBox 构建极简内存根文件系统,提供基本的用户态可执行程序。
首先从https://www.busybox.net下载 busybox源代码解压,解压完成后,跟内核一样先配置编译,并安装。
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
tar -jxvf busybox-1.31.1.tar.bz2
cd busybox-1.31.1
make menuconfig
编译安装:
make -j$(nproc) && make install
新建一个.vscode文件夹,将配置文件放入该文件夹内,配置如下五个文件:c_cpp_properties.json,init,launch.json,settings.json,tasks.json。
start_kernel分析
Linux内核的起点是start_kernel函数。因此,我们可以在start_kernel处设置一个断点,启动调试,程序将在该断点处暂停,从start_kernel开始进行跟踪分析。通过逐步执行进行跟踪分析,我们可以发现0号进程init_task被设为整个系统的初始进程。也就是说,0号进程是手动创建的,其他进程都是由0号进程创建的。在内核引导时,init_task会被创建并启动,它是所有其他进程的起点。
我们继续逐步执行,start_kernel函数执行各种初始化操作。
在start_kernel函数的末尾,我们可以看到arch_call_rest_init()函数体内调用了rest_init()函数。因此,我们可以在rest_init函数内设置一个断点,进入其函数体内部。这个函数由0号进程执行。
kernel_thread函数创建了kernel_init进程,对应1号进程,它是所有用户进程的祖先。
然后,kernel_thread函数创建了kthreadd进程,对应2号进程,它是所有内核进程的祖先。
我们进入kernel_thread函数查看,该函数通过do_fork函数来创建进程。通过kernel_thread函数的代码可以看出,1号进程和2号进程最终都是通过do_fork创建的。用户空间通过系统调用fork来创建进程,最终也是通过do_fork来完成的。
进程的创建过程大致是这样的:父进程通过fork系统调用进入内核_do_fork函数,复制进程描述符及相关进程资源(采用写时复制技术),分配子进程的内核堆栈,并对内核堆栈和进程的关键上下文进行初始化,最后将子进程放入就绪队列。fork系统调用返回后,子进程根据设置的内核堆栈和进程的关键上下文开始执行。
我们查看kernel_init函数的定义,可以看到它会调用run_init_process函数。
run_init_process函数的do_execve函数用于加载可执行文件、运行init程序并执行exec系统调用。1号进程完成了用户空间的初始化。
我们再来查看kthreadd函数的定义。2号进程被创建并完成初始化。