arm-linux内核start_kernel之前启动分析另外2篇博文链接地址如下:
http://blog.csdn.net/skyflying2012/article/details/41344377
http://blog.csdn.net/skyflying2012/article/details/48054417
bl __create_page_tables
kernel版本:3.4.55
看看kernel启动初期,开启MMU之前如何初始化页表。此处分析过程我都写在对应的代码处,方便查看。
#ifdef CONFIG_ARM_LPAE
/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE 0x5000
#define PMD_ORDER 3
#else
#define PG_DIR_SIZE 0x4000
#define PMD_ORDER 2
#endif
.....
.macro pgtbl, rd, phys
add \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
.endm
.....
__create_page_tables:
//据上篇博文分析,r8存储着sdram的物理起始地址(我的板子0x80000000)
//pgtbl宏获取0x80008000之下16K的地址空间作为页表空间
//arm页表一页是4 bytes,完成虚拟地址空间4GB中1MB的映射,
//一共需要4 x 4096 bytes的页表空间
//可以看出,单页完成的是虚拟地址和物理地址高12位的转换。
//低20位的地址(1M内的地址)偏移是一致的。
pgtbl r4, r8 @ page table address
/*
* Clear the swapper page table
*/
//按照16bytes一页将16K页表空间清空
mov r0, r4
mov r3, #0
add r6, r0, #PG_DIR_SIZE
1: str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
//如果定义CONFIG_ARM_LPAE,在PGD与PMD之前还要再加一级页表,这里不详解这种情景
#ifdef CONFIG_ARM_LPAE
/*
* Build the PGD table (first level) to point to the PMD table. A PGD
* entry is 64-bit wide.
*/
mov r0, r4
add r3, r4, #0x1000 @ first PMD table address
orr r3, r3, #3 @ PGD block type
mov r6, #4 @ PTRS_PER_PGD
mov r7, #1 << (55 - 32) @ L_PGD_SWAPPER
1: str r3, [r0], #4 @ set bottom PGD entry bits
str r7, [r0], #4 @ set top PGD entry bits
add r3, r3, #0x1000 @ next PMD table
subs r6, r6, #1
bne 1b
add r4, r4, #0x1000 @ point to the PMD tables
#endif
//据上篇博文分析,r10中存储该CPU的processor_type_list(处理器信息结构体),获取该CPU的mmuflags
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
//首先建立包含turn_mmu_on函数1M空间的平映射(virt addr = phy addr)
//turn_mmu_on距stext不远,所以实际完成0x8000000-0x81000000空间的平映射
//老方法,上篇博文分析过,获取phy到virt的offset
adr r0, __turn_mmu_on_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @ virt->phys offset
//获取turn_mmu_on的首尾物理地址
add r5, r5, r0 @ phys __turn_mmu_on
add r6, r6, r0 @ phys __turn_mmu_on_end
//因1页映射1M空间,所以SECTION_SHIFT为20
//右移20位后,r5,r6代表该段地址空间的物理地址页号,因为是平映射,也代表了页表中的须知下标,即虚拟地址页号!
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
//r5左移20位,获取该页基地址,或上CPU的mmuflags,存在r3中
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
//将r3值存储在页表空间(r4起始)的(r5<<4)的页表中
//因一页用4bytes表示,所以PMD_ORDER=2
str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
//r5与r6之前相距多个1M,则需要填写多个页表。
//因turn_mmu_on函数很短,所以肯定在1M内,该处r5=r6
cmp r5, r6
addlo r5, r5, #1 @ next section
blo 1b
//从上面这次填页表的过程可以看出,16KB的页表以虚拟地址页号为寻址下标,覆盖整个虚拟的4G地址空间
/*
* Now setup the pagetables for our kernel direct
* mapped region.
*/
//接下来以多个1M的线性映射页表,建立kernel整个镜像的线性映射,((0x80000000-0x80000000+kernel_end)-(0xc0000000-0xc0000000+kernel_end))
//开启MMU之后就实现了链接地址(0xc0008000)与运行地址(0xc0008000)的统一
//这里有一个小技巧,利用当前PC值作为内核物理地址起始,create_page_tables距离内核起始地址不超过1MB,因此移位之后就是内核起始的物理页号。
//arm的create_page_tables中,不管是turn_mmu_on还是这里,都是使用的当前pc值计算物理页号,
//这样的好处是,不管内核加载到什么物理地址,都可以迅速的建立正确的页表映射。并且不需要内核开发人员对这部分代码进行修改
mov r3, pc
mov r3, r3, lsr #SECTION_SHIFT
orr r3, r7, r3, lsl #SECTION_SHIFT
//将该1M空间的物理起始地址存储到页表中相应虚拟地址页中
add r0, r4, #(KERNEL_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
str r3, [r0, #((KERNEL_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #1 << PMD_ORDER
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1: cmp r0, r6
add r3, r3, #1 << SECTION_SHIFT
strls r3, [r0], #1 << PMD_ORDER
bls 1b
#ifdef CONFIG_XIP_KERNEL
/*
* Map some ram to cover our .data and .bss areas.
*/
add r3, r8, #TEXT_OFFSET
orr r3, r3, r7
add r0, r4, #(KERNEL_RAM_VADDR & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> (SECTION_SHIFT - PMD_ORDER)]!
ldr r6, =(_end - 1)
add r0, r0, #4
add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
#endif
/*
* Then map boot params address in r2 or the first 1MB (2MB with LPAE)
* of ram if boot params address is not specified.
*/
//将atags的1M地址空间做线性映射,方便start_kernel中对args进行分析
//据上篇博文分析,r2中存储着bootloader传来的atag基地址(我的板子在0x80000100)
//所以该1M空间是0x80000000-0x81000000,映射到0xc0000000-0xc1000000
mov r0, r2, lsr #SECTION_SHIFT
movs r0, r0, lsl #SECTION_SHIFT
moveq r0, r8
sub r3, r0, r8
add r3, r3, #PAGE_OFFSET
add r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
orr r6, r7, r0
str r6, [r3]
//如果需要早期串口输出进行调试,在这里进行I/O空间的映射,从而实现可以对串口控制器的操作,这里不详解了。
#ifdef CONFIG_DEBUG_LL
#if !defined(CONFIG_DEBUG_ICEDCC) && !defined(CONFIG_DEBUG_SEMIHOSTING)
/*
* Map in IO space for serial debugging.
* This allows debug messages to be output
* via a serial console before paging_init.
*/
addruart r7, r3, r0
mov r3, r3, lsr #SECTION_SHIFT
mov r3, r3, lsl #PMD_ORDER
add r0, r4, r3
rsb r3, r3, #0x4000 @ PTRS_PER_PGD*sizeof(long)
cmp r3, #0x0800 @ limit to 512MB
movhi r3, #0x0800
add r6, r0, r3
mov r3, r7, lsr #SECTION_SHIFT
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
orr r3, r7, r3, lsl #SECTION_SHIFT
#ifdef CONFIG_ARM_LPAE
mov r7, #1 << (54 - 32) @ XN
#else
orr r3, r3, #PMD_SECT_XN
#endif
1: str r3, [r0], #4
#ifdef CONFIG_ARM_LPAE
str r7, [r0], #4
#endif
add r3, r3, #1 << SECTION_SHIFT
cmp r0, r6
blo 1b
#else /* CONFIG_DEBUG_ICEDCC || CONFIG_DEBUG_SEMIHOSTING */
/* we don't need any serial debugging mappings */
ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
#endif
#if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
/*
* If we're using the NetWinder or CATS, we also need to map
* in the 16550-type serial port for the debug messages
*/
add r0, r4, #0xff000000 >> (SECTION_SHIFT - PMD_ORDER)
orr r3, r7, #0x7c000000
str r3, [r0]
#endif
#ifdef CONFIG_ARCH_RPC
/*
* Map in screen at 0x02000000 & SCREEN2_BASE
* Similar reasons here - for debug. This is
* only for Acorn RiscPC architectures.
*/
add r0, r4, #0x02000000 >> (SECTION_SHIFT - PMD_ORDER)
orr r3, r7, #0x02000000
str r3, [r0]
add r0, r4, #0xd8000000 >> (SECTION_SHIFT - PMD_ORDER)
str r3, [r0]
#endif
#endif
#ifdef CONFIG_ARM_LPAE
sub r4, r4, #0x1000 @ point to the PGD table
#endif
mov pc, lr
ENDPROC(__create_page_tables)
.ltorg
.align
__turn_mmu_on_loc:
.long .
.long __turn_mmu_on
.long __turn_mmu_on_end
create_page_table完成了3种地址映射的页表空间填写:
(1)turn_mmu_on所在1M空间的平映射物理地址空间和虚拟地址空间映射关系图如下:
(1)为什么turn_mmu_on要做平映射?
turn_mmu_on我会在下一篇博文中分析,主要是完成开启MMU的操作。
那为什么将turn_mmu_on处做一个平映射?
可以想象,执行开启MMU指令之前,CPU取指是在0x80008000附近turn_mmu_on中。
如果只是做kernel image的线性映射,执行开启MMU指令后,CPU所看到的地址就全变啦。
turn_mmu_on对于CPU来说在0xc0008000附近,0x80008000附近对于CPU来说已经不可预知了。
但是CPU不知道这些,它只管按照地址一条条取指令,执行指令。
所以不做turn_mmu_on的平映射(virt addr = phy addr),turn_mmu_on在开启MMU后的运行是完全不可知。
完成turn_mmu_on的平映射,我们可以在turn_mmu_on末尾MMU已经开启稳定后,修改PC到0xc0008000附近,就可以解决从0x8xxxxxxx到0xcxxxxxxx的跳转。
(2)kernel image加载地址为什么会在0x****8000?
分析了kernel image线性映射部分,这个就好理解了,
kernel编译链接时的入口地址在0xc0008000(PAGE_OFFSET + TEXT_OFFSET),但其物理地址不等于其链接的虚拟地址,image的线性映射实现其运行地址等于链接地址。
kernel的每一页表映射1M,所以入口处在(0x80000000-->0xc0000000)映射页表中完成映射。物理地址和虚拟地址的1M内偏移必须一致呀。
kernel定义的TEXT_OFFSET = 0x8000.所以加载的物理地址必须为0x****8000.
这样,开启MMU后,访问0xc0008000附近指令,MMU根据TLB才能正确映射找到0x****8000附近的指令。
(3)atags跟kernel入口是在同一1M空间内,bootparams的线性映射操作是否多余?
根据第二个问题的分析,kernel image可以加载到任何sdram地址空间的0x****8000即可。
atags地址是有bootloader中指定,然后告诉kernel。
那就有这样一种情况,加入sdram起始地址为0x80000000,atags起始地址为0x80000100。
但kernel image我加载到0x81008000,可以看出,这时atags跟kernel image就在不同一1M空间啦
atags单独的线性映射操作还是很有必要的。
这是我想到的关于create_page_table的3个疑问,大家如果有别的疑问,欢迎留言讨论,共同学习。
今天就分析到这,页表准备就绪,只待开启MMU!