linux中bls指令与bl区别,ARM linux的启动部分源代码简略分析
ARM linux的启动部分源代码简略分析以友善之臂的mini2440开发板为平台,以较新的内核linux-2.6.32.7版本为例,仅作说明之用。当内核映像被加载到RAM之后,Bootloader的控制权被释放。内核映像并不是可直接运行的目标代码,而是一个压缩过的zImage(小内核)。但是,也并非是zImage映像中的一切均被压缩了,映像中包含未被压缩的部分,这部分中包含解压缩程序,解压缩程序
ARM linux的启动部分源代码简略分析以友善之臂的mini2440开发板为平台,以较新的内核linux-2.6.32.7版本为例,仅作说明之用。
当内核映像被加载到RAM之后,Bootloader的控制权被释放。内核映像并不是可直接运行的目标代码,而是一个压缩过的zImage(小内核)。但是,也并非是zImage映像中的一切均被压缩了,映像中包含未被压缩的部分,这部分中包含解压缩程序,解压缩程序会解压缩映像中被压缩的部分。zImage使用gzip压缩的,它不仅仅是一个压缩文件,而且在这个文件的开头部分内嵌有gzip解压缩代码。当zImage被调用时它从arch/arm/boot/compressed/head.S的start汇编例程开始执行。这个例程进行一些基本的硬件设置,并调用arch/arm/boot/compressed/misc.c中的decompress_kernel()解压缩内核。
arch/arm/kernel/head.S文件是内核真正的启动入口点,一般是由解压缩内核的程序来调用的。首先先看下对于运行这个文件的要求:
MMU = off;D-cache = off;I-cache =无所谓,开也可以,关也可以;r0 = 0;r1 =机器号;r2 = atags指针。
这段代码是位置无关的,所以,如果以地址0xC0008000来链接内核,那么就可以直接用__pa(0xc0008000)地址来调用这里的代码。
其实,在这个(Linux内核中总共有多达几十个的以head.S命名的文件)head.S文件中的一项重要工作就是设置内核的临时页表,不然mmu开起来也玩不转,但是内核怎么知道如何映射内存呢?linux的内核将映射到虚地址0xCxxx xxxx处,但他怎么知道在4GB的地址空间中有哪一片ram是可用的,从而可以映射过去呢?因为不同的系统有不通的内存映像,所以,LINUX约定,要调用内核代码,一定要满足上面的调用要求,以为最初的内核代码提供一些最重要的关于机器的信息。内核代码开始的时候,R1存放的是系统目标平台的代号,对于一些常见的,标准的平台,内核已经提供了支持,只要在编译的时候选中就行了,例如对X86平台,内核是从物理地址1M开始映射的。
好了好了,看下面的代码。
ENTRY(stext)是这个文件的入口点。最初的几行是这样的:
setmodePSR_F_BIT
| PSR_I_BIT | SVC_MODE, r9
@ ensure svc mode
@ and irqs disabled
//设置为SVC模式,关闭中断和快速中断//此处设定系统的工作状态为SVC,arm有7种状态每种状态
//都有自己的堆栈,SVC为管理模式,具有完全的权限,可以执行任意指令
//访问任意地址的内存
//setmode是一个宏,其定义为:
//.macrosetmode, mode, reg
//msrcpsr_c, #\mode
//.endm
mrcp15,
0, r9, c0, c0@ get processor id
bl__lookup_processor_type@ r5=procinfo r9=cpuid
movsr10,
r5@ invalid processor (r5=0)?
beq__error_p@ yes, error 'p'
这几行是查询处理器的类型的,我们知道arm系列有很多型号,arm7、arm9、arm11、Cortex核等等类型,这么多型号要如何区分呢?其实,在arm的15号协处理器(其实ARM暂时也就这么一个协处理器)中有一个只读寄存器,存放与处理器相关信息。
__lookup_processor_type是arch/arm/kernel/head-common.S文件中定义的一个例程,这个head-common.S用include命令被包含在head.S文件中。其定义为:
__lookup_processor_type:
adrr3, 3f
ldmiar3, {r5 - r7}
addr3, r3, #8
subr3, r3, r7@
get offset between virt&phys
addr5, r5, r3@
convert virt addresses to
addr6, r6, r3@
physical address space
1:ldmiar5, {r3, r4}@
value, mask
andr4, r4, r9@
mask wanted bits
teqr3, r4
beq2f
addr5, r5, #PROC_INFO_SZ@ sizeof(proc_info_list)
cmpr5, r6
blo1b
movr5, #0@
unknown processor
2:movpc, lr
ENDPROC(__lookup_processor_type)
这个例程接受处理器ID(保存在寄存器r9中)为参数,查找链接器建立的支持的处理器表。此时此刻还不能使用__proc_info表的绝对地址,因为这时候MMU还没有开启,所以此时运行的程序没有在正确的地址空间中。所以不得不计算偏移量。若没有找到processor ID对应的处理器,则在r5寄存器中返回返回0,否则返回一个proc_info_list结构体的指针(在物理地址空间)。proc_info_list结构体在文件中定义:
struct proc_info_list {
unsigned
intcpu_val;
unsigned
intcpu_mask;
unsigned
long__cpu_mm_mmu_flags;/* used by head.S */
unsigned
long__cpu_io_mmu_flags;/* used by head.S */
unsigned
long__cpu_flush;/* used by head.S */
const
char*arch_name;
const
char*elf_name;
unsigned
intelf_hwcap;
const
char*cpu_name;
struct
processor*proc;
struct
cpu_tlb_fns*tlb;
struct
cpu_user_fns*user;
struct
cpu_cache_fns*cache;
};第一项是CPU id,将与协处理器中读出的id作比较,其余的字段也都是与处理器相关的信息,到下面初始化的过程中自然会用到。
另外,这个例程加载符地址的代码也是挺值得我辈学习的:
adrr3, 3f加载一个符号的地址,这个符号在加载语句前面(下面)定义,forward嘛,这个符号为3,离这条语句最近的那个。在那个符号为3的位置我们看到这样的代码:
.align2
3:.long__proc_info_begin
.long__proc_info_end
4:.long.
.long__arch_info_begin
.long__arch_info_end
搜索这两个符号的值,在文件arch/arm/kernel/vmlinux.lds.S中:
__proc_info_begin
= .;
*(.proc.info.init)
__proc_info_end
= .;
这两个符号分别是一种初始化的段的结束开始地址和结束地址。为了了解由struct proc_info_list结构体组成的段的实际构成,我们还是得要了解一下在系统中到底都有哪些变量是声明了要被放到这个段的。用关键字.proc.info.init来搜,全部都是arch/arm/mm/proc-*.S文件,这些都是特定于处理器的汇编语言文件,对于我们的mini2440,自然是要看proc-arm920.S文件的,在其中可以看到这些内容:
.section ".proc.info.init", #alloc, #execinstr
.type__arm920_proc_info,#object
__arm920_proc_info:
.long0x41009200
.long0xff00fff0
.longPMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
.longPMD_TYPE_SECT | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b__arm920_setup
.longcpu_arch_name
.longcpu_elf_name
.longHWCAP_SWP | HWCAP_HALF
| HWCAP_THUMB
.longcpu_arm920_name
.longarm920_processor_functions
.longv4wbi_tlb_fns
.longv4wb_user_fns
#ifndef
CONFIG_CPU_DCACHE_WRITETHROUGH
.longarm920_cache_fns
#else
.longv4wt_cache_fns
#endif
.size__arm920_proc_info, . -
__arm920_proc_info
看到这儿我们再回国头去看__lookup_processor_type的代码:
ldmiar3, {r5 - r7}
addr3, r3, #8
subr3, r3, r7
尽管符号3处只有两个有效值,但它加载了三个数,而第三个数,我们看到是这样定义的:
.long.
__lookup_processor_type中,给r3加上8,也就是让r3指向“.”的地址,然后用r3减r7来获取虚拟地址与物理地址的差,这样看来,“.”就应该是虚拟空间(编译地址)里那个数据的地址。
之后的代码获得__proc_info_begin和__arch_info_end这两个符号在物理空间中的地址:
addr5, r5, r3@
convert virt addresses to
addr6, r6, r3
然后便是在那个段中逐个的检查struct proc_info_list结构体,以找到与我们的CPU相匹配的:
1:ldmiar5, {r3, r4}@ value, mask
andr4, r4, r9@ mask wanted bits
teqr3, r4
beq2f
addr5, r5, #PROC_INFO_SZ@ sizeof(proc_info_list)
cmpr5, r6
blo1b
movr5, #0@ unknown processor
2:movpc, lr
__lookup_processor_type例程会返回在文件arch/arm/mm/proc-arm920.S中定义的一个保存有与我们的处理器相关的信息的struct proc_info_list结构体的地址。
接下来我们继续看stext的代码:bl__lookup_machine_type@ r5=machinfo
movsr8,
r5@ invalid machine (r5=0)?
beq__error_a@ yes, error 'a'
在获得了处理器信息之后,则调用__lookup_machine_type来查找机器信息。这个例程同样也在arch/arm/kernel/head-common.S文件中定义。这个例程的定义如下:
__lookup_machine_type:
adrr3, 4b
ldmiar3, {r4, r5, r6}
subr3, r3, r4@
get offset between virt&phys
addr5, r5, r3@
convert virt addresses to
addr6, r6, r3@
physical address space
1:ldrr3, [r5, #MACHINFO_TYPE]@ get machine type
teqr3, r1@
matches loader number?
beq2f@ found
addr5, r5, #SIZEOF_MACHINE_DESC@ next machine_desc
cmpr5, r6
blo1b
movr5, #0@
unknown machine
2:movpc, lr
ENDPROC(__lookup_machine_type)
处理的过程和上面的__lookup_processor_type还是挺相似的。这个例程接收r1中传进来的机器号作为参数,然后,在一个由struct machine_desc结构体组成的段中查找和我们的机器号匹配的struct machine_desc结构体,这个结构体在arch/arm/include/asm/mach/arch.h文件中定义,用于保存机器的信息:
struct
machine_desc {
/*
*
Note! The first four elements are used
* by
assembler code in head.S, head-common.S
*/
unsigned intnr;/* architecture number*/
unsigned intphys_io;/* start of physical io*/
unsigned intio_pg_offst;/* byte offset for io
* page tabe entry*/
const char*name;/* architecture name*/
unsigned longboot_params;/* tagged list*/
unsigned intvideo_start;/* start of video RAM*/
unsigned intvideo_end;/* end of video RAM*/
unsigned intreserve_lp0
:1;/* never has lp0*/
unsigned intreserve_lp1
:1;/* never has lp1*/
unsigned intreserve_lp2
:1;/* never has lp2*/
unsigned intsoft_reboot
:1;/* soft reboot*/
void(*fixup)(struct
machine_desc *,
struct tag *, char **,
struct meminfo *);
void(*map_io)(void);/*
IO mapping function*/
void(*init_irq)(void);
struct sys_timer*timer;/* system tick
timer*/
void(*init_machine)(void);
};
同样这个例程也用到了同上面很相似的方式来获得符号的地址:
adrr3, 4bb代表back,即向后,这个符号为4,紧接着我们前面看到的那个为3的标号:
4:.long.
.long__arch_info_begin
.long__arch_info_end
在文件arch/arm/kernel/vmlinux.lds.S中我们可以看到段的定义:
__arch_info_begin
= .;
*(.arch.info.init)
__arch_info_end
= .;
这两个符号也是分别表示某种初始化的段的开始地址和结束地址。为了找到段的填充内容,还是得要了解一下到底都有哪些struct machine_desc结构体类型变量声明了要被放到这个段的。用关键字.arch.info.init来搜索所有的内核源文件。在arch/arm/include/asm/mach/arch.h文件中我们看到:
#define MACHINE_START(_type,_name)\
static const struct machine_desc
__mach_desc_##_type\
__used\
__attribute__((__section__(".arch.info.init")))
= {\
.nr= MACH_TYPE_##_type,\
.name= _name,
#define MACHINE_END\
};
定义机器结构体,也就是.arch.info.init段中的内容,都是要通过两个宏MACHINE_START和MACHINE_END来完成的啊,MACHINE_START宏定义一个truct machine_desc结构体,并初始化它的机器号字段和机器名字段,可以在arch/arm/tools/mach-types文件中看到各种平台的机器号的定义。那接着我们来搜MACHINE_START吧,这是一个用于定义机器结构体的宏,所以可以看到这个符号好像都是在arch/arm/mach-*/mach-*.c这样的文件中出现的,我们感兴趣的应该是arch/arm/mach-s3c2440/mach-mini2440.c文件中的这个符号:
MACHINE_START(MINI2440,
"MINI2440")
/*
Maintainer: Michel Pollet */
.phys_io= S3C2410_PA_UART,
.io_pg_offst= (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params= S3C2410_SDRAM_PA
+ 0x100,
.map_io= mini2440_map_io,
.init_machine= mini2440_init,
.init_irq= s3c24xx_init_irq,
.timer= &s3c24xx_timer,
MACHINE_END
OK, __lookup_machine_type这个例程的我们也搞明白了。回忆一下,启动代码现在已经完成的工作,R10寄存器中为指向proc_info_list结构体的指针(物理地址空间),这个结构体包含有关于我们的处理器的一些重要信息。R8寄存器中为指向一个与我们的平台相匹配的machine_desc结构体的指针,这个结构体中保存有一些关于我们的平台的重要信息。
回来接着看arch/arm/kernel/head.S文件中的stext:
bl__vet_atags
这个例程同样同样也是在arch/arm/kernel/head-common.S文件中定义:
__vet_atags:
tstr2,
#0x3@ aligned?
bne1f
ldrr5,
[r2, #0]@ is first tag ATAG_CORE?
cmpr5,
#ATAG_CORE_SIZE
cmpner5,
#ATAG_CORE_SIZE_EMPTY
bne1f
ldrr5,
[r2, #4]
ldrr6,
=ATAG_CORE
cmpr5,
r6
bne1f
movpc,
lr@ atag pointer is ok
1:movr2,
#0
movpc,
lr
ENDPROC(__vet_atags)
这个例程接收机器信息(R8寄存器)为参数,并检测r2中传入的ATAGS指针的合法性。内核使用tag来作为bootloader传递内核参数的方式。系统要求r2中传进来的ATAGS指针式4字节对齐的,同时要求ATAGS列表的第一个tag是一个ATAG_CORE类型的。
此时R10寄存器中保存有指向CPU信息结构体的指针,R8寄存器中保存有指向机器结构体的指针,R2寄存器中保存有指向tag表的指针,R9中还保存有CPU
ID信息。
回到arch/arm/kernel/head.S文件中的stext,之后就要进入初始化过程中比较关键的一步了,开始设置mmu,但首先要填充一个临时的内核页表,映射4m的内存,这在初始化过程中是足够了:
bl__create_page_tables
这个例程设置初始页表,这里只设置最起码的数量,只要能使内核运行即可,r8= machinfo,r9= cpuid,r10 = procinfo,在r4寄存器中返回物理页表地址。
__create_page_tables例程在文件arch/arm/kernel/head.S中定义:
__create_page_tables:
pgtblr4@ page table address
// pgtbl是一个宏,本文件的前面部分有定义:
//.macropgtbl,
rd
//ldr\rd,
=(KERNEL_RAM_PADDR - 0x4000)
//.endm
// KERNEL_RAM_PADDR在本文件的前面有定义,为(PHYS_OFFSET
+ TEXT_OFFSET)
// PHYS_OFFSET在arch/arm/mach-s3c2410/include/mach/memory.h定义,
//为UL(0x30000000)
//而TEXT_OFFSET在arch/arm/Makefile中定义,为内核镜像在内存中到内存
//开始位置的偏移(字节),为$(textofs-y)
// textofs-y也在文件arch/arm/Makefile中定义,
//为textofs-y:= 0x00008000
// r4
= 30004000为临时页表的起始地址
//首先即是初始化16K的页表,高12位虚拟地址为页表索引,所以为
// 4K*4 = 16K,大页表,每一个页表项,映射1MB虚拟地址。
//这个地方还来了个循环展开,以优化性能。
movr0,
r4
movr3,
#0
addr6,
r0, #0x4000
1:strr3,
[r0], #4
strr3,
[r0], #4
strr3,
[r0], #4
strr3,
[r0], #4
teqr0,
r6
bne1b
ldrr7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
// PROCINFO_MM_MMUFLAGS在arch/arm/kernel/asm-offsets.c文件中定义,
//为DEFINE(PROCINFO_MM_MMUFLAGS,
// offsetof(struct proc_info_list, __cpu_mm_mmu_flags));
// R10寄存器保存的指针指向是我们前面找到的proc_info_list结构嘛。
//为内核的第一个MB创建一致的映射,以为打开MMU做准备,这个映射将会被
// paging_init()移除,这里使用程序计数器来获得相应的段的基地址。
//这个地方是直接映射。
movr6,
pc
movr6,
r6, lsr #20@ start of kernel
section
orrr3,
r7, r6, lsl #20@ flags + kernel base
strr3,
[r4, r6, lsl #2]@ identity mapping
//接下来为内核的直接映射区设置页表。KERNEL_START在文件的前面定义,
//为KERNEL_RAM_VADDR,即内核的虚拟地址。
//而KERNEL_RAM_VADDR在文件的前面定义,则为(PAGE_OFFSET + TEXT_OFFSET)
//映射完整的内核代码段,初始化数据段。
// PAGE_OFFSET为内核镜像开始的虚拟地址,在
// arch/arm/include/asm/memory.h中定义。在配置内核时选定具体值,默认
//为0xC0000000。
//因为最高12位的值是页表中的偏移地址,而第三高的四位必然为0,
//每个页表项为4字节,右移20位之后,还得再左移两位回来,所以,这里只//是左移18位。
// R3寄存器在经过了上面的操作之后,实际上是变成了指向内核镜像代码段
//的指针(物理地址),在这个地方,再一次为内核镜像的第一个MB做了映射。
// R6随后指向了内核镜像的尾部。R0为页表项指针。
//这里以1MB为单位来映射内核镜像。
addr0,
r4,#(KERNEL_START & 0xff000000)
>> 18
strr3,
[r0, #(KERNEL_START & 0x00f00000)
>> 18]!
ldrr6,
=(KERNEL_END - 1)
addr0,
r0, #4
addr6,
r4, r6, lsr #18//得到页表的结束物理地址
1:cmpr0,
r6
addr3,
r3, #1 << 20
strlsr3,
[r0], #4
bls1b
//为了使用启动参数,将物理内存的第一MB映射到内核虚拟地址空间的
//第一个MB,r4存放的是页表的地址。这里的PAGE_OFFSET的虚拟地址
//比上面的KERNEL_START要小0x8000
addr0,
r4, #PAGE_OFFSET >> 18
orrr6,
r7, #(PHYS_OFFSET & 0xff000000)
.if(PHYS_OFFSET
& 0x00f00000)
orrr6,
r6, #(PHYS_OFFSET & 0x00f00000)
.endif
strr6,
[r0]
//上面的这个步骤显得似乎有些多余。
//总结一下,这个建立临时页表的过程:
// 1、为内核镜像的第一个MB建立直接映射
// 2、为内核镜像完整的建立从虚拟地址到物理地址的映射
// 3、为物理内存的第一个MB建立到内核的虚拟地址空间的第一个MB的映射。
// OK,内核的临时页表建立完毕。整个初始化临时页表的过程都没有修改R8,
// R9和R10。
movpc,
lr
ENDPROC(__create_page_tables)
回到stext:
ldrr13,
__switch_data@ address to jump to
after
@
mmu has been enabled
这个地方实际上是在r13中保存了另一个例程的地址。后面的分析中,遇到执行到这个例程的情况时会有详细说明。
接着看stext:
adrlr,
BSYM(__enable_mmu)@ return (PIC)
address
BSYM()是一个宏,在文件arch/arm/include/asm/unified.h中定义,为:
#define BSYM(sym)sym
也就是说这个语句也仅仅是把__enable_mmu例程的地址加载进lr寄存器中。为了方便之后调用的函数返回时,直接执行__enable_mmu例程。
接着看stext下一句:
ARM(addpc, r10, #PROCINFO_INITFUNC)
ARM()也是一个宏,同样在文件arch/arm/include/asm/unified.h中定义,当配置内核为生成ARM镜像,则为:#define ARM(x...)x
所以这一条语句也就是在调用一个例程。R10中保存的是procinfo结构的地址。PROCINFO_INITFUNC符号在arch/arm/kernel/asm-offsets.c文件中定义,为:
DEFINE(PROCINFO_INITFUNC,
offsetof(struct proc_info_list, __cpu_flush));
也就是调用结构体proc_info_list的__cpu_flush成员函数。回去查看arch/arm/mm/proc-arm920.S文件中struct
proc_info_list结构体的变量的定义,可以看到这个成员为:
b__arm920_setup
也就是说,在设置好内核临时页表之后调用了例程__arm920_setup,这个例程同样在arch/arm/mm/proc-arm920.S中:
__arm920_setup:
movr0,
#0
mcrp15,
0, r0, c7, c7@ invalidate I,D caches
on v4
mcrp15,
0, r0, c7, c10, 4@ drain write
buffer on v4
#ifdef
CONFIG_MMU
mcrp15,
0, r0, c8, c7@ invalidate I,D TLBs on
v4
#endif
adrr5,
arm920_crval
ldmiar5,
{r5, r6}
mrcp15,
0, r0, c1, c0@ get control register v4
bicr0,
r0, r5
orrr0,
r0, r6
movpc,
lr
这一段首先使i,d caches内容无效,然后清除write buffer,接着使TLB内容无效。接下来加载变量arm920_crval的地址,我们看到arm920_crval变量的内容为:
rm920_crval:
crvalclear=0x00003f3f,
mmuset=0x00003135, ucset=0x00001130
crval为一个宏,在arch/arm/mm/proc-macros.S中定义:
.macrocrval, clear, mmuset, ucset
#ifdef CONFIG_MMU
.word\clear
.word\mmuset
#else
.word\clear
.word\ucset
#endif
.endm
其实也就是定义两个变量而已。之后,在r0中,得到了我们想要往协处理器相应寄存器中写入的内容。
之后的__arm920_setup返回,movpc, lr,即是调用例程__enable_mmu,这个例程在文件arch/arm/kernel/head.S中:
__enable_mmu:
#ifdef
CONFIG_ALIGNMENT_TRAP
orrr0,
r0, #CR_A
#else
bicr0,
r0, #CR_A
#endif
#ifdef
CONFIG_CPU_DCACHE_DISABLE
bicr0,
r0, #CR_C
#endif
#ifdef
CONFIG_CPU_BPREDICT_DISABLE
bicr0,
r0, #CR_Z
#endif
#ifdef
CONFIG_CPU_ICACHE_DISABLE
bicr0,
r0, #CR_I
#endif
movr5,
#(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
mcrp15,
0, r5, c3, c0, 0@ load domain access
register
mcrp15,
0, r4, c2, c0, 0@ load page table
pointer
b__turn_mmu_on
在这儿设置了页目录地址(r4寄存器中保存),然后设置domain的保护,在前面建立页表的例程中,注意到,页表项的控制信息,是从struct proc_info_list结构体的某字段中取的,其页目录项的domain都是0,domain寄存器中的domain 0对应的是0b11,表示访问模式为manager,不受限制。在这里同时也完成r0的某些位的进一步设置。
然后,__enable_mmu例程又调用了__turn_mmu_on,在同一个文件中定义:
__turn_mmu_on:
movr0, r0
mcrp15, 0, r0, c1, c0, 0@ write control reg
mrcp15, 0, r3, c0, c0, 0@ read id reg
movr3, r3
movr3, r13
movpc, r3
ENDPROC(__turn_mmu_on)
接下来写控制寄存器:
mcr p15, 0, r0, c1, c0 ,0
一切设置就此生效,到此算是完成了打开d,icache和mmu的工作。
注意:arm的d cache必须和mmu一起打开,而i cache可以单独打开。其实,cache和mmu的关系实在是紧密,每一个页表项都有标志标示是否是cacheable的,可以说本来就是设计一起使用的
前面有提到过,r13中存放的其实是另外一个例程的地址,其值是变量__switch_data的第一个字段,即一个函数指针的值,__switch_data变量是在arch/arm/kernel/head-common.S中定义的:
__switch_data:
.long__mmap_switched
.long__data_loc@ r4
.long_data@ r5
.long__bss_start@ r6
.long_end@ r7
.longprocessor_id@ r4
.long__machine_arch_type@ r5
.long__atags_pointer@ r6
.longcr_alignment@ r7
.longinit_thread_union
+ THREAD_START_SP @ sp
前面的ldr
r13 __switch_data,实际上也就是加载符号__mmap_switched的地址,实际上__mmap_switched是一个arch/arm/kernel/head-common.S中定义的例程。接着来看这个例程的定义,在arch/arm/kernel/head-common.S文件中:
__mmap_switched:
adrr3, __switch_data + 4
ldmiar3!, {r4, r5, r6, r7}
cmpr4, r5@
Copy data segment if needed
1:cmpner5, r6
ldrnefp, [r4], #4
strnefp, [r5], #4
bne1b
movfp, #0@
Clear BSS (and zero fp)
1:cmpr6, r7
strccfp, [r6],#4
bcc1b
ldmiar3, {r4, r5, r6, r7, sp}
strr9, [r4]@
Save processor ID
strr1, [r5]@
Save machine type
strr2, [r6]@
Save atags pointer
bicr4, r0, #CR_A@
Clear 'A' bit
stmiar7, {r0, r4}@
Save control register values
bstart_kernel
ENDPROC(__mmap_switched)
这个例程完成如下工作:
1、使r3指向__switch_data变量的第二个字段(从1开始计数)。
2、执行了一条加载指令,也就是在r4, r5, r6, r7寄存器中分别加载4个符号__data_loc,_data,__bss_start,_end的地址,这四个符号都是在链接脚本arch/arm/kernel/vmlinux.lds.S中出现的,标识了镜像各个段的地址,我们应该不难猜出他们所代表的段。
3、如果需要的话则复制数据段(数据段和BSS段是紧邻的)。
4、初始化BSS段,全部清零,BSS是未初始化的全局变量区域。5、又看到一条加载指令,同样在一组寄存器中加载借个符号的地址,r4中为processor_id,r5中为__machine_arch_type,
r6中为__atags_pointer, r7中为cr_alignment,sp中为init_thread_union + THREAD_START_SP。
6、接着我们看到下面的几条语句,则是用前面获取的信息来初始化那些全局变量r9,机器号被保存到processor_id处;r1寄存器的值,机器号,被保存到变量__machine_arch_type中,其他的也一样。
7、重新设置堆栈指针,指向init_task的堆栈。init_task是系统的第一个任务,init_task的堆栈在task structure的后8K,我们后面会看到。8、最后就要跳到C代码的start_kernel。bstart_kernel到此为止,汇编部分的初始化代码就结束了
O,My God.初始化代码的汇编部分终于结束。从而进入了与体系结构无关的Linux内核部分。start_kernel()会调用一系列初始化函数来设置中断,执行进一步的内存配置。
现在让我们来回忆一下目前的系统状态:临时页表已经建立,在0X30004000处,映射了映像文件大小空间,虚地址0XC000000被映射到0X30000000。CACHE,MMU都已经打开。堆栈用的是任务init_task的堆栈。
如果以为到了c代码可以松一口气的话,就大错特措了,linux的c也不比汇编好懂多少,相反倒掩盖了汇编的一些和机器相关的部分,有时候更难懂。其实作 为编写操作系统的c代码,只不过是汇编的另一种写法,和机器代码的联系是很紧密的。另外,这些start_kernel()中调用的C函数,每一个都具有举足轻重的地位,它们中的许多都肩负着初始化内核中的某个子系统的重要使命,而Linux内核中每一个子系统都错综复杂,牵涉到各种软件、硬件的复杂算法,所以理解起来倒真的是挺困难的。
start_kernel函数在init/main.c中定义:
528 asmlinkage void __init
start_kernel(void)
529 {
530char * command_line;
531extern struct kernel_param __start___param[], __stop___param[];
532
533smp_setup_processor_id();
534
535/*
536* Need to run as early as possible, to initialize the
537* lockdep hash:
538*/
539lockdep_init();
540debug_objects_early_init();
541
542/*
543* Set up the the initial canary ASAP:
544*/
545boot_init_stack_canary();
546
547cgroup_init_early();
548
549local_irq_disable();
550early_boot_irqs_off();
551early_init_irq_lock_class();
552
553 /*
554* Interrupts are still disabled. Do necessary setups, then
555* enable them
556*/
557lock_kernel();
558tick_init();
559boot_cpu_init();
560page_address_init();
561printk(KERN_NOTICE "%s", linux_banner);
562setup_arch(&command_line);
563mm_init_owner(&init_mm, &init_task);
564setup_command_line(command_line);
565setup_nr_cpu_ids();
566setup_per_cpu_areas();
567smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
568
569build_all_zonelists();
570page_alloc_init();
571
572 printk(KERN_NOTICE "Kernel
command line: %s\n", boot_command_line);
573parse_early_param();
574 parse_args("Booting kernel",
static_command_line,
575__start___param, __stop___param -
__start___param,
576&unknown_bootoption);
577/*
578* These use large bootmem allocations and must precede
579* kmem_cache_init()
580*/
581pidhash_init();
582vfs_caches_init_early();
583sort_main_extable();
584trap_init();
585mm_init();
586/*
587* Set up the scheduler prior starting any interrupts (such as the
588* timer interrupt). Full topology setup happens at smp_init()
589* time - but meanwhile we still have a functioning scheduler.
590*/
591sched_init();
592/*
593* Disable preemption - early bootup scheduling is extremely
594* fragile until we cpu_idle() for the first time.
595*/
596preempt_disable();
597if (!irqs_disabled()) {
598printk(KERN_WARNING
"start_kernel(): bug: interrupts were "
599 "enabled *very*
early, fixing it\n");
600local_irq_disable();
601}
602rcu_init();
603radix_tree_init();
604/* init some links before init_ISA_irqs() */
605early_irq_init();
606init_IRQ();
607prio_tree_init();
608init_timers();
609hrtimers_init();
610softirq_init();
611timekeeping_init();
612time_init();
613profile_init();
614if (!irqs_disabled())
615printk(KERN_CRIT
"start_kernel(): bug: interrupts were "
616"enabled
early\n");
617early_boot_irqs_on();
618local_irq_enable();
619
620/* Interrupts are enabled now so all GFP allocations are safe. */
621gfp_allowed_mask = __GFP_BITS_MASK;
622
623kmem_cache_init_late();
624
625/*
626* HACK ALERT! This is early. We're enabling the console before
627* we've done PCI setups etc, and console_init() must be aware of
628* this. But we do want output early, in case something goes wrong.
629*/
630console_init();
631if (panic_later)
632panic(panic_later, panic_param);
633
634lockdep_info();
635
636/*
637* Need to run this when irqs are enabled, because it wants
638* to self-test [hard/soft]-irqs on/off lock inversion bugs
639* too:
640*/
641locking_selftest();
642
643 #ifdef CONFIG_BLK_DEV_INITRD
644if (initrd_start && !initrd_below_start_ok &&
645page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
646printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) -
"
647"disabling it.\n",
648page_to_pfn(virt_to_page((void
*)initrd_start)),
649min_low_pfn);
650initrd_start = 0;
651}
652 #endif
653page_cgroup_init();
654enable_debug_pagealloc();
655kmemtrace_init();
656kmemleak_init();
657debug_objects_mem_init();
658idr_init_cache();
659setup_per_cpu_pageset();
660numa_policy_init();
661if (late_time_init)
662late_time_init();
663sched_clock_init();
664calibrate_delay();
665pidmap_init();
666anon_vma_init();
667 #ifdef CONFIG_X86
668if (efi_enabled)
669efi_enter_virtual_mode();
670 #endif
671thread_info_cache_init();
672cred_init();
673fork_init(totalram_pages);
674proc_caches_init();
675buffer_init();
676key_init();
677security_init();
678vfs_caches_init(totalram_pages);
679signals_init();
680/* rootfs populating might need page-writeback */
681page_writeback_init();
682 #ifdef CONFIG_PROC_FS
683proc_root_init();
684 #endif
685cgroup_init();
686cpuset_init();
687taskstats_init_early();
688delayacct_init();
689
690check_bugs();
691
692acpi_early_init(); /* before LAPIC and SMP init */
693sfi_init_late();
694
695ftrace_init();
696
697/* Do the rest non-__init'ed, we're now alive */
698rest_init();
699 }
接着我们来近距离的观察一下start_kernel函数中调用的这些重量级的函数。
首先来看setup_arch(&command_line)函数,这个函数(对于我们的mini2440平台来说)在arch/arm/kernel/setup.c中定义:
664 void __init setup_arch(char **cmdline_p)
665 {
666struct tag *tags = (struct tag
*)&init_tags;
667struct machine_desc *mdesc;
668char *from =
default_command_line;
669
670unwind_init();
671
672setup_processor();
673mdesc =
setup_machine(machine_arch_type);
674machine_name = mdesc->name;
675
676if (mdesc->soft_reboot)
677reboot_setup("s");
678
679if (__atags_pointer)
680tags =
phys_to_virt(__atags_pointer);
681else if (mdesc->boot_params)
682tags =
phys_to_virt(mdesc->boot_params);
683
684/*
685* If we have the old style
parameters, convert them to
686* a tag list.
687*/
688if (tags->hdr.tag !=
ATAG_CORE)
689convert_to_tag_list(tags);
690if (tags->hdr.tag !=
ATAG_CORE)
691tags = (struct tag
*)&init_tags;
692
693if (mdesc->fixup)
694mdesc->fixup(mdesc,
tags, &from, &meminfo);
695
696if (tags->hdr.tag ==
ATAG_CORE) {
697if (meminfo.nr_banks
!= 0)
698squash_mem_tags(tags);
699save_atags(tags);
700parse_tags(tags);
701}
702
703init_mm.start_code = (unsigned
long) _text;
704init_mm.end_code= (unsigned long) _etext;
705init_mm.end_data= (unsigned long) _edata;
706init_mm.brk= (unsigned long) _end;
707
708/* parse_early_param needs a
boot_command_line */
709strlcpy(boot_command_line,
from, COMMAND_LINE_SIZE);
710
711/* populate cmd_line too for
later use, preserving boot_command_line */
712strlcpy(cmd_line,
boot_command_line, COMMAND_LINE_SIZE);
713*cmdline_p = cmd_line;
714
715parse_early_param();
716
717paging_init(mdesc);
718request_standard_resources(&meminfo, mdesc);
719
720 #ifdef CONFIG_SMP
721 smp_init_cpus();
722 #endif
723
724cpu_init();
725tcm_init();
726
727/*
728* Set up various
architecture-specific pointers
729*/
730init_arch_irq =
mdesc->init_irq;
731system_timer =
mdesc->timer;
732init_machine = mdesc->init_machine;
733
734 #ifdef CONFIG_VT
735 #if defined(CONFIG_VGA_CONSOLE)
736conswitchp = &vga_con;
737 #elif defined(CONFIG_DUMMY_CONSOLE)
738conswitchp = &dummy_con;
739 #endif
740 #endif
741early_trap_init();
742 }
来看一些我们比较感兴趣的地方:
1、666行,struct
tag指针类型的局部变量指向了默认的tag列表init_tags,该静态变量在setup_arch()定义同文件的前面有如下定义:
636 /*
637* This holds our defaults.
638*/
639 static
struct init_tags {
640struct tag_header hdr1;
641struct tag_corecore;
642struct tag_header hdr2;
643struct tag_mem32mem;
644struct tag_header hdr3;
645 }
init_tags __initdata = {
646{ tag_size(tag_core), ATAG_CORE },
647{ 1, PAGE_SIZE, 0xff },
648{ tag_size(tag_mem32), ATAG_MEM },
649{ MEM_SIZE, PHYS_OFFSET },
650{ 0, ATAG_NONE }
651 };
第679行检察__atags_pointer指针的有效性,这个指针是在前面,跳转到start_kernel函数的汇编例程最后设置的几个变量之一,用的是R2寄存器的值。如果bootloader通过R2传递了tag列表的话,自然是要使用bootloader穿的进来的tag列表的。
2、第688行的字符指针类型的局部变量from指向了default_command_line静态变量,这个变量同样在前面有定义:
124 static
char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
传递给内核的命令行参数,是可以在内核配置的时候设置的。
3、第673行以machine_arch_type为参数调用了setup_machine()函数,而这个函数的定义为:
369 static struct machine_desc * __init
setup_machine(unsigned int nr)
370 {
371struct
machine_desc *list;
372
373/*
374*
locate machine in the list of supported machines.
375*/
376list =
lookup_machine_type(nr);
377if
(!list) {
378printk("Machine configuration botched (nr %d), "
379" unable to
continue.\n", nr);
380while (1);
381}
382
383printk("Machine: %s\n", list->name);
384
385return
list;
386 }
在arch/arm/kernel/head-common.S文件中,我们看到了一个对于__lookup_machine_type例程的封装的可被C语言程序调用的汇编语言编写的函数lookup_machine_type(),接收机器号,查表,然后返回匹配的struct machine_desc结构体的指针。在这里,对于我们的mini2440,返回的自然是arch/arm/mach-s3c2440/mach-mini2440.c文件中定义的结构体了:
MACHINE_START(MINI2440, "MINI2440")
/*
Maintainer: Michel Pollet */
.phys_io= S3C2410_PA_UART,
.io_pg_offst= (((u32)S3C24XX_VA_UART)
>> 18) & 0xfffc,
.boot_params= S3C2410_SDRAM_PA
+ 0x100,
.map_io= mini2440_map_io,
.init_machine= mini2440_init,
.init_irq= s3c24xx_init_irq,
.timer= &s3c24xx_timer,
MACHINE_END
然后,machine_desc结构体的name成员的值被赋给全局变量machine_name。
第681行,若bootloader没有传递tag列表给内核,则检测machine_desc结构体的boot_params字段,看看特定的平台是否传递了标记列表。
第730、731、732行分别将machine_desc结构体的init_irq、timer和init_machine成员值赋给了三个全局变量init_arch_irq、system_timer和init_machine,即是设置特定体系结构的指针。初始化的后面阶段自然会用到。
start_kernel()函数调用同文件下的rest_init(void)函数,rest_init(void)函数调用kernel_thread()函数以启动第一个核心线程,该线程执行kernel_init()函数,而原执行序列会调用cpu_idle(),等待调度。
作为核心线程的kernel_init()函数继续完成一些设置,并在最后调用同文件下的init_post()函数,而该函数挂在根文件系统,打开/dev/console设备,重定向stdin、stdout和stderr到控制台。之后,它搜索文件系统中的init程序(也可以由“init=”命令行参数指定init程序),并使用run_init_process()函数执行init程序。(事实上,run_init_process()函数又调用了kernel_execve()来实际执行程序)。搜索init程序的顺序为/sbin/init、/etc/init、/bin/init、和/bin/sh。在嵌入式系统中,多数情况下,可以给内核传入一个简单的shell脚本来启动必需的嵌入式应用程序。
至此,漫长的Linux内核引导和启动过程就结束了,而kernel_init()对应的由rest_init(void)函数创建的第一个线程也进入用户模式。
参考文献:
arm嵌入式LINUX启动过程:
http://blog.ednchina.com/yujiebaomei/4153/message.aspx
http://www.cnblogs.com/bluepointcq/articles/490954.html
Linux设备驱动开发详解,宋宝华
更多推荐
所有评论(0)