一起分析Linux系统设计思想——03内核启动流程分析(五)

    科技2025-03-18  112

    在学习资料满天飞的大环境下,知识变得非常零散,体系化的知识并不多,这就导致很多人每天都努力学习到感动自己,最终却收效甚微,甚至放弃学习。我的使命就是过滤掉大量的垃圾信息,将知识体系化,以短平快的方式直达问题本质,把大家从大海捞针的痛苦中解脱出来。

    文章目录

    7 __create_page_tables剖析(接上篇)7.1 为什么要创建临时页表7.2 创建页表过程分析7.2.1 Identity Mapping7.2.2 Direct Mapping7.2.2.1 映射内核镜像7.2.2.2 映射Boot Paras 7.3 创建细节7.3.1 新增汇编指令汇总7.3.2 代码逐行分析

    7 __create_page_tables剖析(接上篇)

    7.1 为什么要创建临时页表

    因为要使能MMU。那为什么要使能MMU呢?因为要使用虚拟地址运行程序。为什么要使用虚拟地址呢?简单的说就是灵活方便,具体的在这里就不展开了,不然就脱离主线了。链接时使用的都是虚拟地址,且不能保证所写的代码(尤其是C语言代码)都是位置无关码,因此在启动时需要尽早地启动MMU,因此需要准备好页表。

    那页表的作用是什么呢?主要功能就是给出虚拟地址到物理地址的映射关系。所以,页表存储的内容是物理地址,索引是由虚拟地址生成的页号(恒等映射是由物理地址生成的页号,这里的物理地址属于特殊的虚拟地址)。

    创建的是什么类型的页表呢?ARM的L1主页表,L1主页表也称为段页表(section page table)或粗页表。L1 主页表将4 GB 的地址空间分成若干个1 MB的段(section),因此L1页表包含4096个页表项(section entry)。每个页表项是32 bits(4 bytes)因而L1主页表占用 4096 *4 = 16k的内存空间。

    为什么要使用粗页表,不使用细页表?就一句话,自己体会:困难时期就猥琐发育,别浪~

    为什么说是临时页表?因为本函数建立的所有页表都会在paging_init函数中销毁重建,此时建立的页表只是临时过度性的映射关系和页表。白手起家,谁家不是先住小房子再换大房子呢~~ 做自己当下能力范围内的事情。

    7.2 创建页表过程分析

    先上图,有个直观的认识,其实,代码几十行就是为了达到下图的映射效果。

    7.2.1 Identity Mapping

    首先,建立恒等映射。

    什么是恒等映射?简单来说就是,映射前是啥,映射后还是啥(和没映射是一样滴,哈哈)。

    为什么要搞恒等映射?因为,开启MMU之后,所有PC指向的地址都必须经过MMU的映射之后才能使用,但是,在PC指针还未指向虚拟地址时怎么搞呢?能够想到的一个办法就是让PC指向的物理地址经过MMU之后还指向它自己,就是所谓的映射原空间,或者叫恒等映射。

    恒等映射需要映射多大空间呢?映射当前PC指向地址所在section就够了,因为汇编语言很短,PC很快就会指向虚拟地址(链接地址)运行了,1MB空间足够了。

    7.2.2 Direct Mapping

    接下来进行的是直接映射,就是将虚拟地址映射到物理地址上去。

    7.2.2.1 映射内核镜像

    这一步做的就是将内核镜像所在虚拟地址空间完全映射到物理地址空间中去。

    这里需要说明一点。虽然本意是映射内核镜像,但是内核的起始地址是0xc0008000,所在的段基址是0xc0000000。由于L1页表的粒度是1MB,因此,即使是只想映射内核镜像,也需要从0xc0000000开始映射(映射到0x30000000),这就造成一个结果:Boot Paras的空间也被顺带着映射了。

    此外,针对“内核空间第一个section和恒等映射时的section物理空间重叠”再作一个说明。用一句话概括就是 条条大路通罗马 。详细来说是系统启动代码在开启MMU后,启动代码并不会立马调用start_kernel函数(C函数),而是要做一些C语言运行前期的设置,如堆栈设置等工作。此时,PC指向的还是物理地址,因此还需要为这段内存空间建立上文提到的Identity Mapping。这样在这第一个section空间内不管cpu发出的是虚拟地址还是物理地址,都会映射到同一个存储单元。

    7.2.2.2 映射Boot Paras

    为了后续代码继续使用Boot Paras空间的数据,也需要对该空间做一个从虚拟地址到物理地址的映射。上文刚刚说到,其实内核空间映射时已经捎带脚儿地把Boot Paras地址空间也进行映射了。但是考虑到其它处理器架构或不同的内存管理情况,这里做Boot Paras的映射还是十分必要的。

    Tips:从中我们可以总结出提高代码兼容性的一个思路:从更高层的逻辑出发,而不是局限在具体的实现当中。因为,不变的是逻辑,变化的是实现。这里就是 变与不变隔离 思想的一个体现。

    7.3 创建细节

    7.3.1 新增汇编指令汇总

    指令功能说明ldrLoad register from memoryldr也可以load立即数到寄存器,注意不要丢"=",例如:ldr r6, =(KERNEL_END - 1)strStore register to memory注意区分下述两种格式:(1)str r3, [r0], #4 :[r0] = r3, r0 += 4;(2)str r3, [r0, #4]!:[r0 + 4] = r3, r0 += 4。lsrlogical shift right逻辑右移lsllogical shift lift逻辑左移orror按位或

    条件转移指令:

    跳转指令说明beq相等则跳转bne不相等则跳转blo小于(无符号数)则跳转bls小于等于(无符号数)则跳转bhi大于(无符号数)则跳转bhs大于等于(无符号数)则跳转blt小于(有符号数)则跳转ble小于等于(有符号数)则跳转bgt大于(有符号数)则跳转bge大于等于(有符号数)则跳转

    7.3.2 代码逐行分析

    有了前文的分析和铺垫,代码分析起来也就轻松多了。

    /* * 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. * * C语言中调用函数会使用入参出参,但是汇编不会,都是使用“全局指针或变量”, * 所以,将它们标记出来很有必要,便于汇编在调用函数时上下文的联系。 * r8 = machinfo * r9 = cpuid * r10 = procinfo * * Returns: * r0, r3, r6, r7 corrupted * r4 = physical page table address */ .type __create_page_tables, %function __create_page_tables: pgtbl r4 @ page table address,将page_table的基址(0x30004000)存入r4 /* * Clear the 16K level 1 swapper page table */ mov r0, r4 @ r0为移动指针,初始值设置为r4 mov r3, #0 @ r3为填充内容#0 add r6, r0, #0x4000 @ r6指向页表末尾的下一个地址,用来控制循环结束条件 1: str r3, [r0], #4 @ 一个循环清除16个字节,循环1024次,刚好清除16K内存 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 @ 比较r0是否已经指向页表末尾的下一个地址 bne 1b @ 结束跳出循环,否则跳到标号1b处 @ 定义:#define PROCINFO_MM_MMUFLAGS 8 /* offsetof(struct proc_info_list, __cpu_mm_mmu_flags) @ */ @ 取值:.long PMD_TYPE_SECT | \ PMD_SECT_BUFFERABLE | \ PMD_SECT_CACHEABLE | \ PMD_BIT4 | \ PMD_SECT_AP_WRITE | \ PMD_SECT_AP_READ ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags,汇编语言使用基址-变址寻址方式来访问结构体成员变量 /* * Create identity mapping(恒等映射) for first MB *(下面这三行代码只设置内核的第一个section表项)of kernel to * cater for the MMU enable. This identity mapping * will be removed by paging_init()(这就意味着这里的映射只是暂时的). * We use our current program * counter to determine corresponding section base address.(使用PC指针获取当前页基址) */ mov r6, pc, lsr #20 @ start of kernel section(物理地址),r6 = pc >> 20 (每一页是1M,即2的20次方) orr r3, r7, r6, lsl #20 @ flags + kernel base, r3 = r7 | (r6 << 20) ,组成L1 section entry(页表内容) str r3, [r4, r6, lsl #2] @ identity mapping,mem[r4 + r6 * 4] = r3-> mem[0x30004c00] = 0x300xxxxx。 @ r3在这里实际上起到了代码段寄存器的作用。 @ 基址是r4,偏移是r6*4(r6是页号,4代表每一个页号需要占用4个字节来进行存储) /* * Now setup the pagetables for our kernel direct * mapped region. */ add r0, r4, #(KERNEL_START & 0xff000000) >> 18 @ KERNEL_START = KERNEL_RAM_VADDR = \ @ 0xc0000000 + 0x8000 = 0xc0008000 str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! @ [r4 + (0xc0008000 & 0xfff00000)>> 20 * 4] = r3 -> @ mem[0x30007000] = 0x300xxxxx。 @ 这里将r3的内容再次写入内存(相当于映射了两次),只不过页表的偏移计算使用的是虚拟地址 ldr r6, =(KERNEL_END - 1) @ 构建结束位置 add r0, r0, #4 @ 指针自加,指向下一个section存放地址 add r6, r4, r6, lsr #18 @ 继续构建结束位置,存放到r6 1: cmp r0, r6 add r3, r3, #1 << 20 @ 将L1表项编号加1 strls r3, [r0], #4 @ 将r3内容存储到r0后,r0指针加4 bls 1b @ r0小于等于r6则跳转到1b处继续执行,否则跳出 /* * Then map first 1MB of ram in case it contains our boot params. * 由于PAGE_OFFSET和KERNEL_START在同一个section里,所以这1MB的映射其实在内核代码段 * 映射时已经做过了,这里又重复做了一次(但是意义是不同的) */ add r0, r4, #PAGE_OFFSET >> 18 @ PAGE_OFFSET = 0xc0000000, @ 注意这里的r0刚好和内核起始位置在同一个section. orr r6, r7, #(PHYS_OFFSET & 0xff000000) @ PHYS_OFFSET = 0x30000000,r6 = 0x30000000 | r7, @ 注意这里的内容刚好和r3一致。 .if (PHYS_OFFSET & 0x00f00000) @ if条件不成立 orr r6, r6, #(PHYS_OFFSET & 0x00f00000) .endif str r6, [r0] @ 写入页表指定位置,mem[0x30007000] = 0x300xxxxx。 mov pc, lr .ltorg @ LTORG用于声明一个数据缓冲池

    Tips:ltorg用于声明一个数据缓冲池(又名文字池)的开始。在使用ldr指令时,常常需要在适当的地方加入ltorg声明数据缓冲池,ldr加载的数据暂时被编译器放于数据缓冲池中。ltorg通常放在无条件跳转指令之后,或者子程序返回指令之后,这样处理器就不会错误的将数据缓冲池中的数据当作指令来执行。


    <完>

    Processed: 0.013, SQL: 8