内核启动流程分析----如何启动内核2

整理:

arch/arm/kernel/head.s ( 1.判断是否支持这个CPU(机器ID)2.判断是否支持这个单板 3.建立页表,启动MMU 4.跳到start_kernel)
---->start_kernel
---->start_arch #1.解析uboot传入的参数
---->start_command_line #解析uboot传入的参数
---->rest_init
---->kernel_init
---->prepare_namespace
---->mount_rootfs #2.安装根文件系统
---->init_post
#3. 启动initramfs程序,也就是init程序,如: /sbin/init ;/etc/init; /bin/init/ ; /bin/sh

然后,加载真正的文件系统,启动应用程序,内核的最终目的:运行应用程序。

1 第一阶段
通过之前对uboot的分析我们知道uboot启动内核的时候执行的是下面的函数

 

 

 

其中theKernel就是内核的入口地址,然后传进去了三个参数,那么我们的内核刚开始肯定是处理这三个参数。

内核执行的第一个文件是arch/arm/kernel/head.s。

 

 

 

我们前面说uboot给内核传进来了三个参数,其中第二个参数是板子id,上面代码中__lookup_machine_type函数就是看一下内核是否支持这个单板,我们看一下__lookup_machine_type这个函数。

 

我们uboot在调用theKernel传给内核参数的时候,机器ID是第二个参数因此是保存在r1里面的,接下来我们来分析下__lookup_machine_type这个函数,这个函数的定义在kernel\head-common.S文件中。

addr r3 3b,意思是r3等于地址3b,这个地址是物理地址,3b的地址在

 

 

 

然后ldmia r3 ,{r4,r5,r6},那么r4 = ".",这个.表示3b这行代码的虚拟地址, r5 = __arch_info_begin, r6 = __arch_info_end。

然后sub r3, r3, r4这里把r3和r4相减,也就是物理地址减去偏移地址。

然后add r5,r5,r3这里r5加上偏移就等于__arch_info_begin真正的物理地址。

然后add r6,r6,r3这里r5加上偏移就等于__arch_info_end真正的物理地址。

__arch_info_begin和__arch_info_end是在链接脚本中定义的,

 

 

 

__arch_info_begin和__arch_info_end中间的是所有文件的.arch.info.init段。这个段存放的是架构相关的初始化信息,那么内核中的哪些代码是存放在这个段里面的呢,我们用grep ".arch.info.init" * -nR在内核中搜索一下。

 

 

 

我们去arch.h的53行看一下

 

 

 

注意上面是两个宏,一个是MACHINE_START,然后另一个是MACHINE_END定义成};然后我们找一下MACHINE_START这个宏是谁在使用,我们找到Mach-s3c2440.c

 

 

 

然后我们把这个宏展开,根据我们之前分析uboot时的经验可以知道,这几个宏合起来其实就是定义了一个结构体,然后把这个结构体的段属性设置为.arch.info.init,宏展开后如下

 

 

 

这里就相当于是定义了一个machine_desc结构体,然后我们看下这个结构体

 

 

 

我们看到这个结构体里面有架构number,还有boot_aprams,所以我们的uboot传进来的参数就刚好和我们这个结构体对应起来了,然后这个结构体在定义时增加了一个段属性,放在.arch.info.init段,然后我们内核启动的时候会从 __arch_info_begin读到__arch_info_end,然后依次把内容取出来,把ID和uboot传进来的ID进行比较,如果吻合就表示内核支持这个单板,

 

 

 

比较完之后又回到了

 

 

 

然后又跳转到了__create_page_tables创建页表启动MMU, 因为我们从链接脚本中可以看到我们的内核链接地址是0xc0000000+0x00008000,这个地址并不对应真实存在的内存,我们的内存是从0x30000000开始的,所以要建立页表,启动MMU。

 

 

 

然后启动mmu,然后跳转到__switch_data,这个函数的定义在kernel\head-common.S文件中,这个__switch_data细节就不看了,在这个函数的最后是跳转到start_kernel

 

 

 

这个start_kernel就是我们内核的第一个C函数,这个函数是在 init\main.c里面。

2 第二阶段

 

 


做了一系列的初始化工作,

 

 

 

然后printk(linux_banner)是打印内核版本信息,然后setup_arch和setup_command_line这两个函数用来处理uboot传过来的参数,

 

 

 

然后又做了很多初始化工作。我们内核的最终目的是要运行应用程序,而应用程序是在根文件系统上的,所以我们的内核首先还要挂接跟文件系统,我们看上面截图最后有个rest_init函数,这个函数里面又调用了别的函数,一层层的调用关系是

 

 

 

下面我们看一下我们uboot传入了哪些参数,

我们挂载跟文件系统的时候,我们要挂接到那个根文件系统上面去呢,比如我们在windows的时候,我们有C D E F盘,那我们要识别哪一个盘,要告诉内核,我们uboot传入的bootargs里面写的root=/dev/mtdblock3,就是指根文件系统在第四个分区上面,那么我们在挂接根文件系统的时候肯定要处理root=/dev/mtdblock3这个参数,

 

 

 

我们再prepare_namespace函数中发现了saved_root_name数组,我们看一下这个数组在哪里定义的,

 

 

 

这里有一个函数root_dev_setup,还有一个宏__setup,我们大胆猜测一下,当解析命令行参数的时候,发现了root=xxx,然后就会以root=找到root_dev_setup这个函数,然后调用这个函数,这个函数又会去把root=后面的内容保存到saved_root_name这个变量里面,接下来我们分析下是不是这样的,我们先看一下__setup这个宏

 

 

 

然后__setup_param的定义也在上面的截图中,

 

 

 

我们把__setup("root=", root_dev_setup)这个宏展开,会发现就是相当于定义了一个结构体,这个结构体有三个成员,分别是字符串,函数,early。它的属性是.init.setup,.init.setup在链接脚本中会用到。

 

 

 

然后我们在代码中搜索一下这个__setup_start和__setup_end是被谁使用的,也就知道命令行是怎么调用的了,

 

 

 

我们把前面的调用关系更新为如下的调用关系

 

 

 

上面说的early函数和非early函数是指前面宏定义的时候early是0还是1,是1就是early函数。

接下来我们再回顾一个问题,我们的参数root="/dev/mtdblock3",我们之前说过我们的flash是没有分区表的,只能通过地址写死的方式规定,那写在哪里呢,假如我们不知道这块代码在哪里,我们有什么办法吗,我们用boot命令把内核启动起来,然后内核启动的过程中会把这些分区打印出来,我们直接搜索分区名字就可以了,

 

 

 

例如我们搜索bootloader,

grep "\"bootloader\"" * -nR

 

 

 


我们从搜索结果中找一个最像的

 

然后我们去找代码

 

 

 

3 总结
从前面的分析我们知道内核启动主要有下面内容。

 

 



posted @ 2022-09-14 10:39  DMCF  阅读(204)  评论(0编辑  收藏  举报