通过上一篇Makefile我们分析到了,编译出vmlinux的第一个原材料是head.o。.
先回顾下uboot是怎么启动kernel的。uboot将kernel从flash中拷贝到sdram后,设置tag进行工作交接,然后启动内核。
theKernel (0, bd->bi_arch_number, bd->bi_boot_params);
r0 : 0
r1 : bd->bi_arch_number 机器ID
r2 : bd->bi_boot_params tag
1.分析内存va和pa映射
我们的s3c2440的sdram起始地址是0x3000,0000。make完成后,我们分析下vmlinux.lds中的链接地址如下:
. = (0xc0000000) + 0x00008000;
显而易见这不可能是物理地址,因为我们的物理地址空间没有这么大。
ARM Linux kernel将SDRAM的开始地址定义为PHYS_OFFSET。经bootloader加载kernel并由自解压部分代码运行后,最终kernel 被放置到KERNEL_RAM_PADDR(=PHYS_OFFSET + TEXT_OFFSET,即0x30008000)地址上的一段内存,经此放置后,kernel代码以后均不会被移动。
#define PHYS_OFFSET UL(0x30000000)
在进入 kernel代码前,即bootloader和自解压缩阶段,ARM未开启MMU功能。因此kernel启动代码一个重要功能是设置好相应的页表,并开启 MMU功能。为了支持MMU功能,kernel镜像中的所有符号,包括代码段和数据段的符号,在链接时都生成了它在开启MMU时,所在物理内存地址映射到 的虚拟内存地址。
以arm kernel第一个符号(函数)stext为例,在编译链接,它生成的虚拟地址是0xc0008000,而放置它的物理地址为0x30008000(PHYS_OFFSET+TEXT_OFFSET)。实际上这个变换可以利用简单的公式进行表示:va = pa – PHYS_OFFSET + PAGE_OFFSET。Arm linux最终的kernel空间的页表,就是按照这个关系来建立。
2.uboot和kernel的机器ID进行匹配
开始分析head.S
bl __lookup_machine_type #查找支持的机器ID(uboot传进来的)
//跳转到 head_common.S分析这个函数
3: .long .
.long __arch_info_begin
.long __arch_info_end
__lookup_machine_type:
adr r3, 3b //取上面3中 ‘.’的物理地址
ldmia r3, {r4, r5, r6} //r4 = __arch_info_begin,r5 = __arch_info_end都是va
sub r3, r3, r4 //得到物理地址和虚拟地址的偏差
add r5, r5, r3 //得到__arch_info_begin物理地址
add r6, r6, r3 //得到__arch_info_end物理地址
在执行1之前穿插下,得到__arch_info_begin和__arch_info_end物理地址里面到底有什么东东呢?
在arch/arm/kernel/vmlinux.lds中可以找到这两条。(注意vmlinux.lds里面的链接地址是虚拟地址)
. = (0xc0000000) + 0x00008000; #lds中的是虚拟地址
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
搜索 arch.info.init,得到下面的内容:
__attribute的用法:https://my.oschina.net/u/180497/blog/177206
__attribute__((section("section_name")))
//其作用是将作用的函数或数据放入指定名为"section_name"输入段。
//利用 GCC 的 __attribute__ 属性的 section 选项来控制数据区的基地址。
define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \ //##在C/C++中具有连接字符串的作用
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END };
继续搜索MACHINE_START:就以2410为例:
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
//解析这个宏得到下面结构体:
static const struct machine_desc __mach_desc_SMDK2410
__used \
__attribute__((__section__(".arch.info.init"))) = {
.nr = MACH_TYPE_SMDK2410, /* 机器ID,#define MACH_TYPE_SMDK2410 193*/
.name = "SMDK2410",
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
};
看到这里再回去看head.S就明白了,我们首先找到__arch_info_begin和__arch_info_end的物理地址(lds中),这段地址里面存放了linux所支持的板卡的机器ID,name,中断初始化等函数。下面必然要做的就是 uboot传入的机器ID和vmlinux.lds中__arch_info_begin段所支持的板卡类型的机器ID进行match。我们继续往下看head_common.S:
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 //r3是段开始地址,R1是uboot传入的机器ID,开始比对
beq 2f //机器ID相等,蹦到2
add r5, r5, #SIZEOF_MACHINE_DESC //没有找到,蹦到下一个,继续查找
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr // 把lr赋给PC,跳出去。
3.创建临时页表
kernel里面的所有符号在链接时,都使用了虚拟地址值。在完成基本的初始化后,kernel代码将跳到第一个C语言函数start_kernl来 执行,在哪个时候,这些虚拟地址必须能够对它所存放在真正内存位置,否则运行将为出错。为此,CPU必须开启MMU,但在开启MMU前,必须为虚拟地址到 物理地址的映射建立相应的面表。在开启MMU后,kernel指并不马上将PC值指向start_kernl,而是要做一些C语言运行期的设置,如堆栈, 重定义等工作后才跳到start_kernel去执行。在此过程中,PC值还是物理地址,因此还需要为这段内存空间建立va = pa的内存映射关系。当然,本函数建立的所有页表都会在将来paging_init销毁再重建,这是临时过度性的映射关系和页表。
前几篇文章讲了MMU工作机制。我们需要在sdram中建立页表,然后把页表的地址告诉给CP15的C2寄存器(地址转换表基地址)。
bl __create_page_tables
//跳转 __create_page_tables 函数执行
__create_page_tables:
pgtbl r4 //页表基地址
/*分析下 pgtbl 的由来
.macro pgtbl, rd
ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
*/
/*第一步*******************0x4000大小为16Kb,将此16K 清零***************************/
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, [r0], #4 //将r3中的值存到r0所指定的地址中, 同时r0=r0+4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6 //比较是否到了16kB
bne 1b //16KB没有清0结束,继续清
/*第二步*******************建立恒等映射********************************/
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] //r10指向开发板相应的proc_info元素,读出mmuflags到r7
// pc 表示当前代码运行的物理地址。以1M的方式对齐。当前映射的1M空间位为va == pa
/************************************************
*|31|.............|20|..................... |1|0*
1 0 0 0 0 0 0 0... 0 0 0 0 0
| sector |
*************************************************/
mov r6, pc, lsr #20 // r6 = (pc >> 20).start of kernel section
orr r3, r7, r6, lsl #20 // r3 = r7 |(r6 <<< 20).flags + kernel base 1M空间清零
str r3, [r4, r6, lsl #2] // R3-> (r4+(r6<<2)) ,将目录内容写入到对应页目录中
/*
/*第3步*******************kernel空间的映射***************************/
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
/*第4步*******************进行boot传入的1MB的params映射***************************/
/*
* Then map first 1MB of ram in case it contains our boot params.
*/
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0]
4.开启MMU
将页表的基地址传给协处理器cp15的C2寄存器,然后设置一些cache,开MMU操作。
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
mov r3, r3
mov r3, r3
mov pc, r13
5.__switch_data
__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 cr_alignment @ r6
.long init_thread_union + THREAD_START_SP @ sp
__mmap_switched:
adr r3, __switch_data + 4 //地址+4
ldmia r3!, {r4, r5, r6, r7} //将r4->r3地址中(__data_loc),r5->r3+4,r6->r3+8.
cmp r4, r5 //比较__data_start和__data_loc是否相等,是否需要重定位
1: cmpne r5, r6 //不相等开始copy
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 //Clear BSS (and zero fp).BSS段清0.
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
//设置 ID,栈指针。
ldmia r3, {r4, r5, r6, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r6, {r0, r4} @ Save control register values
//跳转执行 c函数
b start_kernel
部分参考: