Use VScode to read Linux source code

Install and compile dependencies

sudo apt install build-essential libncurses-dev bison flex libssl-dev libelf-dev

Download kernel source code

Download the version you want from www.kernel.org, here is 5.4.34.

Configure kernel compilation options

cd linux-5.4.34/
make menuconfig

The relevant configurations to be noted are as follows

make menuconfig
# Open debug related options
Kernel hacking --->
    Compile-time checks and compiler options --->
        [*] Compile the kernel with debug info
        [*] Provide GDB scripts for kernel debugging
 [*] Kernel debugging
# Close KASLR, otherwise the breakpoint will fail
Processor type and features ---->
    [] Randomize the address of the kernel image (KASLR)
vim.config

Search for CONFIG_PREEMPTION in it, set it to y, if this item is not added, otherwise it may cause compilation failure

/CONFIG_PREMPTION
CONFIG_PREEMPTION=y

start compiling

make -j10

Download qemu, and test whether the kernel can load and run normally, because there will be a kernel panic without a file system

sudo apt install qemu-system-x86
qemu-system-x86_64-kernel arch/x86/boot/bzImage

Using busybox to make a file system

First download the busybox source code from https://www.busybox.net and decompress it. After the decompression is complete, configure, compile and install it like the kernel. The version 1.36.0 is installed here. Try to choose a newer version for installation, otherwise Errors may occur.

cd busybox-1.36.0
make menuconfig

The relevant configurations that need to be noted are as follows, using static links

Settings --->
    [*] Build static binary (no shared libs)

start compiling

make -j10

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/

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 "526"
echo "--------------------"
cd home
/bin/sh

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 linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz

can start

Configure VSCode

Open the linux-5.4.34 folder with VSCode

Create a .vscode folder on the linux-5.4.34 folder and add the following configuration files

c_cpp_properties.json

{
    "configurations": [
        {
            "name": "Linux",
            "includePath": [
                "${workspaceFolder}/arch/x86/include/**",
                "${workspaceFolder}/include/**",
                "${workspaceFolder}/include/linux/**",
                "${workspaceFolder}/arch/x86/**",
                "${workspaceFolder}/**"
            ],
            "cStandard": "c11",
            "intelliSenseMode": "gcc-x64",
            "compileCommands": "${workspaceFolder}/compile_commands.json"
        }
    ],
    "version": 4
}

compile_commands.json

Use a Python script 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.

python3 ./scripts/gen_compile_commands.py

Copy the generated compile_commands.json to .vscode

launch.json

Since using qemu directly in preLaunchTask may cause blocking, the task will be canceled here and opened manually.

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
      {
        "name": "(gdb) linux",
        "type": "cppdbg",
        "request": "launch",
        //"preLaunchTask": "vm",
        "program": "${workspaceRoot}/vmlinux",
        "miDebuggerServerAddress": "localhost:1234",
        "args": [],
        "stopAtEntry": true,
        "cwd": "${workspaceFolder}",
        "environment": [],
        "externalConsole": false,
        "MIMode": "gdb",
        "miDebuggerArgs": "-n",
        "targetArchitecture": "x64",
        "setupCommands": [
          {
            "text": "set arch i386:x86-64:intel",
            "ignoreFailures": false
          },
          {
            "text": "dir .",
            "ignoreFailures": false
          },
          {
            "text": "add-auto-load-safe-path ./",
            "ignoreFailures": false
          },
          {
            "text": "-enable-pretty-printing",
            "ignoreFailures": true
          }
        ]
      }
    ]
  }

tasks.json

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "2.0.0",
    "tasks": [
      {
        "label": "vm",
        "type": "shell",
        "command": "qemu-system-x86_64 -kernel ${workspaceFolder}/arch/x86/boot/bzImage -initrd ../rootfs.cpio.gz -S -s -nographic -append "console =ttyS0"",
        "presentation": {
          "echo": true,
          "clear": true,
          "group": "vm"
        },
        "isBackground": true,
        "problemMatcher": [
          {
            "pattern": [
              {
                "regexp": ".",
                "file": 1,
                "location": 2,
                "message": 3
              }
            ],
            "background": {
              "activeOnStart": true,
              "beginsPattern": ".",
              "endsPattern": ".",
            }
          }
        ]
      },
      {
        "label": "build linux",
        "type": "shell",
        "command": "make",
        "group": {
          "kind": "build",
          "isDefault": true
        },
        "presentation": {
          "echo": false,
          "group": "build"
        }
      }
    ]
}

settings.json

