VSCode-based Linux kernel debugging environment construction and start_kernel tracking analysis

1. Install development tools

The first step is to install the corresponding development tools on the Linux virtual machine, use the following command

sudo apt install build-essential
 
sudo apt install qemu # install QEMU# as a virtual machine
 
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
#Dependencies required to compile the kernel --- some basic libraries

2. Download the kernel source code

Install using the following command

?wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz

3. Configure kernel options

make defconfig # Default configuration is based on 'x86_64_defconfig'
make menuconfig# Open debug related options


The interface after opening is shown in the figure
Then
Kernel hacking ->
Compile-time checks and compiler options ->
[] Compile the kernel with debug info
[
] Provide GDB scripts for kernel debugging
[*] Kernel debugging
Processor type and features —->
[ ] Randomize the address of the kernel image (KASLR)

4. Compile the kernel and run the kernel

make -j$(nproc) # nproc gives the number of CPU cores/threads available
# Test whether the kernel can be loaded and run normally, because there will be a kernel panic without a file system
qemu-system-x86_64 -kernel arch/x86/boot/bzImage

Compiled

Run kernel tests

5. Create a memory root file system

When the computer is powered on, the kernel is first loaded by the bootloader, and then the kernel needs to mount the memory root file system, which contains the necessary device drivers and tools. The bootloader loads the root file system into the memory, and the kernel will mount it to the root directory/download , and then run the init script in the root file system to perform some startup tasks, and finally mount the real disk root file system. Here we use BusyBox to build a minimal memory root file system and provide basic user mode executable programs.

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

compile

make menuconfig


Then compile and install

make -j$(nproc) & amp; & amp; make install

Then make a memory root file system image, the general process is as follows:

mkdir 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/


Next, prepare the init script file and put it in the root file system and directory (rootfs/init), add the following content to the init file: (Use touch init to create the init script file, then “vim init” to open the init file, and then press i to insert mode, paste it in, then press ESC to enter the command mode, :wq to save and exit.)

#!/bin/sh
mount -t proc none /proc
mount -t sysfs none /sys
echo "Wellcome YouYouOS!"
echo "--------------------"
cd home
/bin/sh

The mount command is to mount proc and sys.

Next, add executable permissions to the script: chmod + x init
Packaged into a memory root file system image: find . -print0 | cpio –null -ov –format=newc | gzip -9 > …/rootfs.cpio.gz
Test to mount the root file system to see if the init script is executed after the kernel boot is complete:
qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -initrd rootfs.cpio.gz


The init script executed successfully. At this point, you can directly use gdb to debug.

gdb debugging

implement:
qemu-system-x86_64 -kernel ./arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s

Then open another terminal and execute: gdb vmlinux

Then do a gdb test trace

(gdb) target remote:1234 //This is to establish a connection through the previously reserved tcp:1234
(gdb) b start_kernel //This is the first breakpoint set
c, bt, list, next, step.... //Check the gdb command for details

VSCODE debugging

First search and install these extensions in VScode

Because the starting point of the linux kernel is the “start_kernel” function, first break the point at start_kernel: click the run and debug icon, and add a function breakpoint in the breakpoint: start_kernel

Click Run – start debugging, and start trace analysis from start_kernel:

You can see that the program execution stopped at start_kernel:

Click the single point to skip, here we see that process 0 init_task is set as the first process of the whole system (process 0 is created manually, other processes are created by process 0) when the kernel boots, init_task will be created and start, which is the starting point for all other processes.

Continue to skip, start_kernel will continue to perform some initialization operations, including initializing various important data structures, drivers, interrupt handlers, etc. At this stage, the kernel will establish some necessary core data structures, such as physical memory manager, virtual memory manager, and process scheduler.
Finally came to the end of start_kernel arch_call_reset_init(), the definition of this function is to execute the reset_init() function, so we set a function breakpoint “reset_init”, continue to click the single point to skip and enter reset_init inside the function. This function is executed by process 0.


Continue to execute. At this time, we encountered kernel_init, that is, process No. 1. It is the ancestor of all user processes. It is created by the kernel_thread function. The kernel_thread function creates a new kernel thread (actually, linux does not support threads, so it is a kernel process). This thread The entry address is the kernel_init() function.

Click on the definition of the kernel_thread function.


It is found that the kernel_thread function creates a process through the _do_fork function.

The _do_fork function mainly completes calling copy_process() to copy the parent process, calling wake_up_new_task to add the child process to the ready queue to wait for scheduling execution, etc.
Next, run the kernel_init() function in the new thread (process), click on the kernel_init() function, you can see that the run_init_process function is called in this:

In this way, process No. 1 completes the user mode initialization.
Next is the creation of No. 2 process, No. 2 process is the ancestor of all kernel processes, kernel_thread creates No. 2 process, and the function executed by the process is kthreadd:

This thread keeps checking the list for kernel threads that need to be created and creates them as needed.