• 2020-2021-1 20209317《Linux内核原理与分析》第四周作业


    《linux内核原理与分析》第三周作业

    一、实验相关

    实验内容

    使用qemu虚拟机运行内核,并从内核入口start_kernel开始单步分析start_kernel函数执行过程
    实验楼环境一直崩溃,所以改为使用自己的虚拟机。

    下载源码并解压编译

    mkdir LinuxKernel
    cd LinuxKernel
    wget https://www.kernel.org/pub/linux/kernel/v3.x/linux-3.18.6.tar.xz
    xz -d linux-3.18.6.tar.xz
    tar -xvf linux-3.18.6.tar
    cd linux-3.18.6
    make i386_defcongig
    make
    

    编译时出现书中提到gcc编译器版本问题,问题原因在于现在的linux编译器gcc版本过高,解决方法是将内核中存在的头文件重命名为所需的文件。

    制作根文件系统

    mkdir rootfs
    git clone https://github.com/mengning/menu.git
    cd menu
    gcc -pthread -o init linktable.c menu.c test.c -m32 -static
    cd ../rootfs
    cp ../menu/init ./
    find . | cpio -o -Hnewc |gzip -9 > ../rootfs.img
    

    对内核进行跟踪调试

    使用sudo apt-get install qemu命令下载安装qemu虚拟机,进行书中配置编译Linux内核操作,执行以下操作

    make menuconfig
    make
    

    执行该命令时,发现目录中并不存在menuconfig文件,经查阅资料后了解到menuconfig是基于文本选择的配置界面,字符终端下推荐使用,进行该命令后会出现如下图

    该窗口是为了配置内核编译的选项,同时出于好奇,查阅了相关资料,该窗口是由cripts/kconfig目录下面的配置文件生成,该目录下比较重要的文件有mconf.c和lxdialog目录,使用cat命令查看后其中存放的为编译时的配置文件。
    按照书中的操作选择编译时携带调试信息,为了后面使用gdb进行调试,之后进行make操作
    后面进行调试时发现由于linux版本问题和qemu版本问题,进行该操作之后并不能添加调试信息,解决方法是在制作根文件系统和镜像的时候,使用

    gcc -pthread -o init linktable.c menu.c test.c -m32 -static -g
    

    提前将调试信息加入到镜像中,后面惊醒gdb单步查看时就能够查看到调试的符号表信息了。

    跟踪调试Linux内核启动过程

    首先在配置好的qemu虚拟机中启动内核,使用以下命令

    qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img -S -s
    

    其中-S是为了将CPU在初始化之前将其冻结起来,-s是在1234端口上创建了一个gdb-server,方便通过另一个窗口使用gdb把带有符号表的内核镜像加载,然后连接gdbserver,并进行调试。效果图如下:

    打开另一个窗口使用gdb,使用如下命令

    file linux-3.18.6/vmlinux
    target remote:1234
    break start_kernel
    

    加载内核并通过端口1234进行调试,在内核开始的start_kernel函数入口处设置断点,如下图:

    之后开始单步执行,如下图:

    分析:该图为start_kernel函数执行的第一行,改行初始化了一个数据结构init_task,该数据结构类型是PCB,即该行代码的作用为初始化第一个PCB,该PCB指向了内核的0号进程即init_task(),该进程初始化了系统的所有进程和线程,在内核初始化完成之后就会演变为idle进程,在后面内核调度进程中发挥作用。
    继续执行,如下图:

    分析:接下来按行分析各个函数的作用,主要还是通过搜索各种资源,了解每个函数的作用。
    smp_setup_processor_id(),这个函数主要作用是获取当前正在执行初始化的处理器ID,
    debug_objects_early_init(),这个函数主要作用是对调试对象进行早期的初始化,其实就是HASH锁和静态对象池进行初始化。
    boot_init_stack_canary(),初始化stack_canary,用来防止栈溢出攻击保护的堆栈。
    cgroup_init_early(),这个函数主要作用是控制组进行早期的初始化。控制组的作用是为了给进程分配不同的cpu、IO等资源。
    local_irq_disable(),这个函数主要作用是关闭当前CPU的所有中断响应,方便后面对各类资源初始化的时候屏蔽多余中断。
    early_boot_irqs_off(),这个函数主要作用是标记内核还在早期初始化代码阶段,并且中断在关闭状态,如果有任何中断打开或请求中断的事情出现,都是会提出警告,以便跟踪代码错误情况。
    boot_cpu_init(),这个函数主要作用是设置当前引导系统的CPU在物理上存在,在逻辑上可以使用,并且初始化准备好。
    page_address_init(),这个函数主要作用是初始化高端内存的映射表。
    pr_notice("%s", linux_banner),这行代码主要作用是在输出终端上显示版本信息、编译的电脑用户名称、编译器版本、编译时间。
    接下来继续单步执行,调试代码和结果如下图:



    分析:从输出版本之后,start_kernel函数进行的操作就是进行内核架构、页表、内存、命令行、以及各种参数的初始化,这些可以暂时不用详细了解,其中需要我们了解的是:
    vfs_caches_init_early(),这个函数是虚拟文件系统的缓存初始化
    mm_init(),这个函数是标记那些内存可以使用,并且告诉系统有多少内存可以使用,当然是除了内核使用的内存以外。
    sched_init(),这个函数主要作用是对进程调度器进行初始化,比如分配调度器占用的内存,初始化任务队列,设置当前任务的空线程,当前任务的调度策略为CFS调度器
    init_IRQ(),这个函数是初始化中断相关的工作,主要初始化中断描述数组,然后调用每个CPU架构中断初始化。
    softirq_init(),这个函数是初始化软件中断,软件中断与硬件中断区别就是中断发生时,软件中断是使用线程来监视中断信号,而硬件中断是使用CPU硬件来监视中断。
    console_init(),这个函数是用来初始化控制台,从这个函数之后就可以输出内容到控制台了。如下图,可以看到qemu虚拟机中出现了控制台启用的信息:

    接下来进行的操作就是继续进行各种初始化,不过在控制台初始化之后,相应的信息都会在qemu虚拟机的界面上显示出来,看起来就很直观了。我们继续单步执行:

    执行到最后一步rest_init()函数,这个函数会进行后续的初始化工作,并开始正式执行内核进程和用户级进程。

    实验收获

    通过该次实验,单步执行linux内核的入口函数,对内核的初始化有了个大概的了解。其中我觉得比较重要的是,了解到了Linux系统0号进程,1号进程,2号进程的产生过程以及他们分别的作用。
    0号进程来源于init_task()函数,该函数会演变为idle进程,是唯一一个没有通过fork()产生的进程,该进程的作用是当没有其它进程能够执行时,会调度执行idle。在调度器发现执行队列为空的时候执行,将其调入执行。
    1号进程来源于kernel_init()1号内核线程,该进程由0号进程创建,是内核启动的第一个用户态进程,该进程是其他所有用户进程的祖先进程
    2号进程是kthreadd()2号内河县城,始终运行在内核空间,是所有内核态其他守护线程的父线程。

  • 相关阅读:
    数组的typedef 和函数的typedef
    函数返回值当左值的问题
    C++中虚析构函数的作用
    word2013密钥
    子类父类步长问题
    函数重定义——重写———重载
    C++的成员初始化列表和构造函数体(以前未知)
    常引用
    项目开发中的字符串模型
    指针函数的++(极易犯错误)
  • 原文地址:https://www.cnblogs.com/lms20209317/p/13888463.html
Copyright © 2020-2023  润新知