基于PowerPC的Linux内核之旅:第2站-__secondary_start(start_here)-上

    前面一篇的early_init执行完成后,CPU启动早期的基本初始化工作算是做完了,这时内核会开始重定向并复制运行,代码如下:

bl	reloc_offset
	mr	r26,r3
	addis	r4,r3,KERNELBASE@h	/* current address of _start */
	lis	r5,PHYSICAL_START@h
	cmplw	0,r4,r5			/* already running at PHYSICAL_START? */
	bne	relocate_kernel     /*Juan内核重定向,经典启动必备*/

    这里的第一句mr是将当前偏移量保存在r26中,后面relocate_kernel会使用。之后内核会判断是否需要重定向,KERNELBASE为内核的虚拟起始地址,PHYSICAL_START为内核的实际起始地址,而内核则必须要从物理地址运行start函数。下面是relocate_kernel的详细代码:

relocate_kernel:
	addis	r9,r26,klimit@ha	/* fetch klimit */
	lwz	r25,klimit@l(r9)   /*r25 = kilmit + offset*/
	addis	r25,r25,-KERNELBASE@h    /*最后得到的r25为内核大小*/
	lis	r3,PHYSICAL_START@h   	/* 拷贝目标基地址 */
	li	r6,0			/* 实际地址,不偏移 */
	li	r5,0x4000		/* 先拷贝 16K字节*/
	bl	copy_and_flush 
	addi	r0,r3,4f@l		/* 跳到4f */
	mtctr	r0			/* in copy and do the rest. */
	bctr				/* jump to the copy */
4:	mr	r5,r25
	bl	copy_and_flush		/* copy the rest */
	b	turn_on_mmu    /*打开MMU*/

    机制很简单,就是获取内核大小后,先拷16K,再把剩下的拷过去,然后打开MMU,打开MMU的代码和关闭的类似,这里就不再列举了,看一下拷贝函数copy_and_flush,实现的是拷贝内核到内存物理起始处,并关闭cache。代码如下:

_ENTRY(copy_and_flush)
	addi	r5,r5,-4
	addi	r6,r6,-4
4:	li	r0,L1_CACHE_BYTES/4   /*L1_CACHE_BYTES:0b10000=16*/
	mtctr	r0
3:	addi	r6,r6,4			/* copy a cache line */
	lwzx	r0,r6,r4     /*读单字(4Byte),通过Cache*/
	stwx	r0,r6,r3     /*写单字,从r4加载,存在r3*/
	bdnz	  3b     /*递减计数器,循环每次拷4个字*/
	dcbst	r6,r3			/*Data Cache Block Store,再将r3的值写到内存*/
	sync
	icbi	r6,r3			/*Instruction Cache Block Invalidate,强制清空指令Cache */
	cmplw	0,r6,r5
	blt	4b     /*循环写内存,直到写完(r6>=r5)*/
	sync				/* additional sync needed on g4 */
	isync
	addi	r5,r5,4
	addi	r6,r6,4
	blr

    这里的r4是在上面调用relocate_kernel的时候赋的值,为虚拟起始地址-偏移量(偏移量是负的,remember?),即拷贝的源地址。执行完拷贝后,内核会跳转到trun_on_mmu中,该函数在SRR0中写入了start_here的地址,执行完使能MMU后,中断返回指令自动将SRR1更新为MSR,并在新的MSR控制下将SRR0更新为PC指针,实现绝对跳转,处理器即正式跳到start_here中。在此之后,就不再有前面说的链接地址与实际运行地址不同的事情了,即访问变量时也不用加上reloc_offset了

    辛辛苦苦跳了这么久,终于到了执行内核代码的时候了!!这个函数叫start_here,代码比较长,分两段来分析,先看第一段:

start_here:
	/* ptr to current */
	lis	r2,init_task@h
	ori	r2,r2,init_task@l   /*默认初始化的task_struct结构体*/
	/* Set up for using our exception vectors */
	tophys(r4,r2)   /*获取物理地址*/
	addi	r4,r4,THREAD	/* 初始化线程的CPU相关的状态,THREAD为thread在task_struct中的偏移 */
	CLR_TOP32(r4)   /*空的??*/
	mtspr	SPRN_SPRG_THREAD,r4    /*将当前线程信息写入SPRG3*/
	li	r3,0
	mtspr	SPRN_SPRG_RTAS,r3	/* 写SPRG2为0,使其不在RTAS中 */

	/* 堆栈初始化 */
	lis	r1,init_thread_union@ha
	addi	r1,r1,init_thread_union@l
	li	r0,0
	stwu	r0,THREAD_SIZE-STACK_FRAME_OVERHEAD(r1)
