本博客基于 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);