前言
无论是linux还是uboot,首先首先要看的就是其配置文件,而linux各种配置文件,例如xxx_defconfig、make menuconfig(Kconfig)、.config等,最终最终都是服务于makefile的。
先看配置文件吧,且看下面这个图,网课截的
总结起来就是,配置文件要么在架构-平台的configs文件夹里,找一个和你的板子相近的xxx_defconfig来作为蓝本,以此为基础进行修改,要么就用开发板厂家提供的xxx_defconfig或者xxx_config覆盖.config。
上面xxx_defconfig和.config的关系就是,make xxx_defconfig之后,.config就会有xxx_defconfig提及的配置项。
而make menuconfig配置完后,也是改变了.config这个配置文件。
.config这个文件,就直接决定了各级makefile有哪些宏被打开。
配置语法合理性及配置示例
好,现在举个例子
以DM9000为例,如果我们要linux内核支持该网卡,该怎么做呢?
首先收索CONFIG_DM9000可以知道哪类文件与其配置相关,且看下图
由图可知,共有圆序号1~4涉及了DM9000的配置,直接说结论,
.config被某些脚本工具解析,生成autoconf.h和auto.conf
autoconf.h用于将宏define为1,使得c语言代码可以识别宏开关
auto.conf用于将将宏的y,m转换为适应makefile语法的y,m
到了这里宏的配置同c的语法和makefile语法的关系我们就自圆其说了。
实际上我们日常配置只需要关注和DM9000相关的底层的makefile即可(driver/net/makefile),因为如果我们要的网卡内核没有,那我们移植网卡时,是需要修改这里的(以及底层的Kconfig)。如果是内核提供了的驱动,我们仅仅只需要在make menuconfig里将其配置项选上即可。
知道了配置文件是makefile了,现在该去分析它了
分析makefile
可以得知-- -->第一个文件(顺藤摸瓜了解启动流程),链接脚本(内核放在哪个地址,代码段数据段等等是如何排布的)
下图是makefile的类型,可作工具查阅
下面挑一些makefile中最核心的语句
首先第一个要点,顶层makefile第一个包含的makefile是
架构的makefile
这个呢一般都是固定的,例如arm在linux内核中有一些必要配置,x86有一些必要的配置,它们会根据ARCH来找到对应的架构makefile将相关的必要的配置项选上。
然后我们知道我们编译出内核镜像是zImage或者uImage
这个在makefile中肯定是最终的目标文件,我们需要顺藤摸瓜查一查它的依赖是什么
可见,内核镜像依赖于vmlinux,所以经常听到网课啊或者博客人家说vmlinux是真正的内核,就是这么个事。
再看这个vmlinux是依赖于什么(从哪些文件的来的)
图片后面有些看不清没关系,下面有文字描述
vmlinux依赖于以下这些,首先是链接脚本vmlinux-lds,然后是vmlinux-init、vmlinux -main
将它们剖析
vmlinux-init是:
vmlinux -main是:
也就是说,vmlinux是由head-y,init-y等等这些个变量里面,保存的.o对应的源文件构成的。我们要清楚,make编译的时候,编译的顺序就是以上这些.o排放的顺序,因此因此,我们看第一个程序是啥?从vmlinux-init我们可以得知,第一个程序是head-y变量里存放的head.S。
知道了原材料(依赖),下面再看怎么炒菜(编译指令)
上图是紧跟着vmlinux依赖的编译指令,分析起来太复杂了,直接看make uImage v=1的结果,里面就有这段makefile脚本展开的语句,上面的v=1的意思是让编译过程的打印信息更详细的意思。
上图就是,编译指令的展开,看不清楚没关系,下面有文字的解释
首先用arm-linux-ld工具进行链接
-o指定输出的是vmlinux
-T 指定链接脚本vmlinux.lds,即设置程序段"应该"在哪个地址(可以浏览一下,不过表面是看不出什么的,得后面根据源码来分析才知道哪些段是干嘛的)
指定连接脚本之后就是指定一堆原材料,原材料就是一堆的.o目标文件()
下面简单浏览一下链接脚本
链接脚本:vmlinux.lds
在没有编译过的源码里grep就知道,它是由vmlinux.lds.S的来的,是/arch/arm/kernel里的代码
.指令内核放在哪里(虚拟地址)
首先放所有文件的.text.head段,这所谓“所有文件”的.text.head段放置的顺序由链接时候.o的顺序决定
然后就是.init.text段 ...等等
确实看不出有什么帮助,可能只有开头的那个点有一些初步价值。
至此,由Makefile我们知道了,程序执行的第一个文件是head.S还有链接脚本
网课总结到这张图里了,接下来分析内核启动
分析uboot和内核启动之间的这个过程
uboot是负责启动linux内核的,先看一下uboot在最后的阶段做了什么事情:
uboot的最后一条代码是:
uboot在某个地址写了环境变量,然后设置r0(0) r1(机器ID) r2(环境变量地址) 然后启动内核
theKernel是函数指针,其地址是内核在内存中的地址,展开细说就是下图圈起来的部分
而简单的说就是,uboot在某个地址写了一堆的启动信息,然后调用的最后一条函数,这个函数呢,是个指针函数,其地址就是kernel程序的加载地址。这个函数的参数分别对应着r0,r1,r2,r1和r2又分别存在机器ID和启动信息的地址。
重点说一下这个指针函数,我们说它的地址就是kernel的加载地址,举个例子就明白了,我们的uboot,都要设置一个bootcmd,它是uboot加载模式下(你没按按键的时候)它自动执行的指令,一般是这样的形式(不同板子块设备读写指令不一样,但是意思是一个意思)
bootcmd 'movi read kernel 40008000;movi read rootfs 40df0000 100000;bootm 40008000 40df0000'
意思是将内核从固态存储设备加载到0x4008000的地址处,这个0x4008000就是uboot设置的指针函数的地址,重点来了,要让linux内核成功地被uboot启动,在编译linux内核时,也要指定其加载地址与uboot设置的指针函数地址一致,否则可能会启动失败。
怎么办呢?其实很简单,因为编译uImage时是可以指定的,例如:
make uImage LOADADDR=0x40007000
这样uboot和uImage一起约定好的地址就不会有错了。
下面继续分析内核启动流程
上面我们说到,uboot将机器ID,启动参数传到了uboot的最后一条函数,当执行这最后一条函数的时候,pc指针也就指向了这个指针函数所在的地址,然后去指向其代码了,我们很容易理解,其实这个地方,就是linux内核的代码所在了,uboot这个逻辑程序的任务也就此完结,后面都是linux的工作了。
跳转过来的地方,当然就是前面所说的linux的第一个程序head.S啦,它是一个汇编程序
或许有人会疑惑,这是传统的启动方式,现在有了设备树,又是怎么回事呢?其实也很简单,设备树在uboot加载命令bootcmd中被加载到了内存的某个地址,uboot完全可以在启动信息中,告诉内核设备树在哪里,这样内核就能够将设备树展开并获取里面的信息了。实际上,设备树的加入,对原来的内核其实还是很兼容的,许多许多地方是不变的。新的内核设备树展开的部分在本文不会提到,后续应该会再出个帖子分析新的内核,这个帖子只是记录网课学习,涉及的只有没有设备树版本的内核。
分析内核第一个程序head.S
总的来说是先处理传过来的参数:判断是否支持该cpu,判断是否支持该机器id
创建页表的原因是链接地址0xc000000(虚拟)开始的,而物理内存(SDRAM/DDR)是从0x30000000(根据自己的板子)开始的,所以要建一个页表以便后面启动MMU。
在这里先插入详细分析一下__lookup_machine_type这个汇编函数,再往下继续
如果想保持内核启动源码理解的连续性,可以跳过这一小节,因为这个函数内部是比较复杂的,看完思路都飞走了。
分析__lookup_machine_type
这个函数的作用是检测机器ID,说白了就是看linux支不支持这个soc,从编程的角度,无非就是一个数字放到一个数表里比较一下,数表里有的就支持,没有的就不支持,来看看是怎么写的。
先了解一下相关ARM汇编语法
ldmia指令 连续加载内容到寄存器
示例:ldmia r1 ,[r2,r3,r4] 将r1地址开始的内容放到r2,r3,r4中
adr指令
示例:adr r3 , 3b 将3b标签的地址赋给r3
好,来看代码
再次铺垫一些知识:
看上面那个红框
3: 这是一个标签,可以理解为它这个东东就代表了一个地址
3: .long .
这个.long后面的这个 “.” 点这个东西表示的是程序应该处于的地址,是虚拟地址。细品这句话
__arch_info_begin和__arch_info_end这连个东西的值,也是虚拟地址
但是adr r3,3b 里面的这个3b是物理地址
凭什么呢?因为当前程序,还没有开启MMU,根本没有所谓的虚拟地址,3b是个标签是随时就会被跳转然后运行的,现在没有开启MMU,它的地址当然就是物理地址啦,而".",__arch_info_begin和__arch_info_end这三个东西,可以理解为是一个写死的常量,里面保存的是虚拟地址也就是常量的值保存着虚拟地址,和当时开没开其MMU半毛钱关系都没有。
接下来再看这张图
真正开始看这个函数里的代码和注释了
注意看第二个红框下面的sub和add语句
那一句sub实际是:同一个位置的物理地址(r3,”3b”这个标签的地址)---同一个位置的虚拟地址(r4,“.”的地址),得到偏移值(是负数)
可见,r5存着的,就是__arch_info_begin的物理地址了,也就是现在没有开启MMU的情况下,pc指针一指过去就会执行的地址。这个__arch_info_begin明显是一个段,里面是什么名堂呢?我们需要结合链接文件和linux源文件来分析(链接文件终于登场了)
vmlinux.lds.S中是这样写的
可以分析一下这个MACHINE_START宏是干了什么,是被谁、在哪里使用
可见,这个宏是被arch/arm/mach-xxx/Mach-xxx.c使用,,从名字和参数我们不难猜出,这个段,大概率是存着SOC的信息,再回想我们一路追寻到这里,最初的目标,就是检测机器ID,所以这个东西,大概率就是某个板级文件调用了这个宏,就能够将机器ID注册到linux内核中,如果uboot传过来的机器ID与linux内核的机器id匹配,这个所谓的检测机器ID就合格了。反过来说,这个段存着很多机器的信息(机器ID,启动参数地址等),这个检测机器ID的函数,其实更像在匹配,我们到底用这个段中那么多板级文件arch/arm/mach-xxx/Mach-xxx.c那么多板级信息的哪一个板级信息。所以机器ID的匹配是至关重要的。
解析这个宏:先说结论
这个宏是用来造结构体的,这个结构体的特殊之处在于能够把结构体的信息保存在.arch.info.init段中。这也解释的通为什么检测机器ID这个函数和链接脚本扯上了关系,因为linux中保存机器ID信息的结构体,它就是存在.arch.info.init段中的。
且看这个宏,展开和具体示例。下面红框就是这个宏的展开的样子,上面绿框是从某个具体的实例截取的。
将示例替换到定义,可得如下这个结构体(也就是说,它这个宏被调用之后,在代码中真正的样子),包含了一系列信息,并且是保存在arch.info.init段中的
里面的".nr"成员,实际就是保存着机器ID
也就是说,如果我们linux要对一个板子新加支持,要在内核新加一个这样包含MACH_TYPE_XXX的结构体(用宏定义),并且这个信息要与Uboot传进来r1的匹配。
检测机器ID这个函数就解释到这里,下面继续linux第一个程序head.s的分析
继续分析head.S(实际上要跳转到start_kernel函数了)
head.S的最后一条函数是然后跳转到start_kernel (内核的第一个c函数,我们早知道,uboot传来的东西中,机器id处理了,启动参数还没处理呢)
由下面这个图可以知道head.S程序和start_kernel函数具体干了什么事情
其实可以理解为start_kernel之后,就一直在做两件事,一是解析uboot传来的环境变量参数,二是初始化一堆“基础设施”,例如终端呀,打印信息之类的,下面细说。
按顺序分析:进入start_kernel之后
具体内核怎么解析呢?
我们先不提,反正我们知道,它从uboot那里拿到了bootargs的内容(即cmdline命令行参数)还有其他一些信息,看下面,就把uboot存在某个地址的tags信息都取了出来。
好,大家肯定有疑惑为什么mdesc->boot_params里面就是启动的信息的地址呢,这其实和上面讲的检测机器ID里的那个段有关,那个 段是
我们说,这个段里面的内容实际是一堆这样的东西:
里面赫然存放着一个启动参数的地址,和我们前面前面提到的,uboot传给指针函数的第三个参数也就是启动参数地址一模一样。其实按我的理解,我们知道从指针函数拿到第三个参数,就可以去取出启动参数那些信息了,为何linux的arch.info.init还要保存一个机器的信息呢?无法理解,可能是为了安全?总之我们得到了启动参数里面的内容。
继续分析start_kernel
后面就是把启动参数tag里面的东西全部取出来存在变量里,以备后用
这些启动参数里,要重点关注的是命令行参数(即uboot的bootargs参数),form是一个地址,里面存放着linux默认的cmdline(make menuconfig配置的)。下面来看这个
cmdline解析函数里,简言之就是把bootargs参数里,root=xxx,这个所谓的xxx取出来,这个xxx是某个分区的名字,和后面要挂载的根文件系统有关。这个参数里还要init=xxx,这个xxx是指某个程序,和后面挂载根文件系统后要运行的第一个程序有关。
这个__setup宏又是一个结构体,里面包含字符串、函数指针等成员
插讲一下内核分区的概念
在flash里没有分区表,那root=/dev/mtdblock3,内核怎么知道呢?内核其实也不知道,是内核在代码里写死了
启动内核时,内核会打印出分区信息
那内核在哪里写死了呢?当然是在板级文件,由我们来写啦,不然怎么会和uboot烧录的位置一样呢。在板级文件中定义了
继续分析start_kernel函数
解析完启动参数后,start_kernel的最后一个函数是rest_init()
先挂一个图,说明start_kernel函数做的事情,以及rest_init()做的事情
rest_init()又是一堆初始化,里面追踪进去是有mount_root即挂接根文件系统的函数,挂接完事后,调用init_post(),打开中断,内核启动的事情就完事了,接下来是执行应用程序了~
如果我们uboot的bootargs参数有指定init=xxx,那么就会执行这个xxx程序,如果没有指定,会执行红框里/sbin/init程序,如果这个程序不存在就继续往下执行,如果都不存在就报错。值得注意的是,这个run_init_process一旦执行成功就不会再返回了。
因此,内核启动流程其实也没多少东西,下面是图总结:
总结: