arm linux内核建立临时页表(head.S文件分析)

简介

linux内核是通过MMU来管理内存的,MMU其中的一个功能就是把虚拟地址转换为物理地址,但是linux在启动过程中,MMU未打开之前,代码的执行都是在物理地址空间的,那么怎么才能实现物理地址到虚拟地址空间的切换是本文的重点。

swapper_pg_dir定义

#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定义

arch/arm/include/asm/memory.h
/* PAGE_OFFSET - the virtual address of the start of the kernel image */
#define PAGE_OFFSET		UL(CONFIG_PAGE_OFFSET)

TEXT_OFFSET定义

arch/arm/Makefile
# The byte offset of the kernel image in RAM from the start of RAM.
TEXT_OFFSET := $(textofs-y)
textofs-y	:= 0x00008000

因此swapper_pg_dir的地址大小是:0xc0000000 + 0x00008000-0x4000=0xc0004000,从内核编译完成的System.map文件也可以看到swapper_pg_dir的虚拟地址大小:

c0004000 A swapper_pg_dir
c0008000 T _text
c0008000 T stext

head.S汇编分析

  1. 分配物理地址给r8寄存器
#ifndef CONFIG_XIP_KERNEL
	(1) adr	r3, 2f
	(2) ldmia	r3, {r4, r8}
	(3) sub	r4, r3, r4			@ (PHYS_OFFSET - PAGE_OFFSET)
	(4) add	r8, r8, r4			@ PHYS_OFFSET
#else
	ldr	r8, =PLAT_PHYS_OFFSET		@ always constant in this case
#endif
#ifndef CONFIG_XIP_KERNEL
2:	.long	.
	.long	PAGE_OFFSET
#endif

物理地址PHYS_OFFSET的定义如下:

arch/arm/include/asm/memory.h
#define PLAT_PHYS_OFFSET	UL(CONFIG_PHYS_OFFSET)

对于没有定义CONFIG_XIP_KERNEL宏的平台来说,找到物理地址还是有点困难的。
adr汇编指令是相对寻址,与当前位置无关
(1)表示将标号2的物理地址赋值给r3,当前运行的是物理地址,adr汇编指令会被解析为pc+标号2的偏移地址
(2)将当前虚拟地址也就是绝对地址赋值给r4,将PAGE_OFFSET赋值给r8
(3)表示链接地址和物理地址的偏差
(4)得到实际的物理起始地址

  1. 调用 bl __create_page_tables
bl	__create_page_tables
__create_page_tables:
	pgtbl	r4, r8				@ page table address
	/*参见代码详解1*/

	/*
	 * Clear the swapper page table
	 */
	mov	r0, r4
	mov	r3, #0
	add	r6, r0, #PG_DIR_SIZE
1:	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	teq	r0, r6
	bne	1b
	/*参见代码详解2*/
	
	ldr    r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
    /*参见代码详解3*/
	
		/*
	 * 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
	mov	r6, r6, lsr #SECTION_SHIFT

1:	orr	r3, r7, r5, lsl #SECTION_SHIFT	@ flags + kernel base
	str	r3, [r4, r5, lsl #PMD_ORDER]	@ identity mapping
	cmp	r5, r6
	addlo	r5, r5, #1			@ next section
	blo	1b
	/*参见代码详解4*/
	
	/*
	 * Map our RAM from the start to the end of the kernel .bss section.
	 */
	add	r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
	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
	/*参见代码详解5*/

(1)代码详解1

	pgtbl	r4, r8				@ page table address
	.macro	pgtbl, rd, phys
	add	\rd, \phys, #TEXT_OFFSET
	sub	\rd, \rd, #PG_DIR_SIZE
	.endm

pgtbl 宏用于通过DRAM物理地址来获取页表的物理地址。 前面我们已经知道r8用于存放DRAM的起始物理地址,r4则是要存放计算得到的页表物理地址。
kernel起始地址=DRAM起始物理地址+TEXT_OFFSET
内核页表地址=kernel起始地址-PG_DIR_SIZE
(2)代码详解2
为临时内核页表分配空间之后,接下来的任务就是清空临时内核页表分配空间。

    /*
     * Clear the swapper page table
     */
    mov    r0, r4   
@ 将页表物理地址放到r0上
    mov    r3, #0   
@ 把0赋值给r3寄存器
    add    r6, r0, #PG_DIR_SIZE   
@ 把r0+PG_DIR_SIZE的值赋给r6,也就是说临时内核页表的末尾物理地址放到r6上
1:    str    r3, [r0], #4       
@ 把0值写入以r0为地址的存储器中,并将新地址r0+4写入到r0中,接下来的语句表示:从r0(临时内核页表物理地址)指向的寄存器上开始写入0值,每16个字节一个循环
    str    r3, [r0], #4
    str    r3, [r0], #4
    str    r3, [r0], #4
    teq    r0, r6           
@ 比较是否已经写到了r6(临时内核页表的末尾物理地址)上

    bne    1b           
@ 如果还没有写完,跳转到标号1,进入下一个循环

(3)代码详解3
设置MMU的标识并存放到r7寄存器中,后续需要写入到临时内核页表的页表项中。
(4)代码详解4
开始进行映射表的创建,首先是创建恒等映射,就是将物理地址相应到相同的虚拟地址上。其实这个恒等映射仅映射__enable_mmu功能函数区的页表 ,以保证在启用mmu时代码的正确执行–1:1映射(物理地址=虚拟地址)。
首先看一下__turn_mmu_on_loc的定义

__turn_mmu_on_loc:
	.long	.
	.long	__turn_mmu_on
	.long	__turn_mmu_on_end

