linux 内存管理 --- MMU 未开启时的内存映射内核页表建立

在arm平台下,zImage.bin压缩镜像是由bootloader加载到物理内存,然后跳到zImage.bin里一段程序,它专门用于将被压缩的kernel解压缩到KERNEL_RAM_PADDR开始的一段内存中,接着跳进真正的kernel去执行,内核总是驻留在内存中。该kernel的执行起点是stext函数,定义于arch/arm/kernel/head.S。

在分析stext函数前,先介绍此时内存的布局如下图所示

 image 放在起始物理地址PHYS_OFFSET偏移 TEXT_OFFSET 的位置

 

在开发板tqs3c2440中,SDRAM连接到内存控制器的Bank6中,它的开始内存地址是0x30000000,大小为64M,即0x20000000。 ARM Linux kernel将SDRAM的开始地址定义为PHYS_OFFSET。经bootloader加载kernel并由自解压部分代码运行后,最终kernel被放置到KERNEL_RAM_PADDR(=PHYS_OFFSET + TEXT_OFFSET,即0x30008000,TEXT_OFFSET为0x00008000)地址上的一段内存,经此放置后,kernel代码以后均不会被移动。

在进入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空间的页表,就是按照这个关系来建立。

之所以较早提及arm linux 的内存映射,原因是在进入kernel代码,里面所有符号地址值为清一色的0xCXXXXXXX地址,而此时ARM未开启MMU功能,故在执行stext函数第一条执行时,它的PC值就是stext所在的内存地址(即物理地址,0x30008000)。因此,下面有些代码,需要使用地址无关技术(PIC)

 

 以下代码是居于 IMX6ULL ARMv7 A7

ENTRY(stext)

    @ ensure svc mode and all interrupts masked
    safe_svcmode_maskall r9
// 设置 CPU 进入 SVC 模式,并且屏蔽所有中断
    mrc    p15, 0, r9, c0, c0        @ get processor id
    bl    __lookup_processor_type        @ r5=procinfo r9=cpuid
// 有一个struct
proc_info_list 类型的数组,根据读取到的processor id,获得对应成员的地址
    movs    r10, r5                @ invalid processor (r5=0)?
    beq    __error_p            @ yes, error 'p'
    adr_l    r8, _text            @ __pa(_text)
// 对于 IMX6ULL,由于PHYS_OFFSET == PAGE_OFFSET == 0x80000000,所以不需要通过 __pa(_text)获取物理地址;adr相对寻找,r8得到kernel代码起始地址
sub r8, r8, #TEXT_OFFSET @ PHYS_OFFSET // r8 = 0x80008000 - 0x00008000,即 PHYS_OFFSET /* * r1 = machine no, r2 = atags or dtb, * r8 = phys_offset, r9 = cpuid, r10 = procinfo */ bl __vet_atags bl __fixup_smp bl __fixup_pv_table bl __create_page_tables
TEXT_OFFSET = 0x00008000
KERNEL_OFFSET = 0x80000000
PG_DIR_SIZE = 0x4000
PHYS_OFFSET = 0x80000000
r10 指向 proc_info_list 结构体类型数据
#define KERNEL_RAM_VADDR    (KERNEL_OFFSET + TEXT_OFFSET)
内核 image 是放在以 KERNEL_RAM_VADDR 作为起始地址的内存,(KERNEL_RAM_VADDR - PG_DIR_SIZE) ~ KERNEL_RAM_VADDR 是存放初始页表 swapper_pg_dir

内核启动过程中最先使用的 section map(段映射),只有一级页表,共有4096个页表项,存储在 swapper_page_table。每个页表项对应的内存大小为1M,即一页为1M。arm32后面会使用2级页表,一页为4K。

__create_page_tables:
    pgtbl    r4, r8                @ page table address
// r4 = 0x80004000,swapper page table 起始地址
    /*
     * Clear the swapper page table
     */
    mov    r0, r4
    mov    r3, #0
    add    r6, r0, #PG_DIR_SIZE
