一、概述本文针对arm linux的启动过程。以后陆续会更新start_kernel()中每个函数做的事情,这样就清楚linux启动过程中都做了哪些工作。本文及以后的文章代码均采用目前linux最新版本V3.17.为了启动ARM Linux,你需要在内核之前运行一个引导程序(Boot loader).Boot loader的作用是初始化各种设备,调用Linux内核并给内核传递相关的消息。依赖于CPU体系结构的代码(如设备初始化代码等)通常都放在stage1且可以用汇编语言来实现,而stage2则通常用C语言来实现,这样可以实现复杂的功能,而且有更好的可读性和移植性。1、Stage1start.S代码结构 u-boot的stage1代码通常放在start.S文件中,用汇编语言写成,其主要代码部分如下(1)定义入口:该工作通过修改连接器脚本来完成。(2)设置异常向量(Exception Vector)。(3)设置CPU的速度、时钟频率及终端控制寄存器。(4)初始化内存控制器。(5)将ROM中的程序(即uboot的stage2的代码)复制到RAM中。(6)初始化堆栈。(7)转到RAM中执行stage2,该工作可使用指令ldr pc来完成。2、Stage2 C语言代码部分 lib_arm/board.c中的start arm boot是C语言开始的函数也是整个启动代码中C语言的主函数,同时还是整个u-boot(armboot)的主函数,该函数只要完成如下操作:(1)调用一系列的初始化函数。(2)初始化Flash设备。(3)初始化系统内存分配函数。(4)如果目标系统拥有NAND设备,则初始化NAND设备。(5)如果目标系统有显示设备,则初始化该类设备。(6)初始化相关网络设备,填写IP、MAC地址等。(7)将kernl和跟文件系统映射从flash读到ram中。(8)设定内核启动参数和调用内核。二、Linux内核入口Linux 非压缩内核的入口位于文件/arch/arm/kernel/head.S 中的stext段。该段的基地址就是压缩内核解压后的跳转地址。如果系统中加载的内核是非压缩的 Image,那么bootloader将内核从 Flash中拷贝到 RAM 后将直接跳到该地址处,从而启动 Linux 内核。stext函数定义在arch/arm/kernel/head.S,它的功能是获取处理器类型和机器类型信息,并创建临时的页表,然后开启MMU功能,并跳进第一个C语言函数start_kernel。stext函数的在前置条件是:MMU, D-cache, 关闭; r0 = 0, r1 = machine nr, r2 = atags prointer.ENTRY(stext) ARM_BE8(setend be ) //设置 CPSR 中的端标记位,大端存储。 /* 分析如下宏定义可知,内核在GCC版本大于4.0后,可通过配置宏CONFIG_THUMB2_KERNEL来选择编译成THUMB指令集内核或ARM指令集内核。在编译成ARM指令集内核时,THUMB宏定义的代码行是不可见的。同理编译成THUMB指令集内核时,ARM宏定义的代码行无效。BSYM宏是为在THUMB指令集时访问标号处理而定义的宏。#define BSYM(sym) sym。所以在arm指令集编译时,下边THUMB代码是不可见的。 */ THUMB( adr r9, BSYM(1f) ) //Kernel is always entered in ARM. THUMB( bx r9 ) //If this is a Thumb-2 kernel, THUMB( .thumb ) //switch to Thumb now. THUMB(1: ) #ifdef CONFIG_ARM_VIRT_EXT bl __hyp_stub_install#endif //确保kernel运行在SVC模式下,并且IRQ和FIRQ中断已经关闭 safe_svcmode_maskall r9 //从arm协处理器里面读到CPU ID存储到r9,这里的CPU主要是指arm架构相关的CPU型号,比如ARM9,ARM11等等。 mrc p15, 0, r9, c0, c0 //通过从__lookup_processor_type_data中获取处理器信息位置,通过轮询查找该内核是否支持该本处理器。其中r9是通过processor id。 bl __lookup_processor_type //r5保存处理器信息,若为空则出错__error_p movs r10, r5 THUMB( it eq ) beq __error_p #ifdef CONFIG_ARM_LPAE mrc p15, 0, r3, c0, c1, 4 @ read ID_MMFR0 and r3, r3, #0xf @ extract VMSA support cmp r3, #5 @ long-descriptor translation table format? THUMB( it lo ) @ force fixup-able long branch encoding blo __error_lpae @ only classic page table format#endif #ifndef CONFIG_XIP_KERNEL adr r3, 2f ldmia r3, {r4, r8} sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET) add r8, r8, r4 @ PHYS_OFFSET#else ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case#endif /* * r1 = machine no, r2 = atags or dtb, * r8 = phys_offset, r9 = cpuid, r10 = procinfo */ //检查atags parameter的有效性 bl __vet_atags#ifdef CONFIG_SMP_ON_UP bl __fixup_smp#endif#ifdef CONFIG_ARM_PATCH_PHYS_VIRT bl __fixup_pv_table#endif //创建临时页表,它所要做的工作就是将RAM基地址开始的4M空间的物理地址映射到0xC0000000开始的虚拟地址处 bl __create_page_tables //将__mmap_switched这个symbol的链接地址放在sp里面 ldr r13, =__mmap_switched //address to jump to after //把标号1处地址赋给lr //mmu has been enabled adr lr, BSYM(1f) //return (PIC) address mov r8, r4 //set TTBR1 to swapper_pg_dir ARM( add pc, r10, #PROCINFO_INITFUNC ) THUMB( add r12, r10, #PROCINFO_INITFUNC ) THUMB( mov pc, r12 )1: b __enable_mmuENDPROC(stext)//从__lookup_processor_type_data中获取处理器信息位置__lookup_processor_type: adr r3, __lookup_processor_type_data //R3指向类型数据指针 ////读取R3指向地址三个WORD数据到R4,R5,R6,R4=标号3处的虚拟地址,r5=__arch_info_begin,r6=__arch_info_end。 ldmia r3, {r4 - r6} sub r3, r3, r4 //物理地址-虚拟地址get offset between virt&phys add r5, r5, r3 //将R5的虚拟地址转换成物理地址,R5为类型数据起始地址 add r6, r6, r3 //将R6的虚拟地址转换成物理地址,R6为类型数据结束地址1: ldmia r5, {r3, r4} //从R5指向的地址读取两个WORD到R3,R4 and r4, r4, r9 //获取判断位 teq r3, r4 //判断本处理器类型是否已找到 beq 2f //若已找到,跳转到2:执行 add r5, r5, #PROC_INFO_SZ //若未找到,R5指针增到一个类型数据长度sizeof(proc_info_list) cmp r5, r6 //是否已查找完所有处理器类型 blo 1b //未查找完返回1标号处继续循环 mov r5, #0 //若未找到本处理器类型信息,返回值R5设置为NULL2: mov pc, lrENDPROC(__lookup_processor_type)//__lookup_processor_type_data数据结构定义位于文件\linux-xlnx\arch\arm\kernel\head-common.S,参见汇编代码可知,该数据结构有三个数据,数据内容参见代码注释。 .align 2 .type __lookup_processor_type_data, %object__lookup_processor_type_data: .long . //当前内存地址(此处为虚拟地址) .long __proc_info_begin //处理器类型信息起始地址 .long __proc_info_end //处理器类型信息结束地址 .size __lookup_processor_type_data, . - __lookup_processor_type_data//_proc_info_begin,__proc_info_end定义位于\linux-xlnx\arch\arm\kernel\vmlinux.lds.S,它用于存储.proc.info.init段的起始地址及结束地址。 VMLINUX_SYMBOL(__proc_info_begin) = .; \ *(.proc.info.init) \ VMLINUX_SYMBOL(__proc_info_end) = .;//.proc.info.init段定义位于\linux-xlnx\arch\arm\mm\proc-v7.S,该文件定义了本内核版本支持的ARM v7架构下处理器类型等。//检查bootloader传入的参数列表atags的合法性__vet_atags: tst r2, #0x3 //r2指向该参数链表的起始位置,此处判断它是否字对齐 bne 1f ldr r5, [r2, #0] //获取第一个tag结构的size#ifdef CONFIG_OF_FLATTREE ldr r6, =OF_DT_MAGIC //is it a DTB? cmp r5, r6 beq 2f#endif //#define??ATAG_CORE_SIZE??((2*4??+??3*4)??>>??2)??判断该tag的长度是否合法 cmp r5, #ATAG_CORE_SIZE cmpne r5, #ATAG_CORE_SIZE_EMPTY bne 1f ldr r5, [r2, #4] //??获取第一个tag结构体的标记 ldr r6, =ATAG_CORE //判断第一个tag结构体的标记是不是ATAG_CORE cmp r5, r6 bne 1f 2: mov pc, lr //正常退出1: mov r2, #0 mov pc, lr //参数链表不正确ENDPROC(__vet_atags)__mmap_switched: adr r3, __mmap_switched_data ldmia {r4, r5, r6, r7} cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6 ldrne fp, [r4], #4 strne fp, [r5], #4 bne 1b mov fp, #0 @ Clear BSS (and zero fp) 1: cmp r6, r7 strcc fp, [r6],#4 bcc 1b ARM( ldmia r3, {r4, r5, r6, r7, sp}) THUMB( ldmia r3, {r4, r5, r6, r7} ) THUMB( ldr sp, [r3, #16] ) str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type str r2, [r6] @ Save atags pointer cmp r7, #0 bicne r4, r0, #CR_A @ Clear 'A' bit stmneia r7, {r0, r4} @ Save control register values b start_kernel //跳转到start_kernel继续初始化ENDPROC(__mmap_switched)二、start kernel初始化asmlinkage __visible void __init start_kernel(void){ char * command_line; //地址指针,指向内核启动参数在内存中的位置(虚拟地址) extern const struct kernel_param __start___param[], __stop___param[]; //对记录各锁之间依赖关系的结构体进行初始化???!!! lockdep_init(); //指定当前的cpu的逻辑号,这个函数对应于对称多处理器的设置,当系统中只有一个cpu的情况,此函数为空 smp_setup_processor_id(); //用于内核的对象调试。依赖配置CONFIG_DEBUG_OBJECTS debug_objects_early_init(); //初始化防止栈溢出攻击保护的堆栈。 boot_init_stack_canary(); //一组进程的行为控制,数据结构和其中链表的初始化 cgroup_init_early(); //关闭本地中断,因为尚未对中断代码和向量表、中断处理器进行初始化,所以必须关中断 local_irq_disable(); //用来协助调试,执行当前初始启动代码时,对无效的中断激活事件输出警告 early_boot_irqs_disabled = true; //激活当前CPU boot_cpu_init(); //如果有高端内存,则初始化高端内存page_address_htable数组 page_address_init(); pr_notice("%s", linux_banner); //架构初始化相关 setup_arch(&command_line); //初始化init_mm结构体,owner指向init_task,即init_mm->owner = init_task; mm_init_owner(&init_mm, &init_task); //设置init_mm的cpu_mask mm_init_cpumask(&init_mm); //对command_line进行备份与保存 setup_command_line(command_line); //设置nr_cpu_ids变量,即cpu id最大值 setup_nr_cpu_ids(); //用于为每个cpu的per-cpu变量副本分配空间,注意这时alloc内存分配器还没建立起来,通过memblock为初始化期间的这些变量副本分配物理空间。 setup_per_cpu_areas();//参考per-cpu.c文档 //设置arm的TPIDRPRW寄存器 smp_prepare_boot_cpu(); //因为前边在setup_arch中已将节点描述符、管理区描述符和页描述符做了初始化,这里进一步的建立系统内存页区(zone)链表 build_all_zonelists(NULL, NULL); //会调用hotcpu_notifier函数。在编译选项CONFIG_HOTPLUG_CPU起作用时,这个函数才有效。这个编译选项就热插拔技术,而且是CPU的热插拔 page_alloc_init(); pr_notice("Kernel command line: %s\n", boot_command_line); //再次处理early_param定义的参数,在setup_arch调用过一次,可参考 parse_early_param(); //解析Booting kernel传进来的参数 parse_args("Booting kernel", static_command_line, __start___param,__stop___param - __start___param,-1, -1, &unknown_bootoption); //处理静态定义的jump label,执行跳转指令或是nop指令 jump_label_init(); //使用memblock分配一个启动信息的缓冲区 setup_log_buf(0); //使用bootmem分配并初始化PID散列表 pidhash_init(); //虚拟文件系统的早期初始化有函数vfs_caches_init_early()实现,主要负责dentry和inode的hashtable的初始化工作。 vfs_caches_init_early(); //对内核异常表进行排序 sort_main_extable(); //对内核陷阱异常经行初始化,ARM架构中位空函数 trap_init(); //初始化内核内存分配器,过度到伙伴系统,启动slab机制,初始化非连续内存区 //以后分配内存不能使用memblock来分配了 mm_init(); //初始化调度器数据结构并创建运行队列??? sched_init(); //禁止内核抢占,关中断 preempt_disable(); if (WARN(!irqs_disabled(), "Interrupts were enabled *very* early, fixing it\n")) local_irq_disable(); //创建idr layer的高速缓存cache,参考idr机制文档 idr_init_cache(); //初始化系统中"读-写-拷贝"同步机制??? rcu_init(); //cpu处在idle状态时关闭tick中断的优化初始化 tick_nohz_init(); //没有设置CONFIG_CONTEXT_TRACKING_FORCE宏,不会调用该函数 context_tracking_init(); //radix树的初始化,供页面查找 radix_tree_init(); //中断初始化,初始化irq_desc数组 early_irq_init(); //中断初始化,调用特定平台的中断初始化的init_irq()函数,参考early_irq_init.c init_IRQ(); //时钟滴答的初始化,见timer_init.c tick_init(); //初始化本CPU上的软件时钟相关的数据结构,注册时钟软中断TIMER_SOFTIRQ,见timer_init.c init_timers(); //高分辨率定时器框架初始化,见timer_init.c hrtimers_init(); //软中断初始化,参考软中断文档 softirq_init(); //初始化需要和时钟代码共同管理的时间相关值 timekeeping_init(); //调用平台设置的time初始化函数 time_init(); //进程调度时钟初始化 sched_clock_postinit(); //CPU性能监视机制初始化,此机制包括CPU同一时间执行指令数,cache??miss数,分支预测失败次数等性能参数??? perf_event_init(); //对内核的profile(一个内核性能调式工具)功能进行初始化,分配好存放profile信息的内存 profile_init(); //smp下跨cpu的函数传递初始化,参考smp文档 call_function_init(); WARN(!irqs_disabled(), "Interrupts were enabled early\n"); early_boot_irqs_disabled = false; //使能本地中断 local_irq_enable(); //slab分配器后期初始化 kmem_cache_init_late(); //初始化控制台,参考console驱动文档 console_init(); //检查内核恐慌标准,如果有问题,打印信息 if (panic_later) panic("Too many boot %s vars at `%s'", panic_later,panic_param); //打印lockdep调试模块信息 lockdep_info(); //锁测试 locking_selftest(); //检查initrd的位置是否符合要求#ifdef CONFIG_BLK_DEV_INITRD if (initrd_start && !initrd_below_start_ok && page_to_pfn(virt_to_page((void *)initrd_start)) min_low_pfn) { pr_crit("initrd overwritten (0x%08lx , page_to_pfn(virt_to_page((void *)initrd_start)),min_low_pfn); initrd_start = 0; }#endif //容器组的页面内存分配 page_cgroup_init(); //调试相关,参考debug_objects_early_init文档 debug_objects_mem_init(); //内存泄露检测机制的初始化 kmemleak_init(); //为zone中用于实现单一页框的特殊高速缓存设置单一页面数量 setup_per_cpu_pageset(); //一致性内存访问(NUMA)初始化,设置NUMA系统中的内存策略。arm非NUMA系统,不考虑 numa_policy_init(); //arm中为NULL,不掉用 if (late_time_init) late_time_init(); //初始化调度时钟 sched_clock_init(); //用于BogoMIPS,该值显示1jiffy内消耗多少cpu calibrate_delay(); //进程PID位图分配映射初始化 pidmap_init(); //创建anon_vma的slab缓存 anon_vma_init(); //acpi相关初始化,忽略 acpi_early_init();#ifdef CONFIG_X86 if (efi_enabled(EFI_RUNTIME_SERVICES)) efi_enter_virtual_mode();#endif#ifdef CONFIG_X86_ESPFIX64 /* Should be run before the first non-init thread is created */ init_espfix_bsp();#endif //创建进程thread_info的slab高速缓存 thread_info_cache_init(); //为对象的每个用户赋予资格 cred_init(); //进程创建机制初始化 fork_init(totalram_pages); //创建进程所需的各结构体slab缓存 proc_caches_init(); //为buffer_head结构体创建slab高速缓存 buffer_init(); //准备密钥 key_init(); //内核安全框架初始化 security_init(); //内核调试系统后期初始化,kgdb初始化 dbg_late_init(); //初始化VFS中使用的多种缓存 vfs_caches_init(totalram_pages); //创建sigqueue结构高速缓存 signals_init(); //页回写机制初始化 page_writeback_init(); //注册并初始化proc文件系统 proc_root_init(); //注册未能初始化的cgroup子系统 cgroup_init(); //注册cpuset文件系统 cpuset_init(); //初始化任务统计信息接口 taskstats_init_early(); //为管理延迟信息做准备 delayacct_init(); //检查写缓冲一致性 check_bugs(); //simple firmware interface sfi_init_late(); if (efi_enabled(EFI_RUNTIME_SERVICES)) { efi_late_init(); efi_free_boot_services(); } //trace初始化 ftrace_init(); //剩余的初始化 rest_init();}static inline void mm_init_cpumask(struct mm_struct *mm){#ifdef CONFIG_CPUMASK_OFFSTACK mm->cpu_vm_mask_var = &mm->cpumask_allocation;#endif}static void __init setup_command_line(char *command_line){ //把刚才在setup_arch()中拷贝进来的command_line,拷贝到全局变量saved_command_line和static_command_line所指向的内存单元中。 //通过memblock分配内存空间,参考memblock文档 saved_command_line = memblock_virt_alloc(strlen(boot_command_line) + 1, 0); initcall_command_line = memblock_virt_alloc(strlen(boot_command_line) + 1, 0); static_command_line = memblock_virt_alloc(strlen(command_line) + 1, 0); strcpy (saved_command_line, boot_command_line); strcpy (static_command_line, command_line);}//nr_cpu_ids是一个特殊的值,在单CPU情况下是1;而SMP情况下,又是一个全局变量,被find_last_bit函数设置. void __init setup_nr_cpu_ids(void){ nr_cpu_ids = find_last_bit(cpumask_bits(cpu_possible_mask),NR_CPUS) + 1;}//根据smp和cpu架构void __init smp_prepare_boot_cpu(void){ set_my_cpu_offset(per_cpu_offset(smp_processor_id()));}#if defined(CONFIG_SMP) && !defined(CONFIG_CPU_V6)static inline void set_my_cpu_offset(unsigned long off){ /* Set TPIDRPRW */ asm volatile("mcr p15, 0, %0, c13, c0, 4" : : "r" (off) : "memory");}#else#define set_my_cpu_offset(x) do {} while(0)#endif /* CONFIG_SMP */ 11-04 11:44