构造一个Linux系统并跟踪分析Linux内核启动过程

本文详细介绍了如何配置实验环境,包括安装开发工具、下载内核源码、编译内核、制作根文件系统,以及使用QEMU测试内核。此外,还讲解了如何利用VSCode进行远程调试,包括生成编译配置文件和设置断点进行内核调试。
摘要由CSDN通过智能技术生成

实验环境配置

1、安装开发工具

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

2、下载安装多线程下载工具axel并使用axel下载内核源代码

sudo apt install axel
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.4.34.tar.xz

3、解压内核源代码

xz -d linux-5.4.34.tar.xz
tar -xvf linux-5.4.34.tar
cd linux-5.4.34

4、编译内核并配置编译选项

make 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

Processor type and features ---->
      [] Randomize the address of the kernel image (KASLR)

5、编译

make -j$(nproc)
#在linux-5.4.34文件夹下使用qemu测试内核是否能加载,结果会显示“kernel panic”
qemu-system-x86_64 -kernel arch/x86/boot/bzImage(因为实验已经做完所以没有这个报错这部分没有图片)

6、使用busybox制作根文件系统,我是在家目录文件夹下下载并解压的

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
#打开debug选项,选择静态链接
Settings --->
    [*] Build static binary (no shared libs)

编译安装到busybox-1.31.1下的_install目录下

make -j$(nproc) && make install

笔者在这部分遇到报错,然后换了虚拟机版本从20.04换到16.04后编译成功

7、制作内存根文件镜像,在busybox-1.31.1目录下新键rootfs文件并将busybox-1.31.1下的_install文件中的内容复制到rootfs中,新建四个文件夹,在dev文件下新建七个文档。

