微信公众号:二进制人生
专注于嵌入式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 pgtbl, rd, phys
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!, {r0, r1, sp} )
sub r2, r1, r0
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 阅读(375)
评论(0) 编辑 收藏 引用