/* 平台相关的初始化操作和配置MMU */
	mr	r3,r31
	mr	r4,r30
	bl	machine_init
	bl	__save_cpu_setup
	bl	MMU_init

    在该阶段,首先是一个线程和堆栈的初始化过程,Linux在完成MMU和中断向量的初始化后,将创建程序运行于init_thread_union的Stack中,它位于线性映射的第一部分。这是首要工作,即为init_task的运行做准备,先取得Task结构体地址,再将结构体的指针保存在SPRG3(系统专用)中。需要注意的是,PPC32上的数据结构必须要8K个(1<<13)字节对齐,因为堆栈的大小就是8K。

    再这之后就是板件平台相关的初始化工作,先来看machine_init,它主要实现两个功能:1、寻找当前所在的板件类型(Probe),进而确定当前处理器的ppc_md结构;2:将前期early_boot的数据保存,分析OF Tree结构,获得当前处理器的内存使用情况,创建MEMBLOCK结构,同时获得当前处理器系统在OF Tree中的其他硬件信息,如CPU频率、内部寄存器基地址、中断系统等。注意,此时的内存依旧只是少量可用。该函数就是简单的几个函数的调用,具体代码就不贴了。先是lockdep_init和udbg_early_init,这两个函数功能很简单,前者用于启动Lock Dependency Validator(内核依赖的关系表),本质上就是建立两个散列表calsshash_table和chainhash_table,并初始化全局变量lockdep_initialized,标志已初始化完成。后者用于初始化早期调试输出,可以通过配置config文件使能其中的一个,一般都是NS16550的串口打印调试,这个不是很懂,待以后研究了。然后是early_init_devtree,它用于启动时对扁平设备树(FDT)的初始化,用来获取内核前期初始化所需的启动参数和cmd_line等引导信息,后面还会调用unflatten_device_tree来解析dts文件,先看下带注释的early_init_devtree实际代码(位于Prom.c中):

void __init early_init_devtree(void *params)
{
	phys_addr_t limit;  /*内核所在物理地址*/

	/* 参数params,由machine_init传入,用于存放设备树的有效地址*/
	initial_boot_params = params;

	/* 从设备树中获取chosen节点的信息,包括
	 * platform type,initrd location及size, TCE reserve ...*/
	of_scan_flat_dt(early_init_dt_scan_chosen, NULL);

	/*初始化MEMBLOCKs并检索设备树内存节点 */
	memblock_init();
	of_scan_flat_dt(early_init_dt_scan_root, NULL);
	of_scan_flat_dt(early_init_dt_scan_memory_ppc, NULL);

	/*将bootloader传递的命令行参数保存在boot_command_line中 */
	strlcpy(boot_command_line, cmd_line, COMMAND_LINE_SIZE);
	parse_early_param(); /*解析命令行参数*/

	/*将内核和initrd使用的空间在MEMBLOCK中预留*/
	memblock_reserve(PHYSICAL_START, __pa(klimit) - PHYSICAL_START);
	/*若relocatable, 则将内存起始32k空间为中断向量预留*/
	if (PHYSICAL_START > MEMORY_START)
		memblock_reserve(MEMORY_START, 0x8000);
	/*为kdump预留64K的空间,即memblock_reserve(0, 0x10000);*/
	reserve_kdump_trampoline();
	/*为崩溃内核预留空间,代码很长,但都是在计算起始地址及长度*/
	reserve_crashkernel();
	early_reserve_mem();
	phyp_dump_reserve_mem();

	limit = memory_limit;
	if (! limit) {
		phys_addr_t memsize;
	/*确保内存大小页对齐,否则mark_bootmem()会出错 */
		memblock_analyze();
		memsize = memblock_phys_mem_size();
		if ((memsize & PAGE_MASK) != memsize)
			limit = memsize & PAGE_MASK;
	}
	/*按照memory limit裁剪memblock的区域大小*/
	memblock_enforce_memory_limit(limit);

	memblock_analyze();
	memblock_dump_all();

	DBG("Phys. mem: %llx\n", memblock_phys_mem_size());
	/*若设备树超出内存或处于崩溃的内核区,则执行搬运操作*/
	move_device_tree();
    /*使用于PPC64,在32位中为空函数*/
	allocate_pacas();

	/*获得当前系统的CPU个数,并决定当前使用哪一
	 *个作为系统的BSP(Boot Strap Processor)*/
	of_scan_flat_dt(early_init_dt_scan_cpus, NULL);
}

    主要的功能就是检查设备树的chosen节点确定设备的基本信息,然后为设备初始化一个MEMBLOCK,并预留相应空间。再来就是probe_machine,看它的字面意思就可以清楚,它是用来循环查询所有的ppc_md结构体,进而找到适合当前板件类型的结构,定义于Setup-commen.c,看下代码:

