Chinaunix首页 | 论坛 | 博客
  • 博客访问: 121066
  • 博文数量: 31
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 296
  • 用 户 组: 普通用户
  • 注册时间: 2015-01-10 21:57
文章分类

全部博文(31)

文章存档

2016年(4)

2015年(27)

我的朋友

分类: LINUX

2015-03-19 14:02:26

linux内核启动过程可以分为两个部分:架构/开发板相关代码的引导过程,后继的通用启动过程。本文将分析的是ARM架构处理器的linux内核vmlinux的启动过程,之所以强调vmlinux,是因为其它格式的内核在进行与vmlinux相同的流程之前会有一些独特的操作,比如对于压缩格式的zImage,它首先进行自解压得到vmlinux (它将调用函数decompress_kernel()解压,打印“Uncompressing Linux...”,调用gunzip(),打印"done, booting the kernel"),然后再执行vmlinux的启动过程。

内核的引导阶段代码的分析

U-Boot的类似,内核的引导阶段代码通常是使用汇编语言编写的,它首先检查内核是否支持当前的架构处理器,然后检查是否支持当前开发板。通过检查后,就为调用下一阶段的start_kernel函数作准备。主要分为两个步骤:由于连接内核时使用的是虚拟地址,故首先要设置页表,使能MMU;接着调用C函数start_kernel之前的常规工作,包括复制数据段、清除BSS段、调用start_kernel函数。

对于前面的Makfile的分析,知道内核代码入口在arch/arm/kernel/head.S是内核执行的第一个文件。U-Boot调用内核时,此时的状态MMUoffD-cacheoffI-cachedont careonoff没有关系,r0r1为“机器类型ID”,r2atags指针。另外,协处理器CP15的寄存器C0中存放的是CPU IDCPU ID中包含了处理器厂商的编号、产品子编号、ARM体系版本号、产品主编号和处理器版本号。下面分析其流程:

1)设置CPU为系统管理(SVC)模式并且禁止中断

ENTRY(stext)

msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE  

2)查询CPU ID并检查合法性

mrc p15, 0, r9, c0, c0 /*读取CPU ID,存入r9寄存器*/

bl __lookup_processor_type /*调用函数__lookup_processor_type,输入参数r9=cpuid ,返回值r5=procinfo*/

movs r10, r5 /*如果不支持当前CPU,则返回值r5=0*/

beq __error_p /*如果r5=0,则打印错误*/

在内核映像中,定义了若干proc_info_list表示它支持的CPU,对于ARM架构的CPU,这些结构的源码在arch/arm/mm/目录下。比如proc-v6.S中有如下代码,它表示的所有ARMv6架构的CPU(s3c6410ARMv6架构的)proc_info_list结构。

.section ".proc.info.init", #alloc, #execinstr

/*Match any ARMv6 processor core.*/

.type __v6_proc_info, #object

__v6_proc_info:

.long 0x0007b000 /*cpu_val*/

.long 0x0007f000 /*cpu_mask*/

……

不同的proc_info_list结构被用来支持不同的CPU,它们都是定义在“.proc.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__proc_info_begin,结束地址为__proc_info_end。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:

__proc_info_begin = .; /* proc_info_list结构的开始地址*/

*(.proc.info.init)

__proc_info_end = .;  /*proc_info_list结构的结束地址*/

__lookup_processor_type函数就是根据前面从协处理器CP15的寄存器C0中读取到CPU ID(存入r9寄存器),从这些proc_info_list结构中找出匹配。它是在arch/arm /kernel/head-common.S中定义的,代码如下:

.type __lookup_processor_type, %function

__lookup_processor_type:

adr r3, 3f           /*将标号3的实际地址加载到r3*/

ldmda r3, {r5 - r7}  /*然后将编译时生成的 __proc_info_begin虚拟地址载入到r5__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7*/

sub r3, r3, r7 /*得到物理地址和虚拟地址的差值*/