__turn_mmu_on和__turn_mmu_on_end标识了打开MMU的起始代码地址和结束代码地址,kernel将这些链接地址存放到了__turn_mmu_on_loc中。

	adr	r0, __turn_mmu_on_loc
	@当前代码是运行在物理地址空间上,将pc指针+__turn_mmu_on_loc的偏移量赋值给r0,也就是说r0保存的是__turn_mmu_on_loc物理地址
	ldmia	r0, {r3, r5, r6}
	@将r0地址的内容依次赋值给r3,r5,r6,也就是说r3指向的是__turn_mmu_on_loc虚拟地址,r5指向__turn_mmu_on的虚拟地址,r6指向__turn_mmu_on_end的虚拟地址
	sub	r0, r0, r3			@ virt->phys offset
	@得到虚拟地址到物理地址的偏移
	add	r5, r5, r0			@ phys __turn_mmu_on
	@r5保存__turn_mmu_on物理地址
	add	r6, r6, r0			@ phys __turn_mmu_on_end
	@r6保存__turn_mmu_on_end物理地址
	mov	r5, r5, lsr #SECTION_SHIFT
	mov	r6, r6, lsr #SECTION_SHIFT
	@以上两条语句是分别把r5和r6右移20位,也就是1M,分别得到r5和r6的段序号;
	@arm打开MMU初期,使用的是临时内核页表,其类型就是段式页表。段式页表将4GB的地址空间(32bit系统)划分成40961MB的段,因此段式页表有4096个页表项,每个页表项有32bit(4 byte),故段式页表需要16KB的空间。
1:	orr	r3, r7, r5, lsl #SECTION_SHIFT	@ flags + kernel base
@页表项内容为段序号(r5)左移SECTION_SHIFT后或上MMU标识(r7),存放在r3上,这样就填写对应段的段页表项的内容了
	str	r3, [r4, r5, lsl #PMD_ORDER]	@ identity mapping
	@将段页表项值(r3)写入到对应的段页表项中
	@ 段页表项的地址=段页表起始地址(r4)+段序号(r5)*段页表项的大小,PMD_ORDER大小是4
	cmp	r5, r6
	addlo	r5, r5, #1			@ next section
	blo	1b
	@判断是否已经写到__turn_mmu_on结束地址的对应的段页表项中,如果没有的话,继续写入下一个段,跳转到1标识符。

(5)映射kernel的内核空间
通过System.map中可以看出kernel的连接区域如下:

c0008000 T _text
c0a58438 B _end

代码分析:

	/*
	 * Map our RAM from the start to the end of the kernel .bss section.
	 */
	add	r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
	@ PAGE_OFFSET表示内核空间的起始地址,如果用户空间和内核空间按照3:1划分,那么该参数是0xc0000000
	@ 将PAGE_OFFSET右移(SECTION_SHIFT - PMD_ORDER)后得到0xc0000000所在段的段页表项的地址偏移,其实也就是先右移SECTION_SHIFT得到段序号,然后再左移PMD_ORDER得到段页表项偏移
	@ 将段页表项的地址偏移+临时内核页表地址(r4)得到0xc0000000所在段的段页表项的物理地址,然后将这个物理地址赋值给r0
	ldr	r6, =(_end - 1)
	@ 将内核映射区的结束地址存入r6寄存器中。
	orr	r3, r8, r7
	@将r8寄存器或上r7,赋值给r3,其中r8寄存器是存放DRAM的起始物理地址,r7是mmu标识
	add	r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
	@将内核映射区的结束地址左移(SECTION_SHIFT - PMD_ORDER)得到段页表项的地址偏移,然后再加上临时内核页表项地址r4,最后赋值给r6;
1:	str	r3, [r0], #1 << PMD_ORDER
	@将r3的内容存放到r0地址存储器中,也就是内核起始地址0xc0000000所在段的页表项物理地址,然后更新r0的地址:r0=r0+4
	add	r3, r3, #1 << SECTION_SHIFT
	@将r3的值加上1左移SECTION_SHIFT,得到最新的r3值,也就是下一个段页表项值
	cmp	r0, r6
	@判断r0和r6是否相等,也就是说判断是否已经是内核映射区的结束段了。
	bls	1b
	@如果不是,跳转到1标号,继续进行内核映射

(6)创建DTB映射区
uboot引导内核的时候会给传递参数,其中r2寄存器存放的就是dtb的地址,所以创建DTB映射区,主要是从r2中提取dtb的物理内存地址,计算出对应虚拟地址之后,进行映射表创建。

/*
	 * 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
	@将r2右移SECTION_SHIFT,把低位清0,赋值给r0
	movs	r0, r0, lsl #SECTION_SHIFT
	@将r0值再左移SECTION_SHIFT,也就是说得到这个物理内存段的起始地址
	subne	r3, r0, r8
	@得到r0相对于r8(物理起始地址的偏移,然后存放在r3中
	addne	r3, r3, #PAGE_OFFSET
	@将r3的值加上PAGE_OFFSET,物理地址转虚拟地址,也就是说得到了DTB所在物理段对应的虚拟地址
	addne	r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
	@得到映射的虚拟地址的段的页表项的地址,存放在r3中,不懂的话可以参考前文描述
	orrne	r6, r7, r0
	@将物理内存段地址(r0)或上mmu标识(r7),得到对应页表项值,存放到r6中
	strne	r6, [r3], #1 << PMD_ORDER
	@将页表项值(r6)写入到页表项中[r3]存储器中,然后更新r3值:r3+4,获取到下一个页表项的地址
	addne	r6, r6, #1 << SECTION_SHIFT
	@页表项值+0x100000,得到下一个应该写入的页表项值
	strne	r6, [r3]
	@将页表项值(r6)写入到页表项中([r3]

综上,临时内核页表就创建完成了。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值