void probe_machine(void)
{
	extern struct machdep_calls __machine_desc_start;
	extern struct machdep_calls __machine_desc_end;

	/* 循环查询ppc_md 结构体的过程*/
	DBG("Probing machine type ...\n");

	for (machine_id = &__machine_desc_start;
	     machine_id < &__machine_desc_end;
	     machine_id++) {
		DBG("  %s ...", machine_id->name);
		memcpy(&ppc_md, machine_id, sizeof(struct machdep_calls));
		if (ppc_md.probe()) {
			DBG(" match !\n");
			break;
		}
		DBG("\n");
	}
	/*没找到就死循环 */
	if (machine_id >= &__machine_desc_end) {
		DBG("No suitable machine found !\n");
		for (;;);
	}

	printk(KERN_INFO "Using %s machine description\n", ppc_md.name);
}

    两个外部变量__machine_desc_*定义于vmlinux.lds.S中,结构体在mpc83xx平台上的具体实现如下(位于platform/83xx/Mpc831x_rdb.c):

define_machine(mpc831x_rdb) {
	.name			= "MPC831x RDB",
	.probe			= mpc831x_rdb_probe,
	.setup_arch		= mpc831x_rdb_setup_arch,
	.init_IRQ		= mpc831x_rdb_init_IRQ,
	.get_irq		= ipic_get_irq,
	.restart		= mpc83xx_restart,
	.time_init		= mpc83xx_time_init,
	.calibrate_decr		= generic_calibrate_decr,
	.progress		= udbg_progress,
};

    其实查询的过程mpc831x_rdb_probe很简单,就是与之前early_init_devtree保存在启动参数中的根节点的compatible属性做比对,若匹配则找到。在这个过程中,设备树还未必unflatten。之后,程序运行到setup_kdump_trampoline函数,该函数主要是为kdump创建指令备份,kdump 是一个新的、而且非常可信赖的内核崩溃转储机制。崩溃转储数据可以从一个新启动的内核的上下文中获取,而不是从已经崩溃的内核的上下文。当系统崩溃时,kdump使用kexec启动到第二个内核。第二个内核通常叫做捕获内核(capture kernel),以很小内存启动,并且捕获转储镜像。第一个内核保留了内存的一部分,第二个内核可以用来启动,其实整个就是一个热备。这个kdump的主要好处除了服务器的稳定外,还可以用于普通linux异常复位的问题定位,由于当第一个kernel死掉时保存了coredump,所以切换后可以查看coredump来确认异常复位原因。由此可知,要实现此函数,必需要CONFIG_CRASH_DUMP使能,具体含义未细究。

    最后的cpu_has_feature为一个内联函数,实际上就是检查当前CPU是否有某种特性,代码如下:

static inline int cpu_has_feature(unsigned long feature)
{
	return (CPU_FTRS_ALWAYS & feature) ||
	       (CPU_FTRS_POSSIBLE
		& cur_cpu_spec->cpu_features
		& feature);
}

    这里意为检查CPU是否有休眠功能,若有则调用ppc6xx_idle执行保存CPU的基本信息操作。其中的CPU_FTRS_ALWAYS和CPU_FTRS_POSSIBLE实际上就是枚举了CPU所有的特性,然后和之前保存的cpu_spec的features属性一一比对。另外就是ppc6xx_idle函数,之前它的类似函数init_idle_6xx就出现在了early_init阶段的__after_mmu_off函数中,该文件中的三个函数init_idle_6xx、ppc6xx_idle和power_save_ppc32_restore分别用于初始化并保存相关寄存器、使能电源休眠和从休眠中唤醒。

    这之后,machine_init就执行完了,跳转执行__save_cpu_setup函数,该函数定义于cpu_setup_6xx.S中,只用于6xx处理器,对于其他类型的处理器,该函数就是一个简单的blr指令,没有意义。该函数用于备份CPU 0状态的上下文内容,在休眠时也会被调用,它不包括Cache以及MMU的配置,主要保存HIDx和MSSCR0等寄存器的值。具体代码如下:

