本博客基于 Ubuntu 20.04.5 LTS,内核版本 5.4.34,其它 Linux 发行版以及内核版本请自行对个别命令进行调整。
一、环境配置
sudo apt install build-essential sudo apt install qemu # 安装 QEMU 虚拟机 sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
二、下载内核源码
wget 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
三、配置内核编译选项
make defconfig # Default configuration is based on 'x86_64_defconfig' make menuconfig
执行完毕后会出现以下界面。

分别进入以下项目打开、关闭对应选项。
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)
四、编译内核
make -j$(nproc)
编译完成后,内核其实还不能正确加载运行,因为缺少相应的文件系统。
五、制作内存根文件系统
电脑加电启动首先由 bootloader 加载内核,内核紧接着需要挂载内存根文件系统,其中包含必要的设备驱动和工具。bootloader 加载根文件系统到内存中后,内核会将其挂载到根目录下,然后运行根文件系统中 init 脚本执行一些启动任务,最后才挂载真正的磁盘根文件系统。本实验为了简化环境,仅制作内存根文件系统,这里借助 BusyBox 构建极简内存根文件系统,提供基本的用户态可执行程序。
首先下载 busybox 源码并解压。
wget https://busybox.net/downloads/busybox-1.36.0.tar.bz2 tar -jxvf busybox-1.36.0.tar.bz2 cd busybox-1.36.0
解压完成后,配置编译选项,采用静态链接,不使用动态链接库。
make menuconfig
Settings --->
[*] Build static binary (no shared libs)
接着编译安装,默认会安装到源码目录下的 _install 目录中。
make -j$(nproc) && make install
然后制作内存根文件系统镜像,命令如下。
mkdir rootfs
cd rootfs
cp ../busybox-1.36.0/_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
准备 init 脚本放在根文件系统根目录下(rootfs/init),内容如下。
#!/bin/sh mount -t proc none /proc mount -t sysfs none /sys echo "Wellcome LouyuOS!" echo "--------------------" cd home /bin/sh
记得给 init 脚本添加可执行权限。
chmod +x init
最后打包成内存根文件系统镜像。
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz
六、尝试启动内核
在 Linux 内核源码目录用以下命令启动内核,若看到 init 脚本的输出,说明内核启动成功。
qemu-system-x86_64 -kernel arch/x86/boot/bzImage -initrd rootfs.cpio.gz -nographic -append "console=ttyS0"

如果提示内存根文件系统镜像文件找不到,需要根据实际情况调整命令中的路径。
七、在命令行中调试 Linux 内核
要在命令行中使用 gdb 跟踪调试内核,我们需要加两个参数,一个是 -s,表明在 TCP 1234 端口上创建一个 gdb-server。我们此时另外打开一个窗口,用 gdb 把带有符号表的内核镜像 vmlinux 加载进来,然后连接 gdb-server,设置断点跟踪内核(若不想使用默认的 1234 端口,可以使用 -gdb tcp:xxxx 来替代 -s 选项)。另一个是 -S 代表启动时暂停虚拟机,等待 gdb 执行 continue 指令后再开始执行(可以简写为 c)。
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"
打开另一个终端窗口,启动 gdb,把内核符号表加载进来,建立与 gdb server 的连接。
cd linux-5.4.34/ gdb vmlinux (gdb) target remote:1234 # 指定连接remote,这里连接到了上面创建的本地gdb server (gdb) b start_kernel (gdb) c、bt、list、next、step....
八、在 VSCode 中调试 Linux 内核
在命令行下打断点跟踪代码不够方便,下面介绍如何使用 VSCode 来调试代码。首先在 Linux 内核源码目录的 .vscode 文件夹(若没有则新建一个)中放置四个配置文件,接着在 VSCode 中安装 C/C++、C/C++ Themes、GDB Debug 扩展。

