我使用的内核的版本是4.4。处理器是arm v7a内核。
内核中开启虚拟地址的地方
首先找到内核中开启虚拟地址的地方,代码在 arch/arm/kernel/head.S里。
/*
* 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
* r1 = machine ID
* r2 = atags or dtb pointer
* r9 = processor ID
* r13 = *virtual* address to jump to upon completion
*
* other registers depend on the function called upon completion
*/
.align 5
.pushsection .idmap.text, "ax"
ENTRY(__turn_mmu_on)
mov r0, r0
instr_sync
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
instr_sync
mov r3, r3
mov r3, r13
ret r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)
.popsection
观察一下这段代码,发现其中有两句 cp15的操作。”mcr p15, 0, r0, c1, c0, 0” 是把r0写入cp15的寄存器1。这里放个cp15操作有关的链接以供备忘。http://blog.sina.com.cn/s/blog_858820890102v1gc.html
从这里开始,kernel就进入了虚拟地址时代。这个时候,PC的值是mcr p15, 0, r0, c1, c0, 0这条指令的地址再+4。这个时候PC是一个虚拟地址,它会被转换成哪个物理地址?答案是就是下一条指令的物理地址!如果不是这个结果,那么内核就会取到一个无法预期的指令,从而导致程序崩溃。
虚拟地址=物理地址
内核是如何达到这个目的的呢?
观察一下 __create_page_tables
/*
* Setup the initial page tables. We only setup the barest
* amount which are required to get the kernel running, which
* generally means mapping in the kernel code.
*
* r8 = phys_offset, r9 = cpuid, r10 = procinfo
*
* Returns:
* r0, r3, r5-r7 corrupted
* r4 = physical page table address
*/
__create_page_tables:
pgtbl r4, r8 @ page table address
...
/*
* Create identity mapping to cater for __enable_mmu.
* This identity mapping will be removed by paging_init().
*/
adr r0, __turn_mmu_on_loc
ldmia r0, {r3, r5, r6}
sub r0, r0, r3 @ virt->phys offset
add r5, r5, r0 @ phys __turn_mmu_on
add r6, r6, r0 @ phys __turn_mmu_on_end
mov r5, r5, lsr #SECTION_SHIFT
mov r6, r6, lsr #SECTION_SHIFT
...
ENDPROC(__create_page_tables)
adr r0, __turn_mmu_on_loc 这条命令把__turn_mmu_on_loc这个标号的物理地址装入r0,然后从这块内存中读3个word到r3,r5,r6 中。那么这3个word里存放的是什么呢?
从System.map 文件中可以知道,__turn_mmu_on_loc标号的虚拟地址是c0008138
c0008138 t __turn_mmu_on_loc
从 vmlinux 的objdump 出来的结果可以知道,在这个地方存放的是3个地址,分别是__turn_mmu_on_loc,__turn_mmu_on,__turn_mmu_on_end的虚拟地址,
c0008138 <__turn_mmu_on_loc>:
c0008138: c0008138 andgt r8, r0, r8, lsr r1
c000813c: c0008280 andgt r8, r0, r0, lsl #5
c0008140: c00082a0 andgt r8, r0, r0, lsr #5
那么 sub r0, r0, r3 @ virt->phys offset 的意义就很明确了,就是求出__turn_mmu_on_loc这个标号的物理地址和虚拟地址之间的偏移量。然后根据这个偏移量求出__turn_mmu_on的物理地址,再把这个物理地址放到地址映射表里去。这样,当MMU开始工作以后,物理地址和虚拟地址是相同的,不会导致PC+4以后取到非法地址的问题。
转入0xc0000000虚拟地址
在执行__turn_mmu_on之前,r13,也就是sp的值已经被设置为 __mmap_switched 的虚拟地址。在__turn_mmu_on的最后阶段,通过ret r3 这条命令把__mmap_switched的虚拟地址装入PC,ret指令返回时就直接返回到__mmap_switched 标号,从而走上了虚拟地址的金光大道。在这之后,内核都运行在 0xc0000000 以上的虚拟地址空间。那么r13又是什么时候装载虚拟地址的呢?在stext段中,在调用 __mmu_enable之前,__mmap_switched 就被装入 r13了。
*
* The processor init function will be called with:
* r1 - machine type
* r2 - boot data (atags/dt) pointer
* r4 - translation table base (low word)
* r5 - translation table base (high word, if LPAE)
* r8 - translation table base 1 (pfn if LPAE)
* r9 - cpuid
* r13 - virtual address for __enable_mmu -> __turn_mmu_on
*
* On return, the CPU will be ready for the MMU to be turned on,
* r0 will hold the CPU control register value, r1, r2, r4, and
* r9 will be preserved. r5 will also be preserved if LPAE.
*/
ldr r13, =__mmap_switched @ address to jump to after
@ mmu has been enabled
badr lr, 1f @ return (PIC) address
#ifdef CONFIG_ARM_LPAE
mov r5, #0 @ high TTBR0
mov r8, r4, lsr #12 @ TTBR1 is swapper_pg_dir pfn
#else
mov r8, r4 @ set TTBR1 to swapper_pg_dir
#endif
ldr r12, [r10, #PROCINFO_INITFUNC]
add r12, r12, r10
ret r12
1: b __enable_mmu
ENDPROC(stext)
到这里为止,linux启动过程中的开启MMU,并跳转到虚拟地址空间的任务就完成了。