add r5, r5, r3 /* __proc_info_begin对应的物理地址*/

add r6, r6, r3 /*__proc_info_end对应的物理地址*/

1: ldmia  r5, {r3, r4} /*proc_info_list结构中cpu_valcpu_mask分别存放在r3, r4*/

and r4, r4, r9 /* r4 = cpu_mask&CPU_ID*/

teq r3, r4          /*比较*/

beq 2f            /*如果相等,找到匹配的proc_info_list结构,跳转到标

                    号2*/

add r5, r5, #PROC_INFO_SZ /*否则, r5指向下一个proc_info_list结构*/

cmp r5, r6         /*是否比较完所有的proc_info_list结构*/

blo 1b           /*没有则跳转到标号1继续比较*/

mov r5, #0 /*比较完,但是没有匹配的proc_info_list结构,r5 =0*/

2: mov pc, lr  /*函数调用完毕,返回*/

……

/*Look in include/asm-arm/procinfo.h and arch/arm/kernel/arch.[ch] for

 * more information about the __proc_info and __arch_info structures.*/

.long __proc_info_begin

.long __proc_info_end

3: .long .

.long __arch_info_begin

.long __arch_info_end

__proc_info_begin __proc_info_end和“.”这三个数据都是在连接内核时确定的,它们都是虚拟地址,前两个表示proc_info_list结构的开始地址和结束地址,“.”表示当前行代码在编译连接后的虚拟地址。因为MMU没有开启,所以我们此时还不能直接使用这些地址。所以在访问proc_info_list结构前,需要先将它的虚拟地址转化为物理地址。

__lookup_processor_type函数首先将标号3的实际地址加载到r3,然后将编译时生成的 __proc_info_begin虚拟地址载入到r5__proc_info_end虚拟地址载入到r6,标号3的虚拟地址载入到r7。由于r3r7分别存储的是同一位置标号3的物理地址和虚拟地址,所以儿者相减即得到虚拟地址和物理地址之间的offset。利用此offset,将r5r6中保存的虚拟地址转变为物理地址然后从proc_info中读出内核编译时写入的processor ID和之前从cpsr中读到的processor ID对比,查看代码和CPU硬件是否匹配。如果编译了多种处理器支持则会循环每种type依次检验,如果硬件读出的ID在内核中找不到匹配,则r50返回。

3)查询machine ID并检查合法性

bl __lookup_machine_type /*调用函数__lookup_machine_type ,返回值r5=machinfo*/

movs r8, r5 /*如果不支持当前机器(即开发板),则返回值r5=0*/

beq __error_a /*如果r5=0,则打印错误*/

 内核中对每种支持的开发板都会使用宏MACHINE_STARTMACHINE_END来定义一个machine_desc结构,它定义了开发板相关的一些属性和函数,如机器的类型ID、起始I/O物理地址、Bootloader传入的参数地址、中断初始化函数、I/O映射函数等。对于UT-S3C6410开发板在arch/arm/mach-s3c6410/ mach-smdk6410.c有如下代码:

MACHINE_START(SMDK6410, "SMDK6410")

/* Maintainer: Samsung Electronics */

.phys_io    = S3C24XX_PA_UART,  

.io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,

.boot_params    = S3C_SDRAM_PA + 0x100,

.init_irq   = s3c_init_irq, 

.map_io     = smdk6410_map_io,

.fixup      = smdk6410_fixup,

.timer      = &s3c_timer, 

.init_machine   = smdk6410_machine_init,

MACHINE_END

MACHINE_STARTMACHINE_ENDinclude/asm-arm/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 \

}; 

所以上面代码扩展开来就是

static const struct machine_desc __mach_desc_ SMDK6410

__used \

 __attribute__((__section__(".arch.info.init"))) = { \

.nr = MACH_TYPE_ SMDK6410, \

.name = “SMDK6410”,

……

}; 

