实验环境配置
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函数完成了系统的初始化和启动,将系统从裸机状态变成了可用状态。大致流程如下:
-
初始化内核代码段和数据段。这个过程包括将内核镜像从只读状态转变为可读写状态,建立IDT和GDT表,进入保护模式等。
-
初始化物理内存管理器。这个过程将系统中所有的物理内存(包括CPU的缓存区和设备地址空间等)按页大小分割,并建立页表,使得内核可以对这些内存进行读写。
-
初始化虚拟内存管理器。这个过程将系统的虚拟地址空间映射到物理内存,将内存地址空间划分为内核空间和用户空间。
-
初始化进程调度器。这个过程建立进程控制块(PCB)和任务队列,并启动第一个进程。
-
安装各种外部设备驱动程序(如硬盘、网卡、USB等),并进行初始化。
-
在文件系统中查找并挂载根文件系统。
-
从根文件系统中读取和加载各种用户空间应用程序和库。
-
启动系统初始化脚本,执行各种服务初始化和配置操作,如启动各种守护进程、网络配置、挂载磁盘等。
-
用户态启动完毕后,控制权交回内核,执行主循环处理各种系统调用等内核操作。
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