随笔-118  评论-133  文章-4  trackbacks-0

微信公众号:二进制人生
专注于嵌入式linux开发。问题或建议,请发邮件至hjhvictory@163.com。
更新日期:2020/1/30,转载请注明出处。

内存管理系列文章:


内容整理自网络和自己的认知,旨在学习交流,请勿用于商业用途,转载请注明出处。
linux内存管理(一)开篇介绍
linux内存管理(二)两种内存架构和linux三种内存模型
linux内存管理(三)linux内存管理三级结构
linux内存管理(四)页表机制
linux内存管理(八)页表最初的初始化--从内核启动的第一段代码谈起


全局变量swapper_pg_dir记录了内核页目录PGD的虚拟地址。该变量比较特殊,它不在C文件中定义,而是在汇编文件arch/arm/kernel中定义。head.S是内核启动最初运行的代码。

内核启动初期运行汇编代码,今天我们的所讲都是汇编代码,没有一定的arm汇编知识可能看不懂。

swapper_pg_dir的位置如下图:

KERNEL_RAM_VADDR = PAGE_OFFSET+TEXT_OFFSET        -----
                                                  页目录
swapper_pg_dir   = KERNEL_RAM_VADDR-PG_DIR_SIZE   -----

PAGE_OFFSET      = CONFIG_PAGE_OFFSET             -----


                   0                              -----

swapper_pg_dir等于KERNEL_RAM_VADDR-PG_DIR_SIZE
,意味着内核页目录PGD放在KERNEL_RAM_VADDR下方距离PG_DIR_SIZE的位置,这里指的是虚拟地址。PG_DIR_SIZE表示页目录的大小。

KERNEL_RAM_VADDR = PAGE_OFFSET + TEXT_OFFSET
,TEXT_OFFSET是在编译内核时传递进来的宏,表示内核代码起始偏移。

PAGE_OFFSET代表的是内核空间和用户空间对虚拟地址空间的划分界限,对不同的体系结构不同。比如在32位系统中3G-4G 属于内核使用的内存空间,所以 PAGE_OFFSET = 0xC0000000。在X86-64架构下是ffff880000000000。

为了给用户提供更多的自由,PAGE_OFFSET的值可以由用户自行配置,即我们以前默认的用户空间比内核空间=3:1
的这个经典比例可以自行修改。

上面的论述源自以下汇编代码:

/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */

#define KERNEL_RAM_VADDR    (PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#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

    .globl  swapper_pg_dir
    .equ    swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

英文好的同学可以看上面的注释,讲的明明白白。在用户没有配置时,PAGE_OFFSET等于3G,宏CONFIG_ARM_LPAE
通常情况下没有定义,即没有开启大物理页扩展,页目录的大小是16K。为何是16K?32位虚拟地址的高12位作为一级页目录索引,所以页目录总共有2的12次方=4K项,由于一个页目录项占据4个字节,所以页目录大小为16K。

KERNEL_RAM_VADDR是内核代码在虚拟内存的起始位置。
上面有两个汇编伪指令,考虑到有些同学没有接触过arm32汇编,这里补充下。

arm汇编
.global A: 让符号A对链接器可见,可以供其他链接对象模块使用。
.equ A,B: 用于给一个变量赋值,将B赋值给A

head.S是内核启动的入口文件,由汇编实现。我们今天只分析和内存相关的操作。经过提炼如下:

ldr    r8, =PLAT_PHYS_OFFSET       @ always constant in this case

结果就是让r8 = PLAT_PHYS_OFFSE。PLAT_PHYS_OFFSE这个宏前面分析过,是指物理地址偏移,因为我们的物理地址通常都不从0开始,比如你的内存条可能挂在io地址为0x2000000的地方,那么PLAT_PHYS_OFFSE=0x2000000。

bl    __create_page_tables

bl是支持返回的跳转指令,这里跳转到标号__create_page_tables。而b是不支持返回的跳转指令。

arm汇编
B或BL指令引起处理器转移到标号处开始执行。
两者的不同之处在于:
(1)BL指令在转移到子程序执行之前,将其下一条指令的地址拷贝到R14(又名LR,链接寄存器)。
由于BL指令保存了下条指令的地址,因此使用指令“MOV PC ,LR”即可实现子程序的返回。
(2)B指令则无法实现子程序的返回,只能实现单纯的跳转。用户在编程的时候,可根据具体应用选用合适的跳转语句。

姑且把__create_page_tables叫做函数,在调用__create_page_tables之前我们来学习一个宏。

    .macro  pgtblrdphys
    add \rd, \phys#TEXT_OFFSET
    sub \rd, \rd#PG_DIR_SIZE
    .endm