// r6 = 0x80008000,是 swapper page table 结束地址
1: str r3, [r0], #4
// r3 = [r0], r0 += 4
str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b // 上面代码作用:把内存地址 0x80004000~0x80008000 的内容清零 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags // r7 = [r10 + PROCINFO_MM_MMUFLAGS],即 r7 = __cpu_mm_mmu_flags /* * Create identity mapping to cater for __enable_mmu. * This identity mapping will be removed by paging_init(). */ adr_l r5, __turn_mmu_on @ _pa(__turn_mmu_on) adr_l r6, __turn_mmu_on_end @ _pa(__turn_mmu_on_end) mov r5, r5, lsr #SECTION_SHIFT
// r5 = r5 >> 20, 2级页表是20,打开MMU代码的起始一级页表项 mov r6, r6, lsr #SECTION_SHIFT // r6 是打开MMU代码的结束一级页表项
1: orr r3, r7, r5, lsl #SECTION_SHIFT @ r3 = mmu_flags | 页表项 str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
// [r4 + (r5 << 2)] = r3,一次性写入4个字节,所以要左移2位,r5表示的是第几个页表项(一共4096个) cmp r5, r6
// r5<r6时,CPSR.C=0 addlo r5, r5, #
1 @ next section
// lo表示CPSR.C==0时执行add
// r5 += 1,使用的时候左移2,相当于每次地址加4, 0<<2=0, 1<<2=4, 2<<2=8,3<<2=12, 4<<4=16, 5<<2=20 blo 1b // 上面代码作用:为“打开MMU的代码”的内存地址建立页表项
/* * The main matter: map in the kernel using section mappings, and * set two variables to indicate the physical start and end of the * kernel. */ add r0, r4, #KERNEL_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
// KERNEL_OFFSET 是存放 kernel image 虚拟地址首地址
// r0 = 0x80004000 + 0x80000000>>18,作为起始页表项的地址(addr>>20得到第几个页表项,由于一个页表项占4个字节,所以addr>>18是偏移地址) ldr r6,
=(_end - 1)
// _end是kernel代码结束位置 adr_l r5, kernel_sec_start @ _pa(kernel_sec_start)
str r8, [r5] @ Save physical start of kernel (LE)
// [kernel_sec_start] = 0x80000000 orr r3, r8, r7 @ Add the MMU flags
// r3 = 0x80000000 | mmu_flags add r6, r4, r6, lsr #(SECTION_SHIFT
- PMD_ORDER)
// r6 = 0x80004000 + kernel代码结束位置的高14位,作为结束页表项的地址
1: str r3, [r0], #1 << PMD_ORDER
// [r0] = r3, r0 += 4 add r3, r3, #
1 << SECTION_SHIFT
// r3作为页表项,更新其地址信息 cmp r0, r6
//和上面一段代码不同,这里是判断“当前页表项所在地址”是否等于“页表项结束地址” bls 1b eor r3, r3, r7 @ Remove the MMU flags
// 异或 adr_l r5, kernel_sec_end @ _pa(kernel_sec_end)
str r3, [r5] @ Save physical end of kernel (LE) // [kernel_sec_end] = kernel结束代码位置
// 上面代码作用:为0x80000000 ~ kernel结束代码位置之间的内存地址建立页表项,填入swapper page table的某个偏移位置(KERNEL_OFFSET >> (SECTION_SHIFT - PMD_ORDER))
// 感觉第二段建立的页表项包含了第一段代码建立的页表项,是么???? /* * Then map boot params address in r2 if specified. * We map 2 sections in case the ATAGs/DTB crosses a section boundary. */ mov r0, r2, lsr #SECTION_SHIFT
// r0 = r2 >> 20, r2是dtb物理首地址 cmp r2, #
0 ldrne r3, =FDT_FIXED_BASE >> (SECTION_SHIFT - PMD_ORDER)
// r3 = (FDT_FIXED_BASE >> 18),FDT_FIXED_BASE是设备树虚拟地址首地址 addne r3, r3, r4
// r3 = r3 + r4, r3 作为页表首地址 orrne r6, r7, r0, lsl #SECTION_SHIFT
// r6 = mmu_flags | 页表项 strne r6, [r3], #
1 << PMD_ORDER
// [r3] = r6, r3 += 4 addne r6, r6, #
1 << SECTION_SHIFT strne r6, [r3] // 上面代码作用:为dtb的内存地址建立页表项

// 以上三个都是只有一级页表项
ret lr ENDPROC(__create_page_tables)
 恒等映射(identity mapping):开启MMU的代码是恒等映射,即物理地址等于虚拟地址。由于芯片是多级流水,多条指令会被预取到流水线中,开启MMU后,预取的指令会以虚拟地址来访问
    bl    __create_page_tables

    /*
     * The following calls CPU specific code in a position independent
     * manner.  See arch/arm/mm/proc-*.S for details.  r10 = base of
     * xxx_proc_info structure selected by __lookup_processor_type
     * above.
     *
     * 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
 
mov r8, r4 @ set TTBR1 to swapper_pg_dir ldr r12, [r10, #PROCINFO_INITFUNC]
// ????

  add r12, r12, r10
// ????不理解为什么 r12 + r10
  ret    r12
// 跳到 r12 对应的函数执行,由于之前已经设置了lr,即设置的返回地址
1:    b    __enable_mmu
ENDPROC(stext)

 __enable_mmu ---> __turn_mmu_on ---> __mmap_switched ---> start_kernel( c 函数 )

 
 
stext刚开始的代码都是在 MMU 未开启的情况下,PC指向stext第一个指令的物理地址,stext的物理地址和虚拟地址可能存在不一样的情况(IMX6ULL一样),指令如果出现绝对寻址,地址都是虚拟地址,所以要保证PC能正常取指执行,需保证stext在 MMU 未开启的代码都是地址无关代码(相对寻址),即保证了PC不会使用虚拟地址,等 MMU 开启后遇到虚拟地址也会被转换为物理地址
 
struct mm_struct init_mm = {
    .mm_rb        = RB_ROOT,
    .pgd        = swapper_pg_dir,
    .mm_users    = ATOMIC_INIT(2),
    .mm_count    = ATOMIC_INIT(1),
    .write_protect_seq = SEQCNT_ZERO(init_mm.write_protect_seq),
    MMAP_LOCK_INITIALIZER(init_mm)
    .page_table_lock =  __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock),
    .arg_lock    =  __SPIN_LOCK_UNLOCKED(init_mm.arg_lock),
    .mmlist        = LIST_HEAD_INIT(init_mm.mmlist),
    .user_ns    = &init_user_ns,
    .cpu_bitmap    = CPU_BITS_NONE,
    I

 

 

 

 

posted @ 2023-06-10 21:50  流水灯  阅读(286)  评论(0编辑  收藏  举报