Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

时间:2022-09-01 14:44:48

姓名:江军

ID:fuchen1994

实验日期:2016.3.13

实验指导

  1. cd LinuxKernel/
  2. qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img

内核启动完成后进入menu程序(《软件工程C编码实践篇》的课程项目),支持三个命令help、version和quit,您也可以添加更多的命令,对选修过《软件工程C编码实践篇》的童鞋应该是a piece of cake.

  • 使用gdb跟踪调试内核

  1. qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
  2. # -S freeze CPU at startup (use ’c’ to start execution)
  3. # -s shorthand for -gdb tcp::1234 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项

另开一个shell窗口

  1. gdb
  2. (gdb)file linux-3.18.6/vmlinux # 在gdb界面中targe remote之前加载符号表
  3. (gdb)target remote:1234 # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
  4. (gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

 

实验要求:

  • 使用gdb跟踪调试内核从start_kernel到init进程启动

  • 详细分析从start_kernel到init进程启动的过程并结合实验截图撰写一篇署名博客,并在博客文章中注明“真实姓名(与最后申请证书的姓名务必一致) + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 ”,博客内容的具体要求如下:

    • 题目自拟,内容围绕Linux内核的启动过程,即从start_kernel到init进程启动;

    • 博客中需要使用实验截图

    • 博客内容中需要仔细分析start_kernel函数的执行过程

    • 总结部分需要阐明自己对“Linux系统启动过程”的理解,尤其是idle进程、1号进程是怎么来的。

  • 3)请提交博客文章URL到网易云课堂MOOC平台Linux内核分析MOOC课程,编辑成一个链接可以直接点击打开。

Linux内核目录:

arch目录包括了所有和体系结构相关的核心代码。它下面的每一个子目录都代表一种Linux支持的体系结构,例如i386就是Intel CPU及与之相兼容体系结构的子目录。PC机一般都基于此目录。

COPYING目录下是GPL版权申明。对具有GPL版权的源代码改动而形成的程序,或使用GPL工具产生的程序,具有使用GPL发表的义务,如公开源代码。

CREDITS目录下是光荣榜。对Linux做出过很大贡献的一些人的信息。

documentation目录下是一些文档,没有内核代码,可惜都是English的,是对每个目录作用的具体说明。

drivers目录中是系统中所有的设备驱动程序。它又进一步划分成几类设备驱动,每一种有对应的子目录,如声卡的驱动对应于drivers/sound; block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看drivers/block/genhd.c中的device_setup()。它不仅初始化硬盘,也初始化,因为安装nfs文件系统的时候需要网络其他: 如, Lib放置核心的库代码; Net,核心与网络相关的代码; Ipc,这个目录包含核心的进程间通讯的代码; Fs,所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统,例如fat和ext2。

fs目录存放Linux支持的文件系统代码和各种类型的文件操作代码。每一个子目录支持一个文件系统,如ext3文件系统对应的就是ext3子目录。

include目录包括编译核心所需要的大部分头文件,例如与平台无关的头文件在include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录。

init目录包含核心的初始化代码(不是系统的引导代码),有main.c和Version.c两个文件。这是研究核心如何工作的好起点。

ipc目录包含了核心进程间的通信代码。

Kernel内核管理的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同时与处理器结构相关代码都放在archlib/目录下。

MAINTAINERS目录存放了维护人员列表,对当前版本的内核各部分都有谁负责。

Makefile目录第一个Makefile文件。用来组织内核的各模块,记录了个模块间的相互这间的联系和依托关系,编译时使用;仔细阅读各子目录下的Makefile文件对弄清各个文件这间的联系和依托关系很有帮助。