上面定义了一个宏,pgtbl,它有两个参数rd和phys,在调用的时候会对参数进行直接替换。它实现了这样的操作:

rd=phys + TEXT_OFFSET
rd=rd - PG_DIR_SIZE

如果输入参数phys等于物理地址起始偏移,那么rd最终就等于上面所说的swapper_pg_dir,即页目录起始地址(物理地址)。
下面来看看__create_page_tables做了什么事。

pgtbl    r4, r8              @ page table address

调用前面定义的宏,由于r8=物理地址偏移,计算后r4=页目录起始地址。

(1)

接着将页目录清0,即将r4--r4 + PG_DIR_SIZE地址上的值清0。

    /*
     * Clear the swapper page table
     */

    mov r0, r4               @ 让r0等于页目录起始地址
    mov r3, #0
    add r6, r0, #PG_DIR_SIZE @ r6=r0+PG_DIR_SIZE
1:  str r3, [r0], #4       @ 令[r0]=r3,然后r0=r0+4。中括号表示相对寻址,即以r0的值作为地址寻址。
    str r3, [r0], #4         @ 已经是循环了,为何要写4个我也不知道哈,可能是为了快一点,减少调用teq和bne指令的次数。
    str r3, [r0], #4
    str r3, [r0], #4
    teq r0, r6               @ 测试r0是否等于r6
    bne 1b                   @ 如果上面的测试结果不相等则跳转到标号1

上面这段清0的代码比较简单,不做过多解释。

(2)

接下来初始化标号__turn_mmu_on到__turn_mmu_on_end之间这段区域的页目录项。

    /*
     * 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      @ 取出r5高12位,即地址__turn_mmu_on的页目录项
    mov r6, r6, lsr #SECTION_SHIFT      @ 取出r6高12位,即地址__turn_mmu_on_end的页目录项
                                        @ SECTION_SHIFT等于20
1:  orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base构造页目录项的值,即物理地址的高12位或上一些标志位
    str r3, [r4, r5, lsl #PMD_ORDER]    @ identity mapping将页目项存入页目录的相应位置,r4 + r5*4 = r3,乘以4是因为一个页目录项占据4个字节
    cmp r5, r6                       
    addlo   r5, r5, #1          @ next section页目录项的值加1,映射下一个页目录项
    blo 1b

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

我们看下该标号的定义:

__turn_mmu_on_loc:
    .long   .
    .long   __turn_mmu_on
    .long   __turn_mmu_on_end

.是伪指令,表示当前地址,从标号的定义可以知道,在这个地方存放的是3个地址,分别是__turn_mmu_on_loc,__turn_mmu_on,__turn_mmu_on_end的虚拟地址。

vmlinux反汇编的结果截取如下:

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的意义就很明确了,就是求出__turn_mmu_on_loc这个标号的物理地址和虚拟地址之间的偏移量。然后根据这个偏移量求出__turn_mmu_on和__turn_mmu_on_end的物理地址。

接下来是一个循环,初始化__turn_mmu_on到_turn_mmu_on_loc_end这段区域对应的页目录项。页目录项就是指物理地址的高12位或上一些相应的标志位(段映射)。

页表映射是将虚拟地址映射到物理地址,对于一级映射,是以虚拟地址的高12位作为索引idx,将其要映射的物理地址的高12位填充到页目录pgtable的第idx项。

而这里让_turn_mmu_on-_turn_mmu_on_end这段区域映射到自身,意味着这段区域的虚拟地址和物理地址相等,为何要这样暂时不清楚目的。这段映射是临时的,后面会被paging_init函数移除。

(3)

接下来要映射PAGE_OFFSET到标号_end之间的虚拟地址到物理地址起始处。_end定义于vmlinux.lds.S,是内核代码的终点位置。

    /*
     * Map our RAM from the start to the end of the kernel .bss section.
     */

    add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER) @ r0 = r4 + PAGE_OFFSET>>SECTION_SHIFT * 4
    ldr r6, =(_end - 1)
    orr r3, r8, r7
    add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1:  str r3, [r0], #1 << PMD_ORDER
    add r3, r3, #1 << SECTION_SHIFT
    cmp r0, r6
    bls 1b     

第一行是计算虚拟地址PAGE_OFFSET对应的页目录项的地址,存放到r0。
r6是记录要映射的终点地址。r8是物理地址偏移,也就是虚拟地址PAGE_OFFSET待映射的物理地址起点。

最终的结果是把phys_offset到_end映射到PAGE_OFFSET开始的内核虚拟地址上(段映射)。

               物理内存            虚拟内存
        ----            4G  ----
        |  |                |  |
        |  |                |  |
        |  |                |  |
         |  |                |  |
        |  |    PAGE_OFFSET ----
    _end----                |  |
        |  |                |  |
         |  |                |  |
        |  |                |  |
         |  |                |  |
