1. 安装开发工具
sudo apt install build-essential
sudo apt install qemu # install QEMU
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
# 编译内核所需的依赖(一些基础库)
2. 下载内核源码
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
3. 配置内核选项
make defconfig # Default configuration is based on 'x86_64_defconfig'
make menuconfig # 打开debug相关选项
显示界面如下:
进行以下设置:
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)
4. 编译运行内核
make -j$(nproc) # nproc gives the number of CPU cores/threads available
# 测试一下内核能不能正常加载运行,因为没有文件系统最终会kernel panic
qemu-system-x86_64 -kernel arch/x86/boot/bzImage
运行结果如下:
5. 制作内存根文件系统
#首先从https://www.busybox.net下载 busybox源代码解压,解压完成后,跟内核一样先配置编译,并安装。
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2
#出现太多重定向问题 直接使用wget下载
wget 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 # 记得要编译成静态链接,不用动态链接库。
Settings —>
[*] Build static binary (no shared libs)
# 然后编译安装,默认会安装到源码目录下的 _install 目录中。
make -j$(nproc) && make install
然后制作内存根文件系统镜像,大致过程如下:
mkdir rootfs # 在/linux-5.4.34文件夹下新建rootfs文件夹
cd rootfs
cp ../busybox-1.31.1/_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
准备init脚本文件放在根文件系统跟目录下(rootfs/init),添加如下内容到init文件。
touch init
vim init
#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo “Wellcome WeiyiOS!”
echo “--------------------”
cd home
/bin/sh
给init脚本添加可执行权限
chmod +x init
打包成内存根文件系统镜像
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
测试挂载根文件系统,看内核启动完成后是否执行init脚本
qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -initrd rootfs.cpio.gz
成功执行了init脚本。执行结果如下:
6. 配置VSCode调试Linux内核
下载并安装VSCode
sudo apt install ./code_1.76.2-1678817801_amd64.deb
安装VSCode插件C/C++ Intellisense和C/C++ Themes。由于插件C/C++ Intellisense需要GNU Global,还需要使用如下命令安装GNU Global。
sudo apt install global
安装下列插件:
由于 Linux 内核高度定制化,所以没有办法直接通过配置 includePath 等让 Intellisense 正常提示,这里借助一个 Python 脚本来生成 compile_commands.json 文件帮助 Intellisense 正常提示(包括头文件和宏定义等)。在Linux源代码目录下直接运行如下命令就可以生成compile_commands.json 了。
python ./scripts/gen_compile_commands.py
新建一个.vscode文件夹,将配置文件放入该文件夹内。
修改task.json中command的值使得与当前文件路径一致
7. 进行跟踪分析
Linux内核的起点是start_kernel函数,因此先在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函数断点,进入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一个子进程的过程中,复制父进程的资源时采用了Copy On Write(写时复制)技术,不需要修改的进程资源父子进程是共享内存存储空间的
_do_fork函数主要完成了调用copy_process()复制父进程、获得pid、调用wake_up_new_task将子进程加入就绪队列等待调度执行等
copy_process()是创建一个进程的主要的代码,主要完成了调用dup_task_struct复制当前进程(父进程)描述符task_struct、信息检查、初始化、把进程状态设置为TASK_RUNNING(此时子进程置为就绪态)、采用写时复制技术逐一复制所有其他进程资源、调用copy_thread_tls初始化子进程内核栈、设置子进程pid等。其中最关键的就是dup_task_struct复制当前进程(父进程)描述符task_struct和copy_thread_tls初始化子进程内核栈。
总结来说,进程的创建过程大致是父进程通过fork系统调用进入内核_do_fork函数,如下图所示复制进程描述符及相关进程资源(采用写时复制技术)、分配子进程的内核堆栈并对内核堆栈和thread等进程关键上下文进行初始化,最后将子进程放入就绪队列,fork系统调用返回;而子进程则在被调度执行时根据设置的内核堆栈和thread等进程关键上下文开始执行。
具体分析kernel_init和kthreadd的实现
查看kernel_init函数定义,如下图的代码实现部分会调用run_init_process函数。
run_init_process函数如下,其中do_execve用于加载可执行文件、运行init程序并执行exec系统调用。1号进程完成用户态初始化。
查看kthreadd函数定义。2号进程创建并完成初始化。