mm目录包含了所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等。与具体硬件体系结构相关的内存管理代码位于arch/*/mm目录下,例如arch/i386/mm/Fault.c 。

modules目录存放了已建好的、可动态加载的模块文件目录,是个空目录,用于存放编译时产生的模块目标文件。

net目录里是核心的网络部分代码,其每个子目录对应于网络的一个方面。

ReadMe目录里是核心及其编译配置方法简单介绍

REPORTING-BUGS目录里是有关报告Bug 的一些内容

Rules.make目录里是各种Makefilemake所使用的一些共同规则

scripts目录包含用于配置核心的脚本文件等。

一般在每个目录下都有一个.depend文件和一个Makefile文件。这两个文件都是编译时使用的辅助文件。仔细阅读这两个文件对弄清各个文件之间的联系和依托关系很有帮助。另外有的目录下还有Readme文件,它是对该目录下文件的一些说明,同样有利于对内核源码的理解。

命令说明:

qemu -kernel linux-3.18./arch/x86/boot/bzImage -initrd rootfs.img -s -S # 关于-s和-S选项的说明:
# -S freeze CPU at startup (use ’c’ to start execution) //在CPU启动之前冻结CPU
# -s shorthand for -gdb tcp:: 若不想使用1234端口,则可以使用-gdb tcp:xxxx来取代-s选项 //在TCP1234这个端口上创建了一个gdp 服务
gdb
(gdb)file linux-3.18./vmlinux # 在gdb界面中targe remote之前加载符号表
(gdb)target remote: # 建立gdb和gdbserver之间的连接,按c 让qemu上的Linux继续运行
(gdb)break start_kernel # 断点的设置可以在target remote之前,也可以在之后

实验过程:

第一步:启动并冻结

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

现在我们开始启动gdb服务

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

加载符号表

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

链接gdbserver

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

设置一个断点

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

按c系统开始执行到断点处暂停

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

使用list命令,可以看到start_kernel函数的上下文

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

在rest_init处设置断点

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

Linux内核分析-使用gdb跟踪调试内核从start_kernel到init进程启动

start_kernel()是内核的汇编代码和c代码的交接处,在此之前,全是由汇编代码完成各种环境的初始化工作,包括将内核代码载入内存,设置C运行环境等等。

当运行到start_kernel()的时候,我们可以大致分析如下:

1.手工创建0号进程init_task(),他最终变成idle进程

set_task_stack_end_magic(&init_task);

init_idle()函数会把init_task加入到cpu的运行队列中去,在没有其他进程加入cpu队列的时候,init_task会一直运行,当其他进程加入进来的时候,init_task就会被设置成idle,并使用调度函数将切换到新加入进来的进程上。

2.初始化各个模块

模块如下:内存管理模块  中断 调度模块等等

3.运行到rest_init(),初始化进程

其中rest_init()是内核初始化的最后一步,它也是所有进程的祖先。

下面我们分析这个函数

注意这段代码:

kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);  //初始化第一个用户态进程,也就是1号进程

OK,接下来我们继续分析

static void noinline rest_init(void)
__releases(kernel_lock)
{
kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
unlock_kernel(); /*
424 * The boot idle thread must execute schedule()
425 * at least one to get things moving:
426 */
preempt_enable_no_resched();
schedule();
preempt_disable(); /* Call into cpu_idle with preempt disabled */
cpu_idle();
}

在这段代码中,我们只需要分析

kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
cpu_idle();  //cpu队列进程的切换,将0号进程设置idle

这两句就行了,其他的可以暂时忽略

kernel_thread中传入的函数init需要进行分析一下,我们截取部分代码如下:

run_init_process("/sbin/init");           run_init_process()实际上是通过嵌入汇编构建一个类似用户态代码
run_init_process("/etc/init");             一样的 sys_execve()调用,其参数就是要执行的可执行文件名,也就
run_init_process("/bin/init");             是这里的 init process 在磁盘上的文件。
run_init_process("/bin/sh");

因为实验楼的烂环境老是卡,所以我们就不截图分析了。

在run_init_procrss()处断点调试,run_init_process 就是通过 execve()来运行 init 程序。

到这里idle_task()的任务完成; 将会被调度函数设置为空闲的进程。

总结:Linux在start_kernel执行之前都是汇编代码,在他执行后,各种环境初始化后,执行c代码。0号进程是作者手工创建的,它的任务就是在CPU的队列中没有进程的时候一直执行,在有进程的时候切换到新进程,而后被设置为空闲状态。start_kernel最后一部分是第一个用户态进程PID=1的正式生成,就是rest_init(),这个进程是系统的1号进程,这个时候0号进程会被设置成idle进程。1号进程执行,生成系统所需的所有进程,其实就是调用了run_init_process()函数加载文件,生成进程