VS Code-based Linux kernel debugging environment construction and start_kernel tracking analysis

1. Configure VSCode

In order to prevent the problem of slow downloading through the virtual machine, choose to download the vscode installation package and upload the virtual machine through finalshell.

Execute the following installation command to install

sudo apt install ./code_1.76.2-1678817801_amd64.deb

Also install the VSCode plugins C/C++ Intellisense and C/C++ Themes. Since the plug-in C/C++ Intellisense requires GUN Global, the following command is also required to install GUN Global.

sudo apt install global

Then install the above plugin

2. Install development tools

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

3. Download kernel source code

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 #download

xz -d linux-5.4.34.tar.xz

tar -xvf linux-5.4.34.tar#Decompression

cd linux-5.4.34

4. Configure kernel options

make defconfig # Default configuration is based on 'x86_64_defconfig'

make menuconfig# open debug related options

Open the picture as shown below:

Change the following configuration:

Kernel hacking —>

Compile-time checks and compiler options —>

[*] Compile the kernel with debug info

[*] Provide GDB scripts for kernel debugging

[*] Kernel debugging

# Turn off KASLR (random address), otherwise the breakpoint will fail. In this way, the debugger can trace the source code. The reason why the random address is set is to prevent hacker attacks:

Processor type and features —->

[ ] Randomize the address of the kernel image (KASLR)

5. Compile 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

6. Create a 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.

In order to simplify the experimental environment, we only make the memory root file system here. Here, BusyBox is used to build a minimal memory root file system and provide basic user mode executable programs.

First download the busybox source code from https://www.busybox.net and decompress it. After the decompression is complete, configure and compile it like the kernel, and install it

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
#Remember to compile it into a static link instead of a dynamic link library. 

Settings —>

[*] Build static binary (no shared libs) static compilation

Then compile and install, and it will be installed in the _install directory under the source code directory by default.

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 place it in the root file system and directory (rootfs/init), and add the following content to the init file:

#!/bin/sh

mount -t proc none /proc

mount -t sysfs none /sys

echo “Wellcome TinyyOS!”

echo “——————–”

cd home

/bin/sh

Next, add executable permissions to the init 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

7. Debug the Linux kernel

Since the Linux kernel is highly customized, there is no way to make Intellisense prompt normally by configuring includePath, etc. Here, a Python script is used to generate the compile_commands.json file to help Intellisense prompt normally (including header files and macro definitions, etc.). Run the following command directly in the Linux source code directory to generate compile_commands.json. To open the previously prepared linux-5.4.34 folder in vscode, you need to do two things:

(1) Command line input:

python ./scripts/gen_compile_commands.py

(2) You need to manually place the configuration file in the linux-5.4.34 folder before this: create a new .vscode folder, and put all the files in the configuration file into the .vscode folder

Modify the value of command in task.json to be consistent with the current file path

8. Track the boot process of the Linux kernel

The starting point of the Linux kernel is the start_kernel function, so first break the point at start_kernel (click the run and debug icon, add a function breakpoint in the breakpoint: start_kernel)

Start debugging, the program is suspended at the breakpoint, and trace analysis is started from 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.

At the end of the start_kernel() function, the arch_call_rest_init() function body is the rest_init() function, so set a rest_init function breakpoint and enter the rest_init function body, which is executed by process 0.

The kernel_thread function creates kernel_init, which corresponds to process No. 1 and is the ancestor of all user processes. Then the kernel_thread function creates kthreadd, which corresponds to process No. 2 and is the ancestor of all kernel processes. Enter the kernel_thread function to view, which creates a process through the _do_fork function. So it can be seen that process No. 1 and process No. 2 are finally created through _do_fork, and the process created by the user state through the system call fork is finally completed through _do_fork.

The principle of do_fork is as follows:

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, and the run_init_process function uses the do_execve function. do_execve loads the executable file, runs the init program, and executes the exec system call, so that process 1 completes the user mode initialization.

See kthreadd function definition. Process 2 is created and initialized.

The knowledge points of the article match the official knowledge files, and you can further learn relevant knowledge Cloud native entry skill treeHomepageOverview 11124 people are learning systematically