_GLOBAL(__save_cpu_setup)
	/* Some CR fields are volatile, we back it up all */
	/*CR共32位,被分为8段,每段4位,
	  *分别表示LT小于,GT大于,EQ等于和SO溢出*/
	mfcr	r7

	/* Get storage ptr */
	lis	r5,cpu_state_storage@h
	ori	r5,r5,cpu_state_storage@l  /*获取数组指针并保存到r5*/

	/* Save HID0 (common to all CONFIG_6xx cpus) */
	mfspr	r3,SPRN_HID0
	stw	r3,CS_HID0(r5)   /*将HID0的值保存到数组*/

	/* Now deal with CPU type dependent registers */
	mfspr	r3,SPRN_PVR  /*PVR:Processor Version Reg*/
	srwi	r3,r3,16  /*将r3右移16位,若是603则为0x8086*/

    由于后面有一个很长的代码段用来比对具体的CPU型号,这里就不再贴了,判断后的处理和上面的类似,就是先想MSSCR0的值保存,再就是HID1和HID2的。再后面的一个函数为MMU_init,它用于为内核创建基本的内存映射,包括RAM和一些I/O区域,创建页表,以及准备MMU硬件。定义于mm/Init_32.c中,删减版的代码如下:

void __init MMU_init(void)
{
	if (ppc_md.progress)  /*实际上就相当于串口输出当前过程*/
		ppc_md.progress("MMU:enter", 0x111);

	/*设定初始化MMU所能访问的地址范围,8xx为8M,601为16M,其余的为256M*/
	if (PVR_VER(mfspr(SPRN_PVR)) == 1)
		__initial_memory_limit_addr = 0x01000000;
	if (PVR_VER(mfspr(SPRN_PVR)) == 0x50)
		__initial_memory_limit_addr = 0x00800000;

	/*解析引导程序命令行参数,nobats和noltlbs*/
	MMU_setup();

       /*判断当前系统存储器区域个数*/
	if (memblock.memory.cnt > 1) {
#ifndef CONFIG_WII
		memblock.memory.cnt = 1;
		memblock_analyze();  /*只使用第一段物理地址连续的内存空间*/
		printk(KERN_WARNING "Only using first contiguous memory region");
#else
		wii_memory_fixups();
#endif
	}
   /*将第一段物理地址连续的内存空间保存到total_lowmem中*/
	total_lowmem = total_memory = memblock_end_of_DRAM() - memstart_addr;
	lowmem_end_addr = memstart_addr + total_lowmem;

#ifdef CONFIG_FSL_BOOKE
	/*用于Freescale Book-E,83xx中不用*/
	adjust_total_lowmem();
#endif /* CONFIG_FSL_BOOKE */

	if (total_lowmem > __max_low_memory) {
		total_lowmem = __max_low_memory;
		lowmem_end_addr = memstart_addr + total_lowmem;
#ifndef CONFIG_HIGHMEM
		total_memory = total_lowmem;
		memblock_enforce_memory_limit(lowmem_end_addr);
		memblock_analyze();
#endif /* CONFIG_HIGHMEM */
	}

	/*初始化MMU 硬件*/
	MMU_init_hw();  /*ppc_mmu_32.c中,初识化mmu硬件*/

	/*将RAM全部映射到KERNELBASE */
	mapin_ram();

	/* Initialize early top-down ioremap allocator */
	ioremap_bot = IOREMAP_TOP;
}

    对于memblock的操作,在前面的early_init_devtree中就已经见过了,操作基本上都是大同小异。对于83xx处理器的系统,MMU硬件初始化的函数MMU_init_hw相对复杂,除了常规的flush指令缓存之外,还要初始化Hash表,补全hash_low_32.S中的指令。

    至于e300体系下的MMU的硬件机制,其初始化是段很长的过程,我会将它和最后的真正打开MMU放在一起再细细分析一下。

阅读更多
想对作者说点什么?

博主推荐

换一批

没有更多推荐了,返回首页