mkdir rootfs
cd rootfs
cp ../_install/* ./ -rf
mkdir dev proc sys home
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/

8、在rootfs文件夹下新建一个init脚本文件,并在init中添加如下内容:

#!/bin/sh
mount -t proc none /proc mount -t sysfs none /sys
echo "Wellcome MengningOS!" echo "--------------------"
cd home
/bin/sh

9、给init文件赋予可执行权限,此时init文件名变为绿色,说明其为可执行文件了

chmod +x init

10、打包成内存根文件系统镜像

find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../rootfs.cpio.gz 

测试挂载根文件系统,看内核启动完成后是否执行init脚本

cd ../
qemu-system-x86_64 -kernel ../linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz

11、VSCode中远程配置调试环境

 安装remote-ssh插件

配置config文件

 

刷新左侧菜单,再次点击主机名就能进行远程调试界面,输入用户名密码,然后选择我们的项目

可以发现能够成功导入 

在Linux源代码目录下直接运行如下命令就可以生成 compile_commands.json 了再依次创建settings.json,tasks.json和c__cpp_properties.json和lauch.json。

python ./scripts/gen_compile_commands.py

还要安装VSCode插件C/C++ Intellisense和C/C++ Themes。由于插件C/C++ Intellisense需要GNU Global,还需要使用如下命令安装GNU Global。

sudo apt install global

根据提示安装插件

 

 内核启动调试

在Linux系统中,start_kernel函数是整个内核启动过程的入口点,它是在boot loader加载内核映像文件之后被调用的。

我们在start_kernel函数设置断点按F5进行调试

可以看到能够成功进入断点

以set_task_stack_end_magic为例,它是start_kernel函数中的一个函数,主要是给每个进程的栈结构设置一个“magic value”(0x57AC6E9D)作为结束标示符。这个标示符可以有效地检测到进程栈的溢出,并协助内核进行调试和排错。

 总体来说,start_kernel函数完成了系统的初始化和启动,将系统从裸机状态变成了可用状态。大致流程如下:

  1. 初始化内核代码段和数据段。这个过程包括将内核镜像从只读状态转变为可读写状态,建立IDT和GDT表,进入保护模式等。

  2. 初始化物理内存管理器。这个过程将系统中所有的物理内存(包括CPU的缓存区和设备地址空间等)按页大小分割,并建立页表,使得内核可以对这些内存进行读写。

  3. 初始化虚拟内存管理器。这个过程将系统的虚拟地址空间映射到物理内存,将内存地址空间划分为内核空间和用户空间。

  4. 初始化进程调度器。这个过程建立进程控制块(PCB)和任务队列,并启动第一个进程。

  5. 安装各种外部设备驱动程序(如硬盘、网卡、USB等),并进行初始化。

  6. 在文件系统中查找并挂载根文件系统。

  7. 从根文件系统中读取和加载各种用户空间应用程序和库。

  8. 启动系统初始化脚本,执行各种服务初始化和配置操作,如启动各种守护进程、网络配置、挂载磁盘等。

  9. 用户态启动完毕后,控制权交回内核,执行主循环处理各种系统调用等内核操作。

rest_init函数是在start_kernel函数中最后一个被调用的函数。在start_kernel函数完成了系统初始化的一系列操作之后,它会调用rest_init函数创建进程init并启动init进程。

在Linux内核中,0号进程(swapper或idle进程)是在内核启动时就被创建的。它是内核中的一个特殊进程,负责在没有其他进程准备运行时占用CPU,以保证系统CPU的利用率。它的进程描述符init_task是内核静态创建的, 而它在进行初始化的时候通过kernel_thread的方式创建了两个内核线程,分别是kernel_init和kthreadd,其中kernel_init进程号为1

init进程是所有其他进程的祖先进程,它是系统启动之后的第一个进程,由内核启动过程中的一些脚本或程序创建的。一旦内核完成启动过程,启动脚本就会调用init程序,进而创建/init进程,负责初始化用户空间的系统环境,启动各种守护进程,创建其他用户进程,以及监控和维护系统的运行状态等。

 通过kernel_thread来生成一个内核进程,后者则会在新进程环境下调用kernel_init函数

如果内核在初始化init进程之前就创建kthreads,就有可能触发致命错误(内核崩溃),因为当init进程尝试创建kthreads时,相应的内核数据结构可能尚未正确初始化。

因此,为了确保init进程在创建任何kthreads之前被正确初始化并拥有PID 1,内核需要首先生成init进程,然后继续创建kthreads和其他内核级任务。

 _do_fork

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;
	}
    //复制进程描述符,copy_process()的返回值是一个task_struct指针
	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
	pid = get_task_pid(p, PIDTYPE_PID);
	nr = pid_vnr(pid);

	if (clone_flags & CLONE_PARENT_SETTID)
		put_user(nr, args->parent_tid);
    //如果调用的 vfork()方法,初始化 vfork 完成处理信息。
	if (clone_flags & CLONE_VFORK) {
		p->vfork_done = &vfork;
		init_completion(&vfork);
		get_task_struct(p);
	}
    //将子进程加入到调度器中,为其分配 CPU,准备执行
	wake_up_new_task(p);

	/* forking complete and child started to run, tell ptracer */
    //fork 完成,子进程即将开始运行
	if (unlikely(trace))
		ptrace_event_pid(trace, pid);
    //如果是 vfork,将父进程加入至等待队列,等待子进程完成
	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;
}

这里学习了fork和vfork的区别:

1. fork():子进程拷贝父进程的数据段,代码段; vfork():子进程与父进程共享数据段.

2. fork():父子进程的执行次序不确定.

vfork():保证子进程先运行,在调用exec或exit之前与父进程数据是共享的,在它调用exec

或exit之后父进程才可能被调度运行。

3. vfork()保证子进程先运行,在她调用exec或exit之后父进程才可能被调度运行。如果在

调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。

4.当需要改变共享数据段中变量的值,则拷贝父进程。

其中调用了copy_process函数:
其中创建进程指针struct task_struct *p;

复制当前的task_struct
初始化进程数据结构,将进程状态设置为TASK_RUNNING

/* Perform scheduler related setup. Assign this task to a CPU. */
	retval = sched_fork(clone_flags, p);


初始化子进程内核栈(关键)

retval = copy_thread_tls(clone_flags, args->stack, args->stack_size, p,
				 args->tls);