不同的machine_desc结构被用来支持不同的开发板,它们都是定义在“.arch.info.init”段中,在连接内核时,这些结构被组织在一起。开始地址为__arch_info_begin,结束地址为__arch_info_end =。在arm/kernel/vmlinux.lds.S中可以看到这样的代码:

__arch_info_begin = .; /* machine_desc结构的开始地址*/

*(.arch.info.init)

__arch_info_end = .;  /*machine_desc结构的结束地址*/

U-Boot调用内核时,会在r1寄存器中给出开发板的标记即机器类型ID__lookup_machine_type函数将这个值与machine_desc结构的nr成员比较,如果两者相等则表示找到匹配的machine_desc结构,于是返回它的地址(存到r5中)。如果__arch_info_begin__arch_info_end之间所有的machine_desc结构的nr成员都不等于r1寄存器中的值,则返回0,即r5中值为。编码方法与__lookup_processor_type函数完全一样,在此不再详述。

4)检查atags合法性

   bl __vet_atags

r2为中存放的是标记列表atags指针。__vet_atags用来检查atags合法性,

.type __vet_atags, %function

__vet_atags:

tst r2, #0x3  /*检测是否地址对齐*/

bne 1f       /*没对齐则跳转到标号1*/

ldr r5, [r2, #0] /*对齐,则将标记列表第一个标记ATAG_CORE存入r5*/

subs r5, r5, #ATAG_CORE_SIZE  /* r5指向下一个标记结构*/

bne 1f   /* r50则跳转到标号1*/

ldr r5, [r2, #4] /*否则将标记的类型放入r5*/

ldr r6, =ATAG_CORE /*将代表ATAG_CORE值放入r6*/

cmp r5, r6 /*比较 r5, r6*/

bne 1f  /*不相等跳转到标号1*/

mov pc, lr /* 相等则说明atag是合法性,函数调用完毕,返回*/

1: mov r2, #0  /* atag非法,将r2置为0*/

mov pc, lr/*函数调用完毕,返回*/

5)创建一级页表

bl __create_page_tables

__create_page_tables:函数在arch/arm/kernel/head.S中实现,它完成一级页表的创建。代码如下:

/*首先将内核起始地址-0x4000到内核起始地址之间的16K存储器清0*/

mov r0, r4

mov r3, #0

add r6, r0, #0x4000

1: str r3, [r0], #4

str r3, [r0], #4

str r3, [r0], #4

str r3, [r0], #4

teq r0, r6

bne 1b

/*将proc_info中的mmu_flags加载到r7*/

ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

/*PC指针右移20位,得到内核第一个1MB空间的段地址存入r6,接着根据此值存入映射标识*/

mov r6, pc, lsr #20 @ start of kernel section

orr r3, r7, r6, lsl #20 @ flags + kernel base

str r3, [r4, r6, lsl #2] @ identity mapping

6)设置跳转地址

ldr r13, __switch_data

adr lr, __enable_mmu

   设置r13寄存器值为__switch_data,在mmu被使能即__turn_mmu_on函数执行完之后会跳转到该地址处。__switch_data段在arch/arm/kerne/ head-common.S有定义。代码如下:

__switch_data:

.long __mmap_switched

.long __data_loc @ r4

.long __data_start @ r5

.long __bss_start @ r6

.long _end @ r7

.long processor_id @ r4

.long __machine_arch_type @ r5

.long __atags_pointer @ r6

.long cr_alignment @ r7

.long init_thread_union + THREAD_START_SP @ sp

设置lr寄存器值为__enable_mmu,执行完函数__v6_setup,函数会跳转到__enable_mmu地址处。

7) 初始化 TLB, Caches, 和 MMU状态

add  pc, r10, #PROCINFO_INITFUNC

执行这条指令后,程序将会跳转到函数__v6_setup处执行。__v6_setup是在文件arch/arm/mm/proc-v6.S中实现的,它的初始化 TLB, Caches, 和 MMU状态,为下一步开启MMU作准备。然后将原来保存在lr中的地址载入pc,跳转到arch/arm/kerne/ head.S__enable_mmu处执行,代码不再详述。