{
    "search. exclude": {
        "**/.git": true,
        "**/.svn": true,
        "**/.DS_Store": true,
        "**/drivers": true,
        "**/sound": true,
        "**/tools": true,
        "**/arch/alpha": true,
        "**/arch/arc": true,
        "**/arch/c6x": true,
        "**/arch/h8300": true,
        "**/arch/hexagon": true,
        "**/arch/ia64": true,
        "**/arch/m32r": true,
        "**/arch/m68k": true,
        "**/arch/microblaze": true,
        "**/arch/mn10300": true,
        "**/arch/nds32": true,
        "**/arch/nios2": true,
        "**/arch/parisc": true,
        "**/arch/powerpc": true,
        "**/arch/s390": true,
        "**/arch/sparc": true,
        "**/arch/score": true,
        "**/arch/sh": true,
        "**/arch/um": true,
        "**/arch/unicore32": true,
        "**/arch/xtensa": true
    },
    "files. exclude": {
        "**/.*.*.cmd": true,
        "**/.*.d": true,
        "**/.*.o": true,
        "**/.*.S": true,
        "**/.git": true,
        "**/.svn": true,
        "**/.DS_Store": true,
        "**/drivers": true,
        "**/sound": true,
        "**/tools": true,
        "**/arch/alpha": true,
        "**/arch/arc": true,
        "**/arch/c6x": true,
        "**/arch/h8300": true,
        "**/arch/hexagon": true,
        "**/arch/ia64": true,
        "**/arch/m32r": true,
        "**/arch/m68k": true,
        "**/arch/microblaze": true,
        "**/arch/mn10300": true,
        "**/arch/nds32": true,
        "**/arch/nios2": true,
        "**/arch/parisc": true,
        "**/arch/powerpc": true,
        "**/arch/s390": true,
        "**/arch/sparc": true,
        "**/arch/score": true,
        "**/arch/sh": true,
        "**/arch/um": true,
        "**/arch/unicore32": true,
        "**/arch/xtensa": true
    },
    "[c]": {
        "editor. detectIndentation": false,
        "editor.tabSize": 8,
        "editor.insertSpaces": false
    },
    "C_Cpp.errorSquiggles": "disabled"
}

Now you can start debugging, add a breakpoint in the start_kernel() function in init/main.c, and enter in the terminal

qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"< /pre>
 <p>Then press F5 in VSCode to debug</p>
 
  
   
    <img src="//i2.wp.com/img-blog.csdnimg.cn/img_convert/f27bfed27d993c39ba50a07eba5b1bcc.png" style="margin-left:;display:block;width:1844px;margin-top: -56.50759%;height:auto;">
   
  
 
 <h2>Track the boot process of the Linux kernel</h2>
 <p>init_task is located in the init/init_task.c folder, is a manually created static process, and is the ancestor of all processes in the linux system.</p>
 
  
   
    <img src="//i2.wp.com/img-blog.csdnimg.cn/img_convert/30a190fe3e2e32597e397518abd0bfee.png" style="margin-left:;display:block;width:1844px;margin-top: -56.50759%;height:auto;">
   
  
 
 <p>init_task is the first thread in the Linux kernel. It runs through the initialization process of the entire Linux system. This process is also the only kernel-mode process (kernel thread) in the Linux system that is not created with the kernel_thread() function.</p>
 <p>Next, start_kernel executes the initialization of each important subsystem of the kernel in turn, such as mm, cpu, sched, irq, etc. Finally, a rest_init will be called to initialize the rest.</p>
 <pre>asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;
    set_task_stack_end_magic( &init_task);
    smp_setup_processor_id();
    debug_objects_early_init();

    cgroup_init_early();

    local_irq_disable();
    .........
//remaining initialization
    arch_call_rest_init();
}

void __init __weak arch_call_rest_init(void)
{
    rest_init();
}
noinline void __ref rest_init(void)
{
    struct task_struct *tsk;
    int pid;

    rcu_scheduler_starting();
    /*
     * We need to spawn init first so that it obtains pid 1, however
     * the init task will end up wanting to create kthreads, which, if
     * we schedule it before we create kthreadd, will OOPS.
     */
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    /*
     * Pin init on the boot CPU. Task migration is not properly working
     * until sched_init_smp() has been run. It will set the allowed
     * CPUs for init to the non isolated CPUs.
     */
    rcu_read_lock();
    tsk = find_task_by_pid_ns(pid, & init_pid_ns);
    set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
    rcu_read_unlock();

    numa_default_policy();
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, & init_pid_ns);
    rcu_read_unlock();

    /*
     * Enable might_sleep() and smp_processor_id() checks.
     * They cannot be enabled earlier because with CONFIG_PREEMPTION=y
     * kernel_thread() would trigger might_sleep() splats. With
     * CONFIG_PREEMPT_VOLUNTARY=y the init task might have scheduled
     * already, but it's stuck on the kthreadd_done completion.
     */
    system_state = SYSTEM_SCHEDULING;

    complete( &kthreadd_done);

    /*
     * The boot idle thread must execute schedule()
     * at least once to get things moving:
     */
    schedule_preempt_disabled();
    /* Call into cpu_idle with preempt disabled */
    cpu_startup_entry(CPUHP_ONLINE);
}

Only two lines actually need to be concerned in rest_init

//line 12
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
//line 24
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

View kernel_thread function

/*
 * Create a kernel thread.
 */
pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
{
    struct kernel_clone_args args = {
        .flags = ((flags | CLONE_VM | CLONE_UNTRACED) & ~CSIGNAL),
        .exit_signal = (flags & CSIGNAL),
        .stack = (unsigned long)fn,
        .stack_size = (unsigned long) arg,
    };

    return _do_fork( &args);
}

Check out the _do_fork() function

/*
 * Ok, this is the main fork-routine.
 *
 * It copies the process, and if successful kick-starts
 * it and waits for it to finish using the VM if required.
 *
 * args->exit_signal is expected to be checked for sanity by the caller.
 */
long _do_fork(struct kernel_clone_args *args)
{
    u64 clone_flags = args->flags;
    struct completion vfork;
    struct pid *pid;
    struct task_struct *p;
    int trace = 0;
    long nr;

    /*
     * Determine whether and which event to report to ptracer. When
     * called from kernel_thread or CLONE_UNTRACED is explicitly
     * requested, no event is reported; otherwise, report if the event
     * for the type of forking is enabled.
     */
    if (!(clone_flags & CLONE_UNTRACED)) {
        if (clone_flags & CLONE_VFORK)
            trace = PTRACE_EVENT_VFORK;
        else if (args->exit_signal != SIGCHLD)
            trace = PTRACE_EVENT_CLONE;
        else
            trace = PTRACE_EVENT_FORK;

        if (likely(!ptrace_event_enabled(current, trace)))
            trace = 0;
    }

    p = copy_process(NULL, trace, NUMA_NO_NODE, args);
    add_latent_entropy();

    if (IS_ERR(p))
        return PTR_ERR(p);

    /*
     * Do this prior waking up the new thread - the thread pointer
     * might get invalid after that point, if the thread exits quickly.
     */
    trace_sched_process_fork(current, p);

    pid = get_task_pid(p, PIDTYPE_PID);
    nr = pid_vnr(pid);

    if (clone_flags & CLONE_PARENT_SETTID)
        put_user(nr, args->parent_tid);

    if (clone_flags & CLONE_VFORK) {
        p->vfork_done = &vfork;
        init_completion( & vfork);
        get_task_struct(p);
    }

    wake_up_new_task(p);

    /* forking complete and child started to run, tell ptracer */
    if (unlikely(trace))
        ptrace_event_pid(trace, pid);

    if (clone_flags & CLONE_VFORK) {
        if (!wait_for_vfork_done(p, &vfork))
            ptrace_event_pid(PTRACE_EVENT_VFORK_DONE, pid);
    }

    put_pid(pid);
    return nr;
}

_do_fork() calls copy_process(), which does the actual work of spawning the child process and copies the parent’s data according to the specified flags. After the child process is generated, call wake_up_new_task to add the child process to the scheduler and allocate CPU for it. Finally returns the child process pid.

Next look at the kernel_init() and kthreadd() functions, which are started as processes.

static int __ref kernel_init(void *unused)
{
    int ret;

    kernel_init_freeable();
    /* need to finish all async __init code before freeing the memory */
    async_synchronize_full();
    ftrace_free_init_mem();
    free_initmem();
    mark_readonly();

    /*
     * Kernel mappings are now finalized - update the userspace page-table
     * to finalize PTI.
     */
    pti_finalize();

    system_state = SYSTEM_RUNNING;
    numa_default_policy();

    rcu_end_inkernel_boot();

    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\\
",
               ramdisk_execute_command, ret);
    }

    /*
     * We try each of these until one succeeds.
     *
     * The Bourne shell can be used instead of init if we are
     * trying to recover a really broken machine.
     */
    if (execute_command) {
        ret = run_init_process(execute_command);
        if (!ret)
            return 0;
        panic("Requested init %s failed (error %d).",
              execute_command, ret);
    }
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    panic("No working init found. Try passing init= option to kernel. "
          "See Linux Documentation/admin-guide/init.rst for guidance.");
}

Execute the initialization of various peripheral drivers, mount the root file system, execute /init executable files in order, and complete the user mode initialization.

int kthreadd(void *unused)
{
    struct task_struct *tsk = current;

    /* Setup a clean context for our children to inherit. */
    set_task_comm(tsk, "kthreadd");
    ignore_signals(tsk);
    set_cpus_allowed_ptr(tsk, cpu_all_mask);
    set_mems_allowed(node_states[N_MEMORY]);

    current->flags |= PF_NOFREEZE;
    cgroup_init_kthreadd();

    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);
        if (list_empty( &kthread_create_list))
            schedule();
        __set_current_state(TASK_RUNNING);

        spin_lock( &kthread_create_lock);
        while (!list_empty( & amp;kthread_create_list)) {
            struct kthread_create_info *create;

            create = list_entry(kthread_create_list.next,
                        struct kthread_create_info, list);
            list_del_init( &create->list);
            spin_unlock( &kthread_create_lock);

            create_kthread(create);

            spin_lock( &kthread_create_lock);
        }
        spin_unlock( &kthread_create_lock);
    }

    return 0;
}

Set the name of the current process to “kthreadd”, and set the state of the current process cyclically as TASK_INTERRUPTIBLE, which can be interrupted, and judge whether the kthread_create_list linked list is empty. If it is empty, it will be dispatched to give up the cpu. If it is not empty, then Take one from the linked list, and then call kthread_create to create a kernel thread. So the parent process of all kernel threads is kthreadd.