linux start kernel
2016-07-22 15:08
573 查看
查找对应arm架构下的arch/arm/kernel/vmlinux.lds.S, 找到入口点ENTRY(stext),这个stext在arch/arm/kernel/head.S中定义,定义如下:
在进入到linux kernel时,需要确认在svc模式下,而且irq和fiq都是disable状态。
1.processor id
arm的协处理器指令,通过对协处理器15 c0 c0操作获取processor id,存放到r9
adr r3, 3f
将前面标号为3位置的地址存放到r3中,这条指令获取的地址是基于pc的偏移地址,也就是运行时的地址,属于位置无关码。
ldmda r3, {r5 - r7}
r3的地址确定,所以指令运行结束后
r5存的是符号__proc_info_begin的地址;
r6存放的是符号__proc_info_end的地址;
r7存放的是符号3f的地址,这里需要注意运行地址和链接地址的区别,r7中的存放的地址是链接时确定的标号地址,r3中存放的是运行时的地址。
__proc_info_begin和__proc_info_end是在arch/arm/kernel/vmlinux.lds.S中的init代码段中:
linux kernel中使用struct proc_info_list 描述processor type,
在include/asm-arm/procinfo.h定义
以arm-926为例,可以在arch/arm/mm/proc-arm926.S中找到定义。
从上述代码中可以看到__arm926_proc_info被放在“.proc.info.init”段当中。
继续分析__lookup_processor_type,
上面的分析我们可以知道r3中存储的是3f处的物理地址,而r7存储的是3f处的虚拟地址,这一行是计算当前程序运行的物理地址和虚拟地址的差值,将其保存到r3中。
将r5,r6存储的虚拟地址转换成物理地址,
对照struct proc_info_list结构体定义,可以知道r3,r4分别保存__arm926_proc_info中的cpu_val和cpu_mask;
r9中存储了processor id,与r4的cpu_mask进行逻辑与得到我们需要的值,然后和r3的cpu_val进行比较,如果相等则找到对应的processor id,然后返回。
如果没有找到,则继续寻找比较下一个proc_info,直到__proc_info_end结束,然后设置r5为0并返回,
如果r5为0,则跳转至__error_p错误处理。
2. machinfo
看完processor id,我们再来看看machinfo,linux kernel使用struct machine_desc结构体描述machine type,通过MACHINE_START来定义,例如:
__lookup_machine_type的汇编函数实现,
在arch/arm/kernel/head-common.S中定义。
和分析processor type类似,
把3b处的地址存入r3中,此处是物理地址;
把3b处开始的连续地址即3b处的地址,__arch_info_begin,__arch_info_end依次存入r4,r5,r6;
计算物理地址和虚拟地址的偏移,并将r5,r6的虚拟地址转换为物理地址
获取machine_desc结构成员nr,并和r1进行比较,其中r1是uboot调用kernel入口地址时传入的参数。如果找到匹配直接返回,如果在“arch.info.init”段中都找不到,那就设置r5为0,然后返回进入错误处理。
3. vet_atags
检测bootloader传入参数链表atags的合法性。
首先检测参数链表指针是否对齐,然后检测第一个tag长度是否合法,是不是ATAG_CORE,如果正常直接返回,如果有其中某一项不正常,则将参数设置为0然后返回。
相关结构定义如下:
4.__create_page_tables
首先来看ARM MMU所支持的虚实地址转换机制,下图所示虚地址VA的[20-31]位和CP15 CR2的[14-31]位共同构成一个地址,这个虚拟地址里存放的是一级页表项,表项中section base address对应virtual address的table index,由此找到虚拟地址对应的物理地址。
下面看下代码是如何实现创建页表的,函数定义如下:
为页表存放预留16K区域并清零,后面需要通过协处理器指令进行设置。
获得proc_info_list的__cpu_mm_mmu_flags的值,并存储到 r7中。
通过PC值高12位得到kernel的section基地址,
r3 = r7 | (r6 << 20) @ kernel base + mmu flag
设置页表项,并将该页表项数据存放对应的页表中,具体是 *(r4 + r6 << 2) = r3
r0 存放转换表的起始位置
r6 存放kernel_end虚拟地址
此后依次递增
计算最后一条存放的地址,存放在r6中
从开始位置到结束,依次填充页表项,一个页表项代表了1MB空间的映射关系。XIP宏相关的映射我们直接跳过。
设置RAM的第一MB虚拟地址的页表,映射完成之后如下图所示
4.调用平台特定的 __cpu_flush 函数
mmu页表配置完成后,在开启mmu前还需要完成很多操作,比如清除ICache,DCache,wrtiebuffer,TLB等,这些都可以通过CP15来完成。
代码如下:
分别设置sp,lr,pc,注意r10存储的是proc info的基地址, PROCINFO_INITFUNC 宏指xiang的是proc_info_list结构体中的成员函数__cpu_flush,这里也就是 b __arm926_setup函数
使数据cache,指令cache无效;使write buffer无效;使数据TLB,指令TLB无效;
获取arm926_crval地址,并存入r5,存入该地址连续8字节数据分别到r5, r6,
r5 = 0x00007f3f, r6 = 0x00003135.
通过cp15获取操作寄存器的值,并对mmu进行设置,然后返回进入__enable_mmu.
mmu的具体操作我们不在关心,这里主要看下最后一条指令
由此进入__switch_data,看下__switch_data的定义,
明显可以看出会调用到__mmap_switched.
这里检测data段存储位置和数据开始位置是否不相等,是否需要搬运数据。清除bss段,并保存相关参数,最后进入start_kernel.
.section ".text.head", "ax" .type stext, %function ENTRY(stext) msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode @ and irqs disabled mrc p15, 0, r9, c0, c0 @ get processor id bl __lookup_processor_type @ r5=procinfo r9=cpuid movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p' bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error 'a' bl __vet_atags bl __create_page_tables
在进入到linux kernel时,需要确认在svc模式下,而且irq和fiq都是disable状态。
1.processor id
mrc p15, 0, r9, c0, c0 @ get processor id
arm的协处理器指令,通过对协处理器15 c0 c0操作获取processor id,存放到r9
.type __lookup_processor_type, %function __lookup_processor_type: adr r3, 3f ldmda r3, {r5 - r7} sub r3, r3, r7 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b mov r5, #0 @ unknown processor 2: mov pc, lr /* * This provides a C-API version of the above function. */ ENTRY(lookup_processor_type) stmfd sp!, {r4 - r7, r9, lr} mov r9, r0 bl __lookup_processor_type mov r0, r5 ldmfd sp!, {r4 - r7, r9, pc} /* * 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
adr r3, 3f
将前面标号为3位置的地址存放到r3中,这条指令获取的地址是基于pc的偏移地址,也就是运行时的地址,属于位置无关码。
ldmda r3, {r5 - r7}
r3的地址确定,所以指令运行结束后
r5存的是符号__proc_info_begin的地址;
r6存放的是符号__proc_info_end的地址;
r7存放的是符号3f的地址,这里需要注意运行地址和链接地址的区别,r7中的存放的地址是链接时确定的标号地址,r3中存放的是运行时的地址。
__proc_info_begin和__proc_info_end是在arch/arm/kernel/vmlinux.lds.S中的init代码段中:
__proc_info_begin = .; *(.proc.info.init) __proc_info_end = .;
linux kernel中使用struct proc_info_list 描述processor type,
在include/asm-arm/procinfo.h定义
struct proc_info_list { unsigned int cpu_val; unsigned int cpu_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 int elf_hwcap; const char *cpu_name; struct processor *proc; struct cpu_tlb_fns *tlb; struct cpu_user_fns *user; struct cpu_cache_fns *cache; };
以arm-926为例,可以在arch/arm/mm/proc-arm926.S中找到定义。
.section ".proc.info.init", #alloc, #execinstr .type __arm926_proc_info,#object __arm926_proc_info: .long 0x41069260 @ ARM926EJ-S (v5TEJ) .long 0xff0ffff0 .long PMD_TYPE_SECT | \ PMD_SECT_BUFFERABLE | \ PMD_SECT_CACHEABLE | \ PMD_BIT4 | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ .long PMD_TYPE_SECT | \ PMD_BIT4 | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ b __arm926_setup .long cpu_arch_name .long cpu_elf_name .long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP|HWCAP_JAVA .long cpu_arm926_name .long arm926_processor_functions .long v4wbi_tlb_fns .long v4wb_user_fns .long arm926_cache_fns .size __arm926_proc_info, . - __arm926_proc_info
从上述代码中可以看到__arm926_proc_info被放在“.proc.info.init”段当中。
继续分析__lookup_processor_type,
sub r3, r3, r7
上面的分析我们可以知道r3中存储的是3f处的物理地址,而r7存储的是3f处的虚拟地址,这一行是计算当前程序运行的物理地址和虚拟地址的差值,将其保存到r3中。
add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space
将r5,r6存储的虚拟地址转换成物理地址,
ldmia r5, {r3, r4} @ value, mask and r4, r4, r9 @ mask wanted bits teq r3, r4 beq 2f add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) cmp r5, r6 blo 1b mov r5, #0 @ unknown processor 2: mov pc, lr
对照struct proc_info_list结构体定义,可以知道r3,r4分别保存__arm926_proc_info中的cpu_val和cpu_mask;
r9中存储了processor id,与r4的cpu_mask进行逻辑与得到我们需要的值,然后和r3的cpu_val进行比较,如果相等则找到对应的processor id,然后返回。
如果没有找到,则继续寻找比较下一个proc_info,直到__proc_info_end结束,然后设置r5为0并返回,
movs r10, r5 @ invalid processor (r5=0)? beq __error_p @ yes, error 'p'
如果r5为0,则跳转至__error_p错误处理。
2. machinfo
看完processor id,我们再来看看machinfo,linux kernel使用struct machine_desc结构体描述machine type,通过MACHINE_START来定义,例如:
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
__lookup_machine_type的汇编函数实现,
在arch/arm/kernel/head-common.S中定义。
.long __proc_info_begin .long __proc_info_end 3: .long . .long __arch_info_begin .long __arch_info_end /* * Lookup machine architecture in the linker-build list of architectures. * Note that we can't use the absolute addresses for the __arch_info * lists since we aren't running with the MMU on (and therefore, we are * not in the correct address space). We have to calculate the offset. * * r1 = machine architecture number * Returns: * r3, r4, r6 corrupted * r5 = mach_info pointer in physical address space */ .type __lookup_machine_type, %function __lookup_machine_type: adr r3, 3b ldmia r3, {r4, r5, r6} sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine 2: mov pc, lr
和分析processor type类似,
adr r3, 3b
把3b处的地址存入r3中,此处是物理地址;
ldmia r3, {r4, r5, r6}
把3b处开始的连续地址即3b处的地址,__arch_info_begin,__arch_info_end依次存入r4,r5,r6;
sub r3, r3, r4 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space
计算物理地址和虚拟地址的偏移,并将r5,r6的虚拟地址转换为物理地址
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type teq r3, r1 @ matches loader number? beq 2f @ found add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc cmp r5, r6 blo 1b mov r5, #0 @ unknown machine 2: mov pc, lr
获取machine_desc结构成员nr,并和r1进行比较,其中r1是uboot调用kernel入口地址时传入的参数。如果找到匹配直接返回,如果在“arch.info.init”段中都找不到,那就设置r5为0,然后返回进入错误处理。
3. vet_atags
检测bootloader传入参数链表atags的合法性。
.type __vet_atags, %function __vet_atags: tst r2, #0x3 @ aligned? bne 1f ldr r5, [r2, #0] @ is first tag ATAG_CORE? subs r5, r5, #ATAG_CORE_SIZE bne 1f ldr r5, [r2, #4] ldr r6, =ATAG_CORE cmp r5, r6 bne 1f mov pc, lr @ atag pointer is ok 1: mov r2, #0 mov pc, lr
首先检测参数链表指针是否对齐,然后检测第一个tag长度是否合法,是不是ATAG_CORE,如果正常直接返回,如果有其中某一项不正常,则将参数设置为0然后返回。
相关结构定义如下:
struct tag { struct tag_header hdr; union { struct tag_core core; struct tag_mem32 mem; struct tag_videotext videotext; struct tag_ramdisk ramdisk; struct tag_initrd initrd; struct tag_serialnr serialnr; struct tag_revision revision; struct tag_videolfb videolfb; struct tag_cmdline cmdline; struct tag_acorn acorn; struct tag_memclk memclk; } u; };
struct tag_header { u32 size; u32 tag; };
size=(sizeof(tag->tag_header)+sizeof(tag->u.core))>>2 #define ATAG_CORE_SIZE ((2*4 + 3*4) >> 2)
4.__create_page_tables
首先来看ARM MMU所支持的虚实地址转换机制,下图所示虚地址VA的[20-31]位和CP15 CR2的[14-31]位共同构成一个地址,这个虚拟地址里存放的是一级页表项,表项中section base address对应virtual address的table index,由此找到虚拟地址对应的物理地址。
下面看下代码是如何实现创建页表的,函数定义如下:
.macro pgtbl, rd ldr \rd, =(KERNEL_RAM_PADDR - 0x4000) .endm .type __create_page_tables, %function __create_page_tables: pgtbl r4 @ page table address /* * Clear the 16K level 1 swapper page table */ 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
为页表存放预留16K区域并清零,后面需要通过协处理器指令进行设置。
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
获得proc_info_list的__cpu_mm_mmu_flags的值,并存储到 r7中。
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
通过PC值高12位得到kernel的section基地址,
r3 = r7 | (r6 << 20) @ kernel base + mmu flag
设置页表项,并将该页表项数据存放对应的页表中,具体是 *(r4 + r6 << 2) = r3
/* * Now setup the pagetables for our kernel direct * mapped region. */ add r0, r4, #(KERNEL_START & 0xff000000) >> 18 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
r0 存放转换表的起始位置
ldr r6, =(KERNEL_END - 1)
r6 存放kernel_end虚拟地址
add r0, r0, #4
此后依次递增
add r6, r4, r6, lsr #18
计算最后一条存放的地址,存放在r6中
1: cmp r0, r6 add r3, r3, #1 << 20 strls r3, [r0], #4 bls 1b
从开始位置到结束,依次填充页表项,一个页表项代表了1MB空间的映射关系。XIP宏相关的映射我们直接跳过。
/* * 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] mov pc, lr .ltorg
设置RAM的第一MB虚拟地址的页表,映射完成之后如下图所示
4.调用平台特定的 __cpu_flush 函数
mmu页表配置完成后,在开启mmu前还需要完成很多操作,比如清除ICache,DCache,wrtiebuffer,TLB等,这些都可以通过CP15来完成。
代码如下:
ldr r13, __switch_data @ address to jump to after @ mmu has been enabled adr lr, __enable_mmu @ return (PIC) address add pc, r10, #PROCINFO_INITFUNC
分别设置sp,lr,pc,注意r10存储的是proc info的基地址, PROCINFO_INITFUNC 宏指xiang的是proc_info_list结构体中的成员函数__cpu_flush,这里也就是 b __arm926_setup函数
.type __arm926_setup, #function __arm926_setup: mov r0, #0 mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4 mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4 #ifdef CONFIG_MMU mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4 #endif adr r5, arm926_crval ldmia r5, {r5, r6} mrc p15, 0, r0, c1, c0 @ get control register v4 bic r0, r0, r5 orr r0, r0, r6 mov pc, lr .size __arm926_setup, . - __arm926_setup .type arm926_crval, #object arm926_crval: crval clear=0x00007f3f, mmuset=0x00003135, ucset=0x00001134
使数据cache,指令cache无效;使write buffer无效;使数据TLB,指令TLB无效;
获取arm926_crval地址,并存入r5,存入该地址连续8字节数据分别到r5, r6,
r5 = 0x00007f3f, r6 = 0x00003135.
通过cp15获取操作寄存器的值,并对mmu进行设置,然后返回进入__enable_mmu.
.type __enable_mmu, %function __enable_mmu: 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 @ load domain access register mcr p15, 0, r4, c2, c0, 0 @ load page table pointer b __turn_mmu_on /* * Enable the MMU. This completely changes the structure of the visible * memory space. You will not be able to trace execution through this. * If you have an enquiry about this, *please* check the linux-arm-kernel * mailing list archives BEFORE sending another post to the list. * * r0 = cp#15 control register * r13 = *virtual* address to jump to upon completion * * other registers depend on the function called upon completion */ .align 5 .type __turn_mmu_on, %function __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
mmu的具体操作我们不在关心,这里主要看下最后一条指令
mov pc,r13
由此进入__switch_data,看下__switch_data的定义,
.type __switch_data, %object __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
明显可以看出会调用到__mmap_switched.
.type __mmap_switched, %function __mmap_switched: adr r3, __switch_data + 4 ldmia r3!, {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 ldmia r3, {r4, r5, r6, r7, sp} str r9, [r4] @ Save processor ID str r1, [r5] @ Save machine type str r2, [r6] @ Save atags pointer bic r4, r0, #CR_A @ Clear 'A' bit stmia r7, {r0, r4} @ Save control register values b start_kernel
这里检测data段存储位置和数据开始位置是否不相等,是否需要搬运数据。清除bss段,并保存相关参数,最后进入start_kernel.
相关文章推荐
- Linux curl命令详解
- 常用的Linux组合命令备忘
- linux socket 编程(二)
- LINUX下PHP安装VLD扩展,利用opcode优化php代码
- 发布ASP.NET Core程序到Linux生产环境
- 转: Linux 技巧:让进程在后台可靠运行的几种方法
- linux系统下安装php的pcntl扩展
- linux 操作系统中cp复制命令的使用
- linux 基础学习入门 2
- 一些cmd命令和linux触摸板关启命令
- 阿里云CentOS7挂载SSD云盘的方法
- linux 基础学习入门 1
- 正确配置Linux系统ulimit值的方法【转】
- Linux进程创建:fork,vfork,exec,clone总结
- linux 操作系统中rmkdir命令的使用
- linux 操作系统中pwd命令的使用
- 教你用 google-drive-ocamlfuse 在 Linux 上挂载 Google Drive
- linux 操作系统中cd切换目录命令的使用
- linux 操作系统中mkdir创建目录命令的使用
- linux LVM 学习笔记(二)