8)为开启MMU前做好准备工作

在开启MMU之前还要做好最后的准备工作,比如无效IcachesDcaches,设置域访问控制器和页表指针等。这些设置是在arch/arm/kerne/ head.S文件中__enable_mmu函数中实现的.

__enable_mmu:

#ifdef CONFIG_ALIGNMENT_TRAP /*开启地址对齐检查*/

orr r0, r0, #CR_A

#else

bic r0, r0, #CR_A

#endif

#ifdef CONFIG_CPU_DCACHE_DISABLE /*无效Dcaches*/

bic r0, r0, #CR_C

#endif

#ifdef CONFIG_CPU_BPREDICT_DISABLE

bic r0, r0, #CR_Z

#endif

#ifdef CONFIG_CPU_ICACHE_DISABLE/*无效Icaches*/

bic r0, r0, #CR_I

#endif

mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \

      domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \

      domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \

      domain_val(DOMAIN_IO, DOMAIN_CLIENT))

mcr p15, 0, r5, c3, c0, 0 /*设置域访问控制器*/

mcr p15, 0, r4, c2, c0, 0 /*设置页表基址寄存器*/

9)使能MMU

b __turn_mmu_on

__enable_mmu执行完后,跳转到__turn_mmu_on,该函数会把MMU使能位写入MMU控制寄存器,使能MMU激活虚拟地址。然后将原来保存在sp中的地址载入pc,跳转到head-common.S__mmap_switched,至此代码进入虚拟地址的世界。

.align 5

.type __turn_mmu_on, %function

__turn_mmu_on:

mov r0, r0

mcr p15, 0, r0, c1, c0, 0 /*使能MMU*/

mrc p15, 0, r3, c0, c0, 0 

mov r3, r3

mov r3, r3

mov pc, r13 /*跳转到__mmap_switched*/

10) 跳转到第二阶段代码的C入口点。

在跳转到第二阶段代码的C入口点之前,需要做一些准备工作。包括复制数据段、清除BSS段、保存一些重要的信息到寄存器和内存中。这些都在函数__mmap_switched中实现。这里只粘贴部分代码:

__mmap_switched:

.type __mmap_switched, %function

__mmap_switched:

adr r3, __switch_data + 4

ldmia r3!, {r4, r5, r6, r7}

cmp r4, r5 /*复制数据段*/

1: cmpne r5, r6

ldrne fp, [r4], #4

strne fp, [r5], #4

bne 1b

mov fp, #0 /*清除BSS段*/

1: cmp r6, r7

strcc fp, [r6],#4

bcc 1b

ldmia r3, {r4, r5, r6, r7, sp}

str r9, [r4] /* processor ID保存在r9*/

str r1, [r5] /* machine ID报存在r1*/

str r2, [r6] /* atags地址保存在r2*/

bic r4, r0, #CR_A /*Clear 'A' bit*/

stmia r7, {r0, r4} /*将控制寄存器保存到r7定义的内存地址*/

然后再接下来跳入/init/main.cstart_kernel函数,它是内核第二阶段代码的入口点。

b start_kernel    /*调用start_kernel函数*/

2. start_kernel函数部分代码的分析

start_kernel函数代码如下:

asmlinkage void __init start_kernel(void)

{

char * command_line;

extern struct kernel_param __start___param[], __stop___param[];

smp_setup_processor_id();

/*Need to run as early as possible, to initialize thelockdep hash:*/

lockdep_init();

debug_objects_early_init();

cgroup_init_early();

local_irq_disable();

early_boot_irqs_off();

early_init_irq_lock_class();

/*Interrupts are still disabled. Do necessary setups, then enable them*/

lock_kernel();

tick_init();

boot_cpu_init();

page_address_init();

printk(KERN_NOTICE);

printk(linux_banner);

setup_arch(&command_line);/*后面会讲到*/

mm_init_owner(&init_mm, &init_task);

setup_command_line(command_line); /*后面会讲到*/

setup_per_cpu_areas();

setup_nr_cpu_ids();

smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */

/*Set up the scheduler prior starting any interrupts (such as the

 * timer interrupt). Full topology setup happens at smp_init()

 * time - but meanwhile we still have a functioning scheduler.

 */

sched_init();

/*

 * Disable preemption - early bootup scheduling is extremely

 * fragile until we cpu_idle() for the first time./

preempt_disable();

build_all_zonelists();

page_alloc_init();

printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);

parse_early_param();/*后面会提到*/

parse_args("Booting kernel", static_command_line, __start___param,

   __stop___param - __start___param,

   &unknown_bootoption);

if (!irqs_disabled()) {

printk(KERN_WARNING "start_kernel(): bug: interrupts were "

"enabled *very* early, fixing it\n");

local_irq_disable();

}

sort_main_extable();

trap_init();/*设置异常处理函数,很重要,见韦东山《嵌入式LINUX应用开发完全手册》 p397*/

rcu_init();

/* init some links before init_ISA_irqs() */

early_irq_init();

init_IRQ();/*设置异常处理函数,很重要,见韦东山《嵌入式LINUX应用开发完全手册》 p397*/

pidhash_init();

init_timers();

hrtimers_init();

softirq_init();

timekeeping_init();

time_init();//后面会整理

sched_clock_init();

profile_init();

if (!irqs_disabled())

printk(KERN_CRIT "start_kernel(): bug: interrupts were "

 "enabled early\n");

early_boot_irqs_on();

local_irq_enable();

/*

 * HACK ALERT! This is early. We're enabling the console before

 * we've done PCI setups etc, and console_init() must be aware of

 * this. But we do want output early, in case something goes wrong. /

console_init();/*后面会讲到*/

if (panic_later)

panic(panic_later, panic_param);

lockdep_info();

/*

 * Need to run this when irqs are enabled, because it wants

 * to self-test [hard/soft]-irqs on/off lock inversion bugs

 * too:

 */

locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD

if (initrd_start && !initrd_below_start_ok &&

    page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {

printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "

    "disabling it.\n",

    page_to_pfn(virt_to_page((void *)initrd_start)),

    min_low_pfn);

initrd_start = 0;

}

#endif

vmalloc_init();

vfs_caches_init_early();

cpuset_init_early();

page_cgroup_init();

mem_init();

enable_debug_pagealloc();

cpu_hotplug_init();

kmem_cache_init();

debug_objects_mem_init();

idr_init_cache();

setup_per_cpu_pageset();

numa_policy_init();

if (late_time_init)

late_time_init();

calibrate_delay();

pidmap_init();

pgtable_cache_init();

prio_tree_init();

anon_vma_init();

#ifdef CONFIG_X86

if (efi_enabled)

efi_enter_virtual_mode();

#endif

thread_info_cache_init();

cred_init();

fork_init(num_physpages);

proc_caches_init();

buffer_init();

key_init();

security_init();

vfs_caches_init(num_physpages);

radix_tree_init();

signals_init();

/* rootfs populating might need page-writeback */

page_writeback_init();

#ifdef CONFIG_PROC_FS

proc_root_init();

#endif

cgroup_init();

cpuset_init();

taskstats_init_early();

delayacct_init();

check_bugs();

acpi_early_init(); /* before LAPIC and SMP init */

ftrace_init();

/* Do the rest non-__init'ed, we're now alive */

rest_init();

}

start_kernel()中完成了一系列系统初始化,由于代码过量大,本文将重点介绍与内核移植相关的几部分内容:内核又对u-boot传递过来的参数的接收、开发板的初始化(静态I/O映射过程的实现,时钟初始化、中断初始化、片上设备的注册)、控制台初始化过程、设备模型建立、系统初始化(包括设备,文件系统,内核模块等)和内核启动init进程过程。其它不再详细介绍。

阅读(1904) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~