Linux内核初探
在uboot学习的时候, 我们知道了一个庞大的程序,感觉无从下手,但其实,通过韦老师和一些老手的经验告诉我,如果我们不是专门弄uboot的,一般只用知道怎么用就行了。确实这个东西太大了而且花那么多时间去弄这个也不值得。同理,uboot的终极奥义是启动内核,现在uboot的简单应用我们已经会了,内核是一个比uboot更加庞大的家伙,只是2440相关的文件都1w多个,要想一接触就弄清楚每一步是干什么的也几乎不可能,但是内核和uboot不同,里面需要细细研究和借鉴的东西很多,只是刚开始,我们还是跟着师傅学一点皮毛,能跑上应用程序再说。显而易见的是现在没有什么教程能教会你linux内核的全部知识,除非linus来教你,^_^。
内核操作方式如同uboot一样,依旧先下载内核源码,然后解压缩,然后打补丁。接着就是配置了。
以下部分内容借鉴韦老师一学员的笔记,我太懒了,有现成的决不自己来。
搜索到了目录部分截图如下:
然后我们到arm构架下查看有没有我们熟悉的配置项,结果是有的,2410:
现在执行make s3c2410_defconfig报错如下:
Makefile:416: *** mixed implicit and normal rules: deprecated syntax
Makefile:1449: *** mixed implicit and normal rules: deprecated syntax
make: *** No rule to make target 'menuconfig'. Stop
因为我的ubuntu是16.04新版的make编译器:
新版Makefile不支持这样的组合目标:config %config(一个有通配符,另一个没有通配符)
解决方法:
修改linux-2.6.22.6 顶层Makefile 416行:
config %config: scripts_basic outputmakefile FORCE
改为:
%config: scripts_basic outputmakefile FORCE
修改linux-2.6.22.6 顶层 Makefile 1449行:
/ %/: prepare scripts FORCE
改为:
%/: prepare scripts FORCE
可以看到生成了.config:
然后make menuconfig,这个会去调用刚才生成的.config
像上面内核配置的界面,高亮显示的第一个字母是这个选项的快捷键(不区分大小写),按下 / 建进入搜索模式,具体介绍上面图片的英文有说明。
上面是默认配置,和默认配置同等级别或者更高级别的是使用厂家提供的config文件,这里使用韦老师的config_ok文件试试:
使用厂家的配置文件覆盖了.config之后,执行make menuconfig:
如果根文件系统没有下载到内存,启动内核的时候的会失败。
等于m代表的是编译或模块。看看.config中网卡DM9000的配置:
我们在内核文件全局搜索一下:grep -w选项表示精确查找
需要注意的是,能搜素到DM9000是我们要执行make或者make uImage之后,否则搜索不到哦。
可以看看autocnf.h看看自动配置头文件。
还有 Makefile 中有:
drivers\net\Makefile
#obj-$(CONFIG_DM9000) += dm9000.o
#obj-$(CONFIG_DM9000) += dm9ks.o
则:
a. C 源代码中用到 CONFIG_DM9000.从 C 语言语法看肯定是个宏。宏只能在 C 文件或是
头文件中定义。
依照这里的情况,应该是在“include/linux/autoconf.h ”这个头文件中定义。
b. 子目录下的 Makefile 中有 CONFIG_DM9000 配置项(如 drivers/net/Makefile)
c. include/config/auto.conf 中有。
d. include/linux/autoconf.h 中有。从 autoconf.h 可猜测这个文件是自动生成的。它里面的内
容来源于
make 内核时, make 机制会自动根据生成的 “.config” 配置文件,生成 autconf.h 这个文
件。
在 autoconf.h 中搜索 DM9000 得到: #define CONFIG_DM9000 1
可见 CONFIG_DM9000 被定义成一个宏,为“1”。
整个文件 autoconfi.h 中的宏基本都是定义为“1”,就是说不管在 .config 配置文件中,
配置项=y,还是=m,
在这个由 .config 生成的头文件 autoconf.h 中都被定义成“1”。
C 语言源码中就只使用这些 宏 了。这些等于 y 等于 m 的在 autoconf.h 中定义宏时都定
义为“1”,它们的区别则在
使用这些宏的 C 语言中体现不出来了。这些区别是在 Makefile 中定义。看子目录下的
Makefile.
看子目录 dervice/net/Makefile 文件。搜索其中 CONFIG_DM9000 文件。
对于内核的 Makefile,它的子目录的 Makefie 很简单。
2. 内核子目录 Makefile:
1.格式比较简单:内核子目录下的 Makefile 中
obj-y += xxx.o
xxx.c 的文件最后会被编译进内核中去。
obj-m += yyy.o
yyy.c 文件最后会编译成 可加载 的模块 yyy.ko 。
这两种格式。
如下示例:
obj-$(CONFIG_DM9000) += dm9000.o
意思:若 CONFIG_DM9000 这个变量被定义为 y 的话,这个 dm9000.c 就会被编译进内核
中去。
若这个配 置 项 CONFIG_DM9000 被 定义为 m 的话 ,这 个 dm9000.c 会被编译成
dm9000.ko 模块
y 和 m 的区别就是在内核的子目录下的 Makefile 中体现的。子目录下的 Makefile
中的 CONFIG_DM9000 是谁来定义的呢?
也是由其他来定义的。由 "include/config/auto.conf " 来定义(这个文件也是来源于 make
内核时的 .config)。从 auto.conf
可知它也是自动生成的。 里面的内核和 .config 文件很像。其中的项要么等于 y,要么等于
m,或其他值。
显然这个 include/config/auto.conf 文件是会被别人包含进去。(被顶层的 Makefile 包含)
配置内核时生成了 .config 文件。然后 make menuconfig 或是 make uImage 时:
1, .config 被自动来创建生成了一个 include/linux/autoconfig.h 文件 .这个头文件被 C 源
代码去对应里面的配置。
2, .config 也被自动来生成一个 include/config/auto.conf 文件。这个文件由顶层 Makefile
来包含。由子目录下的 Makefile 来用它。
三、 内核 Makefile
分析 Makefile:找到第一个目标文件和链接文件。
对于 Makefile 的文档在 Documentation\kbuild 下的 makefiles.txt 对内核的 makefile 讲的很透彻。
子目录下的 Makefile 很简单,就只有几条格式:
obj-y += a.o b.o
obj-m += a.o
2. 架构相关的 Makefile 。(arch/$(ARCH)/Makefile)
分析一个 Makefile 时,从它的命令开始分析。编译内核时是直接 make 或 make uImage
从顶层 Makefile 一直往下走时会涉及到所有的东西。
<1> make uImage 时这个目标 uImage 不在顶层的 Makefile 中,在 arch/arm/Makefile 中
定义了这个目标。
我们是在顶层目录 make uImage 的,则可知顶层 Makefile 会包含 arch/arm/Makefile 。
3. 顶层目录的 Makefile.
从 make uImage 命令往下分析。
<1> 目标 uImage 定义在 arch/arm/Makefile 中,找到 uImage 目标所在行,查看它相关
的依赖。
在顶层目录直接输入 make ,默认就是执行第一个目标, "all"就是第一个目标。这个目标也
是依赖于 vmlinux 。即都是要先生成 vmlinux .
init-y := $(patsubst %/, %/built-in.o, $(init-y))
这是一个 Makefile 的函数。
%/ 代表的是 init/目录下的所有文件。
%/built-in.o 相当于在 init/下的文件全部编译成 built-in.o 。
这个函数的意思是: init-y := $(patsubst %/, %/built-in.o, $(init-y)) = init/built-in.o
即 init-y 等于 init 目录下所有涉及的那些文件,这些文件会被编译成一个 built-in.o
patsubst :替换通配符。
在$(patsubst %.c, %.o, $(dir) )中, patsubst 把$(dir)中的变量符合后缀是.c 的全部替换成.o,
任何输出。
或者可以使用
obj=$(dir:%.c=%.o)
效果也是一样的。
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
core-y :核心 libs-y:库 drivers-y:驱动 net-y:网络
在 Makefile 中搜索 core-y 有如下依赖:
core-y := usr/
core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/
core-y := $(patsubst %/, %/built-in.o, $(core-y))
意思是最后 core-y = usr/built-in.o
+= kernel/built-in.o
+= mm/built-in.o
+= fs/built-in.o
+= ipc/built-in.o
+= security/built-in.o
+= crypto/built-in.o
+= block/built-in.o
就是将这些目录(usr、 kernel、 mm、 fs、 ipc、 security、 crypto、 block)下涉及的文件分别
编译成 built-in.o
不是所有文件,而是涉及到的文件。
libs-y : 依赖
libs-y1 := $(patsubst %/, %/lib.a, $(libs-y))
libs-y2 := $(patsubst %/, %/built-in.o, $(libs-y))
libs-y := $(libs-y1) $(libs-y2)
最后 libs-y = lib/lib.a
+= lib/built-in.o
drivers-y : 驱动
drivers-y := drivers/ sound/ (依赖了这两个目录)
drivers-y := $(patsubst %/, %/built-in.o, $(drivers-y))
意思是最后 drivers-y = drivers/built-in.o (将 drivers 目录下所有涉及的文件编译成
built-in.o 文件)
+= sound/built-in.o (将 sound 目录下所有涉及的编译成 built-in.o 文件)
net-y : 网络
net-y := net/
net-y := $(patsubst %/, %/built-in.o, $(net-y))
意思是最后,将 net/目录下的所有涉及到的文件编译 built-in.o 这个文件。
从面的依赖文件展开来看,源材料就是上面这一大堆东西。这些东西如何组合成一个内核
(链接成在一块),要看 vmlinux 如何编译的。
4. vmlinux 如何编译
编译时是通过这些命令来编译的。这些命令最终会生成什么东西?可以通过这里一一分析下
去。这里涉及的脚本、函数太庞大了。没精
力去做。
想知道上在的源材料如何编译成内核:
方法 1:分析 Makefile .
方法 2:直接编译内核。 看编译过程。
a.rm vmlinux 先删除原来编译得到的内核。
b.make uImage V=1 (V=1 是更加详细的列出那些命令。 )
我们关心详细命令的最后一条。
分析后确定两个方面:
第一个文件是谁:从上面 make uImage 最后一条命令可知,内核第一个文件是
arch/arm/kernel/head.o(head.S)
链接脚本: arch/arm/kernel/vmlinux.lds (决定内核如何排布).
再接着是放所有文件的“.init.text”段。
等等、、、
这些所有文件排放在相应的 “段” 中,排放的顺序就是如下“链接脚本”后面“.o”文件
的排布顺序:
四、 机器 ID,启动参数
1. 建立 SI 工程:先是添加所有的代码,再去除不相关的代码。
添加完所有的文件代码后,再移除 ARCH 目录(因为里面有不需要的代码),移除后再进到
ARCH 目录重新
添加 ARM 相关的代码(因为这里处理的是 ARM 平台)
最后添加上平台: Plat-s3c24xx 平台,其他平台不需要加。
<2> include 目录。里面也有很多东西,先“Remove Tree”后,再挑选我们需要的加进工
程。
在 include 目录中, asm 开头的目录显然是 架构 相关的头文件。我们只关心 Asm-arm
arm 架构的头文件。
head.S 做的事情:
(0) .判断是否支持此 CPU
(1) .如何比较 机器 ID 是:(判断是否支持单板)
(3) .创建页表。
(4) .使能 MMU。
(5) .跳转到 start_kernel (它就是内核的第一个 C 函数)
2. 分析内核源代码:
<1> 通过 make uImage V=1 详细查看内核编译时的最后一条命令可知。
内核中排布的第一个文件是: arch/arm/kernel/head.S
链接脚本: arch/arm/kernel/vmlinux.lds
UBOOT 启动时首先在内存里设置了一大堆参数。
3. 内核启动:最终目标是就运行应用程序。对于 Linux 来说应用程序在 根文件系统中。需要挂接文件系统。
1)处理 UBOOT 传入的参数。
内核中排布的第一个文件是: arch/arm/kernel/head.S
4. 内核:
处理 UBOOT 传入的参数
1.首先判断是否支持这个 CPU。
2.判断是否支持这个单板。(UBOOT 启动内核时传进来的:机器 ID bd->bi_arch_number)
查 UBOOT 代码,对于这块开发板是: gd->bd->bi_arch_number = MACH_TYPE_SMDK2410
这个 362 这个值存在哪里?从汇编的 C 语言交互规则知道。这个参数 bi_arch_number 是存在r1寄存器中的。
2410, 2440, qt2410 的单板代码都强制放在这个地方。
内核启动时,会从 __arch_info_begin = . 开始读,读到 __arch_info_end = . 一个一个的将
单板
信息取出来。将里面的机器 ID 和 UBOOT 传进来的机器 ID 比较。相同则表示内核支持这个
单板。
下面就是比较机器 ID了。(内核中的和 UBOOT传进来的)看 arch\arm\kernel\head-common.S
从“__lookup_machine_type:”中可知 r5=__arch_info_begin
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
r5 是: __arch_info_begin
r1 是 UBOOT 传来的参数: bi_arch_number
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
最后比较成功后,会回到: head.S
以上 单板机器 ID 比较完成。
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);创建内核线程。暂且
认为它是调用 kernel_init 这个函数。这个函数中又有一个: prepare_namespace(),它其中
又有一个: mount_root();挂接根文件系统。
缩进表示调用关系, prepare_namespace 中执行完了 挂接根文件系统 后,会执行 init_post
函数,
在这个函数中会打开 /dev/console 和 执行应用程序。
__setup(“root=” ,root_dev_setup):其中 __setup 是一个宏。
大 概 意 思 是 发 现 在 命 令 行 参 数 : bootargc=ninitrd root=/dev/mtdblock3 init=/linuxrc
console=ttySAC0
中的 root= 时,就以这个 root= 来找到“root_dev_setup”这个函数。然后调用这个函数。
这个函数将:
/dev/mtdblock3 init=/linuxrc console=ttySAC0 保存到
“strlcpy(saved_root_name, line, sizeof(saved_root_name));”中的变量 saved_root_name 中。
这个变量是个数组。
static int __init root_dev_setup(char *line)
{
strlcpy(saved_root_name, line, sizeof(saved_root_name));
return 1;
}
__setup("root=", root_dev_setup);
__setup 这也是个宏,这个宏也是定义一个结构体。经过分析 UBOOT 和内核可知,这个宏
是个 结构体 。结构体里面有“root=”
和 root_dev_setup 函数指针(结构体中有“名字”和“函数指针”)。
这个结构体中的 段属性强制将其放一个段.init.setup 中,这个段在链接脚本中。
**********************************************************************
内核启动流程:
arch/arm/kernel/head.S
start_kernel
setup_arch //解析 UBOOT 传入的启动参数
setup_command_line //解析 UBOOT 传入的启动参数
parse_early_param
do_early_param
从__setup_start 到__setup_end,调用 early 函数
unknow_bootoption
obsolete_checksetup
从__setup_start 到__setup_end,调用非 early 函数
rest_init
kernel_init
prepare_namespace
mount_root //挂接根文件系统
init_post
//执行应用程序
“early”“非 early”是:
__setup("root=",root_dev_setup)
#define __setup(str, fn)
__setup_param(str,fn,fn,0)中参数"0".
**********************************************************************
从代码里知道,这里 early 这个成员为 0.则没“do_early_param” 和“parse_early_param”,
现在是 root=/dev/mtdblock3 ,我们说过在 FLASH 中没有分区表。那这个分区 mtdblock3
体现在写死的
代码。和 UBOOT 一样: “bootloader|参数|内核|文件系统”在代码中写死。也是用这个分
区,在代码里也要写死。
启动内核时,会打印出这些“分区信息”。
MTDPART_OFS_APPEND 这个 offset 意思是紧接着上面一个分区的意思。
欢迎加入作者的小圈子
扫描下方左边二维码加入QQ交流群,扫描下方右边二维码关注个人微信公众号并获取更多隐藏干货,QQ交流群:816747642 微信公众号:Crystal软件学堂
作者:Crystal软件学堂 bilibili视频教程地址:https://space.bilibili.com/5782182 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在转载文章页面给出原文连接。 如果你觉得文章对你有所帮助,烦请点个推荐,你的支持是我更文的动力。 文中若有错误,请您务必指出,感谢给予我建议并让我提高的你。 |