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.