最后设置子进程ID

p->pid = pid_nr(pid);

进入copy_thread_tls函数内部

int copy_thread_tls(unsigned long clone_flags, unsigned long sp,
		unsigned long arg, struct task_struct *p, unsigned long tls)
{
	int err;
	struct pt_regs *childregs;
	struct fork_frame *fork_frame;
	struct inactive_task_frame *frame;
	struct task_struct *me = current;

	childregs = task_pt_regs(p);
	fork_frame = container_of(childregs, struct fork_frame, regs);
	frame = &fork_frame->frame;

	frame->bp = 0;
    //子进程返回地址 设置为ret_from_fork,因此子进程从ret_from_fork开始执行
	frame->ret_addr = (unsigned long) ret_from_fork;
	p->thread.sp = (unsigned long) fork_frame;//中断上下文复制
	p->thread.io_bitmap_ptr = NULL;

	savesegment(gs, p->thread.gsindex);
	p->thread.gsbase = p->thread.gsindex ? 0 : me->thread.gsbase;
	savesegment(fs, p->thread.fsindex);
	p->thread.fsbase = p->thread.fsindex ? 0 : me->thread.fsbase;
	savesegment(es, p->thread.es);
	savesegment(ds, p->thread.ds);
	memset(p->thread.ptrace_bps, 0, sizeof(p->thread.ptrace_bps));

	if (unlikely(p->flags & PF_KTHREAD)) {
		/* kernel thread */
		memset(childregs, 0, sizeof(struct pt_regs));
		frame->bx = sp;		/* function */
		frame->r12 = arg;
		return 0;
	}
	frame->bx = 0;
	*childregs = *current_pt_regs();
    //子进程 eax 置 0,因此fork 在子进程返回0
	childregs->ax = 0;
	if (sp)
		childregs->sp = sp;

	err = -ENOMEM;
	if (unlikely(test_tsk_thread_flag(me, TIF_IO_BITMAP))) {
		p->thread.io_bitmap_ptr = kmemdup(me->thread.io_bitmap_ptr,
						  IO_BITMAP_BYTES, GFP_KERNEL);
		if (!p->thread.io_bitmap_ptr) {
			p->thread.io_bitmap_max = 0;
			return -ENOMEM;
		}
		set_tsk_thread_flag(p, TIF_IO_BITMAP);
	}

	/*
	 * Set a new TLS for the child thread?
	 */
	if (clone_flags & CLONE_SETTLS) {
#ifdef CONFIG_IA32_EMULATION
		if (in_ia32_syscall())
			err = do_set_thread_area(p, -1,
				(struct user_desc __user *)tls, 0);
		else
#endif
			err = do_arch_prctl_64(p, ARCH_SET_FS, tls);
		if (err)
			goto out;
	}
	err = 0;
out:
	if (err && p->thread.io_bitmap_ptr) {
		kfree(p->thread.io_bitmap_ptr);
		p->thread.io_bitmap_max = 0;
	}

	return err;
}

由0号进程创建1号进程(内核态),1号内核线程负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟主存管理的内核线程。kernel_init函数将完成设备驱动程序的初始化,并调用run_init_process函数启动用户空间的init进程。

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)\n",
		       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.");
}

在Linux系统中,run_init_process(const char *init_filename)函数是在内核启动过程中调用的一个函数,它负责执行系统的第一个用户空间进程——init进程。

run_init_process()函数会根据参数init_filename指定的程序路径和名称,加载并执行该程序,该程序就是init进程的主程序。在加载和执行init进程的主程序之前,它会关闭当前进程的所有文件描述符,以便在用户空间中进行运行。同时,它将指定init进程的PID设置为1,设置init进程的进程组和会话ID,然后执行init进程的主程序。

1号进程通过execve执行init程序来进入用户空间,后,1号进程调用do_execve运行可执行程序init,并演变成用户态1号进程,即init进程。

在rest_init函数中同样调用了

pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

创建kthreadd进程,具体分析过程与kernel_init进程类似。

学号后三号240

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值