在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。
1 Makefile的分类
Linux 2.6内核中Makefile的分类如下表所示。
分类 | 说明 |
---|---|
/Makefile | 主Makefile主要控制编译的整体流程,类似于main函数的功能。 |
/arch/$(ARCH)/Makefile | CPU架构相关的Makefile,主要根据各自架构的特点指定需要编译的文件,并且负责特定格式镜像文件的生成。 |
scripts/Makefile.* | /scripts目录下包含了Makefile公用的规则和通用的脚本文件。 |
kbuild Makefiles | 各个模块目录下的子Makefile用来控制当前目录下编译的内容,比如哪些需要编译进内核,哪些编译成内核模块。 |
auto.conf | 是编译阶段自动生成的配置文件,用来进行文件粒度的裁剪。 |
我们来提出一些问题:Makefile为什么要分成这么多类别呢?只编写一个不行么?我们平时的工作中是不是经常只编写一个Makefile,这么做有什么弊端么?
和源码一样,Makefile的分类并不是技术上的分类,而是功能上的分类。这是基于 共性/特性-分类思想 进行的抽象。和源码一样,并不是创造了新的语法,而是创造了更多的功能模块,各司其职,共同完成一项工作。每个模块可以看作是一个 对象 ,对象生存的价值就是为其他对象或者整体提供服务贡献价值。这其中有一个 主对象 具有至高无上的权利,负责调度和协调,完成最终的目标。
2 找到程序的入口
程序的入口就好比是进入任何一座软件大厦的大门,所以,第一件事一般都是先找到程序入口。大家刚学C语言时,老师都会告诉你,main()函数就是程序的入口。但做底层的编程就不太一样了。
底层程序的入口由Makefile指定。
Tips:重点掌握如何通过Makefile找到程序入口点,可以不用太关注代码本身。
首先找到 /Makefile 中的第一个目标。第一个目标就是直接make的默认目标。
# The all: target is the default when no target is given on the
# command line.
# This allow a user to issue only 'make' to build a kernel including modules
# Defaults vmlinux but it is usually overridden in the arch makefile
all: vmlinux
第一个目标是 all ,而 all 又依赖于 vmlinux 。所以,继续跟踪。
# vmlinux image - including updated kernel symbols
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) $(kallsyms.o) FORCE
vmlinux-init := $(head-y) $(init-y)
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
vmlinux-all := $(vmlinux-init) $(vmlinux-main)
vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds
追踪到此,我们可以猜测整个系统的入口文件应该在 head-y 这个变量中指定,但是head-y并不存在与/Makefile文件中,而是存放在架构相关的Makefile中。
在 /arc/arm/Makefile 中我们查到:
#Default value
head-y := arch/arm/kernel/head$(MMUEXT).o arch/arm/kernel/init_task.o
# defines filename extension depending memory manement type.
ifeq ($(CONFIG_MMU),)
MMUEXT := -nommu
endif
这里我们考虑处理器带MMU模块的情况,则可以得出结论:整个系统的入口文件是 /arch/arm/kernel/head.S 。
我们进入该文件简单看下第一行代码长什么样子(后面的文章再仔细分析)。
/*
* Kernel startup entry point.
* ---------------------------
*/
.section ".text.head", "ax"
.type stext, %function
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __create_page_tables
嗯嗯,就长这个样子的啦,这就是Linux2.6的第一个“main函数”。
3 链接脚本lds
lds文件是指导整个链接过程的,主要用于进行内存布局的指导。
在 /Makefile 中我们可以看到这样一行代码:
vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds
我们先看看 arch/arm/kernel/vmlinux.lds 长什么样子(注意,编译内核之后才会由vmlinux.S生成vmlinux.lds文件),有个感性认识。
/* arch/arm/kernel/vmlinux.lds 代码片段 */
SECTIONS
{
. = (0xc0000000) + 0x00008000;
.text.head : {
_stext = .;
_sinittext = .;
*(.text.head)
}
.init : { /* Init code and data */
*(.init.text)
_einittext = .;
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
. = ALIGN(16);
__setup_start = .;
*(.init.setup)
__setup_end = .;
__early_begin = .;
*(.early_param.init)
__early_end = .;
...
3.1 代码在内存中的排布
lds文件最重要的功能就是控制代码在内存中的排布。这句话说的其实很含糊。如果想精细化,必须区分下面两个概念: Section 和 Position 。
其实,Position 这个概念是先出现的,因为最简单。如果,只存在Position概念的话,内存是怎样排布的呢?就是简单的将你编写的所有代码,不管是 data 还是 commend 都统一的从上到下按照 位置 齐刷刷排布在内存中。数据和命令是混在一起的(注意和冯诺依曼结构/普林斯顿体系结构概念区分开,不过这种编译方式是绝对不适合哈佛架构处理器的)。
但数据和指令混在一起有一个缺点:处理器在 取指-解析-执行 时没办法区分哪些是指令,哪些是数据,更谈不上做流水线了。因此,指令和数据必须分开—— Section的概念应运而生。
.text 和 .data 就属于不同的Section。到这里我们会发现Section的诞生底层的逻辑还是 分类思想 (即分而治之)。
.text和.data的区分是硬性需求,但为了方便,各种Section都渐渐地出现了,比如 .bss。
回归到本节主题,lds文件到底是怎样指导代码在内存中地排布地呢?
用一句话表达就是——Section概念中嵌套Position概念。Section的划分由链接脚本指定,Position的前后由源文件在Makefile中出现的位置决定。
举个例子,假设a.c(定义了g_aVal变量和foo_a()函数)和b.c(定义了g_bVal变量和foo_b()函数)在Makefile中的位置是a.c在b.c之前。那么编译之后.text段中foo_a()函数在foo_b()函数前面;.data段中g_aVal变量在g_bVal变量前面。
Tips:冯诺依曼结构和哈佛结构(或改进哈佛架构)是CPU硬件结构相关的概念,主要是看数据与指令是否分开存储(分开存储的意思是物理存储上分开,与是否共享同一条总线没有直接关系)。注意不要和编译阶段的概念混淆。
3.2 lds中虚拟地址是如何确定的
S3C2440在lds文件中指示编译地址(虚拟地址)是0xc0008000。为什么是这个数值而不是其他呢?
SECTIONS
{
. = (0xc0000000) + 0x00008000; /* 虚拟地址 */
.text.head : {
_stext = .;
_sinittext = .;
*(.text.head)
}
首先,解释为什么TEXT_OFFSET=0x000008000。0x8000的大小是32KB,其中有16KB给boot para使用了(当然没有用尽);剩下16KB给一级页表使用了(这里展开内容比较多,后续单独拉出来梳理)。
再次,解释下为什么PAGE_OFFSET=0xc0000000。这个代表的含义是链接脚本指定的虚拟地址基址从3GB(32位处理器最大寻址空间是4GB)偏移开始。但为什么选择这个偏移呢?现在我还没有找到合理的原因(如果有人知道,望回复,谢谢),但是我猜测可能跟S3C2440的RAM基址是0x80000000有些关系(比它刚好大1GB)。
<完>