linux 内存管理 --- MMU 未开启时的内存映射内核页表建立
在分析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
内核启动过程中最先使用的 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)
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 函数 )
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
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
2021-06-10 STM32 CM3/CM4 ------ startup.s 启动文件分析 ------ GCC RT-Thread Studio 版本
2018-06-10 串口、COM口、TTL、RS-232、RS-485区别详解