一起分析Linux系统设计思想——02Makefile简析(上)

在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

1 Makefile的分类

Linux 2.6内核中Makefile的分类如下表所示。

分类说明
/Makefile主Makefile主要控制编译的整体流程,类似于main函数的功能。
/arch/$(ARCH)/MakefileCPU架构相关的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文件最重要的功能就是控制代码在内存中的排布。这句话说的其实很含糊。如果想精细化,必须区分下面两个概念: SectionPosition

其实,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)。


<完>

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

穿越临界点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值