phys_offset----                |  |
                  |  |
                             |  |
                            |  |
                             |  |
                             |  |
                             |  |
                            |  |
                           0 ----

(4)

为uboot传递过来的启动数据(atags或者设备树dtb)建立映射,uboot在启动内核时将启动数据的物理地址存放在r2寄存器,供内核使用。

    /*
     * 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    @先右移20位
    movs    r0, r0, lsl #SECTION_SHIFT    @再左移20位,这两步操作将r2的值做了向下1M对齐。
    subne   r3, r0, r8                    @r3=r2到物理地址起始地址的偏移
    addne   r3, r3, #PAGE_OFFSET          @r3=待映射的虚拟地址起始地址
    addne   r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER) @r3=对应页目录项的地址
    orrne   r6, r7, r0                 
    strne   r6, [r3], #1 << PMD_ORDER     @映射一个页目录项
    addne   r6, r6, #1 << SECTION_SHIFT   @r6=r6+1M
    strne   r6, [r3]                      @再映射一个页目录项

一个页目录项表示的内存大小是1M,启动数据一般不会太大,这里怕1M不够,所以映射了两个页目录项。

总结一下,__create_page_tables映射了三段关键区域,第一段是打开mmu的代码片段(采用identify mapping),第二段是内核代码和数据占据的区域,第三段是uboot传递过来的启动数据(atag或者dtb)。

(5)

head.S接近尾声,

ldr    r13, =__mmap_switched       @ address to jump to after
                        @ mmu has been enabled
badr    lr, 1f              @ return (PIC) address
...
1:    b   __enable_mmu

第一句将标号__mmap_switched的值存放在r13,即sp寄存器。然后跳转到标号1。
标号1直接执行__enable_mmu,从名字可以知道是使能mmu。

__enable_mmu:
        ...
    b   __turn_mmu_on
ENDPROC(__enable_mmu)

可以看到最后调用了__turn_mmu_on。

ENTRY(__turn_mmu_on)
        ...
    mov r3, r3
    mov r3, r13
    ret r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)

我们只关心最后的三条指令,让r3等于r3不知有什么用意,r13在此前已经做了赋值,等于__mmap_switched,这里相当于ret __mmap_switched,即跳转到__mmap_switched执行。

__mmap_switched没有做和内存初始化相关事,但既然提及我们就简单分析下,它定义于head-common.S:

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register (exc_ret for M-class)
 *  r1  = machine ID
 *  r2  = atags/dtb pointer
 *  r9  = processor ID
 */
    __INIT
__mmap_switched:
    ...
    adr r4, __mmap_switched_data
    mov fp, #0

   @ 将bss段清0
    ARM(    ldmia   r4!, {r0r1sp} )
    sub r2r1r0
    mov r1#0
    bl  memset              @ clear .bss

上面这段代码将__bss_start-__bss_stop的区域清0,这段代码看懂你会觉得很有趣。这里直接调用了memset函数。C语言函数调用,会将参数0存于r0,将参数1存于r1,…
我们联想下memset的使用就会明白了。

void *memset(void *s, int ch, size_t n);

执行ldmia会将r4地址处的值赋值给r0,然后r4自加4,再将r4地址的值赋值给r1…
所以r0=__bss_start,r1=__bss_stop,sp=init_thread_union + THREAD_START_SP

    ...
    mov lr, #0
    b   start_kernel
ENDPROC(__mmap_switched)
    .align  2
    .type   __mmap_switched_data, %object
__mmap_switched_data:
    .long   __bss_start         @ r0
    .long   __bss_stop          @ r1
    .long   init_thread_union + THREAD_START_SP @ sp
    ...

可以看到最后调用了start_kernel函数,众所周知,start_kernel是内核C语言阶段的入口函数,是一个重要的转折点,在此之前都是汇编代码。注意一个细节,跳转到start_kernel采用的是b指令而非bl指令,b指令永不返回,跳转之后一去不复返,踏上了C语言的康庄大道。而我们留意到在调用start_kernel之前特意把连接寄存器lr做了清0操作,倘若出错,那就会跳转到0地址执行,而0地址存放的应该是异常处理。

start_kernel定义于init/main.c,可以理解为main函数。

至此我们分析完了head.S汇编文件中和内存相关的部分。今天的主要目的是探寻swapper_pg_dir的位置,即内核页目录存放在哪里。

posted on 2022-12-28 11:13 lfc 阅读(261) 评论(0)  编辑 收藏 引用
只有注册用户登录后才能发表评论。