由于 C/C++ Intellisense 需要依赖 GNU Global,需要使用如下命令安装 GNU Global。
sudo apt install global
在用 VSCode 打开源码项目前,还要在内核源码根目录下运行以下脚本,否则后面的调试会报错。
python ./scripts/gen_compile_commands.py
由于 Linux内核高度定制化,所以没有办法直接通过配置 includePath等让 Intellisense 正常提示,这里借助一个 Python 脚本来生成 compile_commands.json文件帮助 Intellisense正常提示(包括头文件和宏定义等)。在 Linux源代码目录下直接运行上面的命令就可以生成 compile_commands.json 了。
上述配置完毕后,在 VSCode 的运行菜单选择启动调试即可。


九、简要分析 Linux 内核的启动过程
Linux 内核的启动主要由 init/main.c 下的 start_kernel 函数完成。start_kernel 函数完成了设置 init_task(0 号进程)栈底、初始化处理器 ID、初始化追踪对象、初始化内存分页、初始化中断向量表、初始化内存分配器、初始化进程调度器、系统时钟设置、vfs 初始化等操作,最后执行 arch_call_rest_init() 调用 rest_init()。5.4.34 版本的内核中,rest_init() 函数代码及关键注释如下。
noinline void __ref rest_init(void)
{
struct task_struct *tsk;
int pid;
rcu_scheduler_starting();
/*
* 调用kernel_thread()创建1号进程,后续会演变为init进程
* 是系统中用户进程的祖先进程,Linux中的所有进程都是有init进程创建并运行的
* 在系统启动完成完成后,init将变为守护进程监视系统其他进程
*/
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();
/*
* 调用kernel_thread(),创建2号进程,负责所有内核线程的创建、调度和管理
* 所有的内核线程都是直接或者间接的以kthreadd为父进程
*/
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);
/*
* 内含对函数schedule()的调用,切换当前进程(0号进程)为1号进程
* 在调用该函数之前,Linux系统中只有两个进程,即0号进程init_task和1号进程kernel_init
*/
schedule_preempt_disabled();
/* Call into cpu_idle with preempt disabled */
cpu_startup_entry(CPUHP_ONLINE); // 调用cpu_idle(),0号线程init_task进入idle函数的循环(即变成了“闲逛进程”),一旦系统有其它任务到来,就会退出这个状态
}
值得一提的是,作为 “万物之源” 的 0 号进程 init_task(所有的进程都是由 0 号进程创建)是在源码中写死的,其定义在 init/init_task.c,代码如下。
struct task_struct init_task
#ifdef CONFIG_ARCH_TASK_STRUCT_ON_STACK
__init_task_data
#endif
= {
#ifdef CONFIG_THREAD_INFO_IN_TASK
.thread_info = INIT_THREAD_INFO(init_task),
.stack_refcount = REFCOUNT_INIT(1),
#endif
.state = 0,
.stack = init_stack,
.usage = REFCOUNT_INIT(2),
.flags = PF_KTHREAD,
.prio = MAX_PRIO - 20,
.static_prio = MAX_PRIO - 20,
.normal_prio = MAX_PRIO - 20,
.policy = SCHED_NORMAL,
.cpus_ptr = &init_task.cpus_mask,
.cpus_mask = CPU_MASK_ALL,
.nr_cpus_allowed= NR_CPUS,
.mm = NULL,
.active_mm = &init_mm,
.restart_block = {
.fn = do_no_restart_syscall,
},
.se = {
.group_node = LIST_HEAD_INIT(init_task.se.group_node),
},
.rt = {
.run_list = LIST_HEAD_INIT(init_task.rt.run_list),
.time_slice = RR_TIMESLICE,
},
.tasks = LIST_HEAD_INIT(init_task.tasks),
#ifdef CONFIG_SMP
.pushable_tasks = PLIST_NODE_INIT(init_task.pushable_tasks, MAX_PRIO),
#endif
#ifdef CONFIG_CGROUP_SCHED
.sched_task_group = &root_task_group,
#endif
.ptraced = LIST_HEAD_INIT(init_task.ptraced),
.ptrace_entry = LIST_HEAD_INIT(init_task.ptrace_entry),
.real_parent = &init_task,
.parent = &init_task,
.children = LIST_HEAD_INIT(init_task.children),
.sibling = LIST_HEAD_INIT(init_task.sibling),
.group_leader = &init_task,
RCU_POINTER_INITIALIZER(real_cred, &init_cred),
RCU_POINTER_INITIALIZER(cred, &init_cred),
.comm = INIT_TASK_COMM,
.thread = INIT_THREAD,
.fs = &init_fs,
.files = &init_files,
.signal = &init_signals,
.sighand = &init_sighand,
.nsproxy = &init_nsproxy,
.pending = {
.list = LIST_HEAD_INIT(init_task.pending.list),
.signal = {{0}}
},
.blocked = {{0}},
.alloc_lock = __SPIN_LOCK_UNLOCKED(init_task.alloc_lock),
.journal_info = NULL,
INIT_CPU_TIMERS(init_task)
.pi_lock = __RAW_SPIN_LOCK_UNLOCKED(init_task.pi_lock),
.timer_slack_ns = 50000, /* 50 usec default slack */
.thread_pid = &init_struct_pid,
.thread_group = LIST_HEAD_INIT(init_task.thread_group),
.thread_node = LIST_HEAD_INIT(init_signals.thread_head),
#ifdef CONFIG_AUDIT
.loginuid = INVALID_UID,
.sessionid = AUDIT_SID_UNSET,
#endif
#ifdef CONFIG_PERF_EVENTS
.perf_event_mutex = __MUTEX_INITIALIZER(init_task.perf_event_mutex),
.perf_event_list = LIST_HEAD_INIT(init_task.perf_event_list),
#endif
#ifdef CONFIG_PREEMPT_RCU
.rcu_read_lock_nesting = 0,
.rcu_read_unlock_special.s = 0,
.rcu_node_entry = LIST_HEAD_INIT(init_task.rcu_node_entry),
.rcu_blocked_node = NULL,
#endif
#ifdef CONFIG_TASKS_RCU
.rcu_tasks_holdout = false,
.rcu_tasks_holdout_list = LIST_HEAD_INIT(init_task.rcu_tasks_holdout_list),
.rcu_tasks_idle_cpu = -1,
#endif
#ifdef CONFIG_CPUSETS
.mems_allowed_seq = SEQCNT_ZERO(init_task.mems_allowed_seq),
#endif
#ifdef CONFIG_RT_MUTEXES
.pi_waiters = RB_ROOT_CACHED,
.pi_top_task = NULL,
#endif
INIT_PREV_CPUTIME(init_task)
#ifdef CONFIG_VIRT_CPU_ACCOUNTING_GEN
.vtime.seqcount = SEQCNT_ZERO(init_task.vtime_seqcount),
.vtime.starttime = 0,
.vtime.state = VTIME_SYS,
#endif
#ifdef CONFIG_NUMA_BALANCING
.numa_preferred_nid = NUMA_NO_NODE,
.numa_group = NULL,
.numa_faults = NULL,
#endif
#ifdef CONFIG_KASAN
.kasan_depth = 1,
#endif
#ifdef CONFIG_TRACE_IRQFLAGS
.softirqs_enabled = 1,
#endif
#ifdef CONFIG_LOCKDEP
.lockdep_depth = 0, /* no locks held yet */
.curr_chain_key = INITIAL_CHAIN_KEY,
.lockdep_recursion = 0,
#endif
#ifdef CONFIG_FUNCTION_GRAPH_TRACER
.ret_stack = NULL,
#endif
#if defined(CONFIG_TRACING) && defined(CONFIG_PREEMPTION)
.trace_recursion = 0,
#endif
#ifdef CONFIG_LIVEPATCH
.patch_state = KLP_UNDEFINED,
#endif
#ifdef CONFIG_SECURITY
.security = NULL,
#endif
};
EXPORT_SYMBOL(init_task);