arm64 linux 学习笔记二

版权声明:本文为CSDN博主「ashimida@」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/lidan113lidan/article/details/48707579
更多内容可关注微信公众号arm64 linux 学习笔记二_第1张图片

##一些关键的宏
下面是启动阶段用到的一些宏在测试平台上的值,当前linux使用的是三级页表,4KB小页。

#define __pa(x)	__virt_to_phys((unsigned long)(x))
#define __va(x)	((void *)__phys_to_virt((phys_addr_t)(x)))

PTRS_PER_PGD = 0x200	//PGD表项为512项
PTRS_PER_PMD = 0x200	//PMD表项为512项
PTRS_PER_PTE = 0x200	//PTE表项为512项

//PGD中一个页表项代表的地址范围是1GB
PGDIR_SHIFT = 30
PGDIR_SIZE = 0x40000000 //(2^PGDIR_SHIFT)
PGDIR_MASK = 0x3FFFFFFF

//PMD中一个表项代表的地址范围是2MB
PMD_SHIFT = 21
PMD_SIZE = 0x200000 	//(2^PMD_SHIFT)
PMD_MASK = 0x1FFFFF

//物理页的大小为4KB,同时也是PTE中一个页表项代表的地址范围
PAGE_SHIFT = 12
PAGE_SIZE = 0x1000      //(2^PAGE_SHIFT)
PAGE_MASK = 0xFFFFFFFFFFFFF000

SECTION_SHIFT = 21 
BLOCK_SHIFT = 21
KERNEL_RAM_VADDR = 0xFFFFFFC000800000
PAGE_OFFSET: 内核态的起始地址。
PAGE_OFFSET + TEXT_OFFSET: 内核代码(stext)的起始地址。
PHYS_OFFSET: 是PAGE_OFFSET这个虚拟地址对应的物理地址

总结之:
整个64位地址空间中有两段有效地址空间:

	1. 0x0000000000000000 - 0x0000007FFFFFFFFF(512GB),通过TTBR0寻址,从level1开始做地址转换。
	2. 0xFFFFFF8000000000 - 0xFFFFFFFFFFFFFFFF(512GB),通过TTBR1寻址,从level1开始做地址转换。 

####pgd

  1. 对于每一段地址空间,都有一个pgd表,表项为512个, 每一个表项大小8byte,共占空间4KB。
  2. pgd的每一个表项代表1GB地址空间,512个页表项共代表512GB空间。
  3. 每个pgd表项可分为block/table(对应level1),如果为block,则pgd的内容就直接指向这1GB空间的物理地址(除去属性位),如果为table,则pgd的内容指向一个二级页表(pmd表)的物理地址(除去属性位)。
    ####pmd
  4. 对于每个pmd表,表项为512个,每个表项大小8byte,共占空间4KB.
  5. pmd的每一个表项代表2MB地址空间,512个页表项共代表1GB空间。
  6. 每个pmd表项可分为block/table(对应level2),如果为block,则pmd的内容就直接指向这2MB空间的物理地址(除去属性位),如果为table,则pmd的内容指向一个三级页表(pte表)的物理地址(除去属性位)。
    ####pte
  7. 对于每个pte表,表项为512个,每个表项大小8byte,共占空间4KB。
  8. pte的每一个表项代表4KB地址空间,512个页表项共代表2MB空间。
  9. 每个pte表项只分为有效/无效(对应level3),如果有效,则pte的内容就直接指向这4KB空间的物理地址(除去属性位),如果无效,则mmu解析到这里会产生访问异常。

##linux kernel启动过程

###ENTRY(stext)

//arm64内核的入口
ENTRY(stext)
	mov	x21, x0		//x21=FDT,bootloader传过来的设备树地址
	//获取物理地址和虚拟地址之差 -> x28, PHYS_OFFSET -> x24
	bl	__calc_phys_offset		
	//不知道干啥的,先过......
	bl	el2_setup			// Drop to EL1
	//读取cpuid -> x22,cpuid是记录在midr_el1寄存器中的
	mrs	x22, midr_el1			//x22=cpuid
	//lookup_processor_type根据传入的w0(x0)查找体系结构相关的cpu_table,找到后返回地址给x0
	mov	x0, x22
	bl	lookup_processor_type
	//x23存当前体系结构的cpu_table地址
	mov	x23, x0			//x23=current cpu_table
	//如果没找到,直接error
	cbz	x23, __error_p	//invalid processor (x23=0)?
	//没看,先过......
	bl	__vet_fdt
	/*
	运行到这里之前,x0 = cpu_table, x21 = FDT, x24 = PHYS_OFFSET(0x40000000), 这个函数内部做了两张表,idmap_pg_dir 和 swapper_pg_dir。idmap_pg_dir负责恒等映射,__turn_mmu_on函数所在的2MB空间映射到这张表了,swapper_pg_dir负责正常内核的映射,整个kernel映射在这张表上。
	*/
	bl	__create_page_tables		// x25=TTBR0, x26=TTBR1
	//这个是__turn_mmu_on执行后的跳转地址,也是最后调用的函数,这个函数内部调用start_kernel.
	ldr	x27, __switch_data		
	//__enable_mmu内部跳转到__turn_mmu_on,这是最后一句br x12返回的地址
	adr	lr, __enable_mmu		// return (PIC) address
	//找到cpu对应的CPU_INFO_SETUP函数,并调用之
	ldr	x12, [x23, #CPU_INFO_SETUP]
	add	x12, x12, x28			// __virt_to_phys
	//跳转到体系结构相关的CPU_INFO_SETUP函数
	br	x12				// initialise processor
	//这里的实际调用流程是: CPU_INFO_SETUP -> __enable_mmu -> __turn_mmu_on -> __switch_data.__mmap_switched -> start_kernel
ENDPROC(stext)

###__create_page_tables

__create_page_tables:
	/* 
	pgtbl定义如下:
	.macro	pgtbl, ttb0, ttb1, phys
	add	\ttb1, \phys, #TEXT_OFFSET - SWAPPER_DIR_SIZE
	sub	\ttb0, \ttb1, #IDMAP_DIR_SIZE
	.endm
	在内核初始化的时候,kernel是放在PAGE_OFFSET + TEXT_OFFSET的位置,而idmap_pg_dir,swapper_pg_dir这两张表是固定紧挨着kernel,在kernel前面的。这句宏是根据内核载入的物理基地址PHYS_OFFSET(x24),计算idmap_pg_dir,swapper_pg_dir的物理地址分别 -> x25, x26 (在__create_page_tables返回后,开启mmu之前,x25的值会赋值给ttb0,x26的值会赋值给ttb1)。
     */
    // idmap_pg_dir and swapper_pg_dir addresses
	pgtbl	x25, x26, x24	
	/*
	将[idmap_pg_dir, idmap_pg_dir + SWAPPER_DIR_SIZE + IDMAP_DIR_SIZE] 全部清空,这里面不止包含idmap_pg_dir, swapper_pg_dir两张表,具体的结构如下:
PA=idmap_pg_dir:------> idmap_pg_dir
PA+=0x000001000:------> idmap_pg_dir[x]'s tbl(idm_tbl)
PA+=0x000001000:------> swapper_pg_dir
PA+=0x000001000:------> swapper_pg_dir[x]'s tbl(swap_tbl)
PA+=0x000001000:------> kernel(PA=PHYS_OFFSET + TEXT_OFFSET):
	idmap_pg_dir是一张恒等映射表,对应arm的一级页表,idm_tbl是一个二级页表。
	swapper_pg_dir是系统正常运行时的一份内核页表,所有进程页表的kernel部分都是从这里复制出来的,swap_tbl是其的一个二级页表
	最后空的1MB不知道是干嘛的.
	
	arm64的一个页表项为8byte,idmap_pg_dir大小4KB, 4KB/8byte = 512(0x200)个页表项。每个页表代表的地址空间就是PGDIR_SIZE(1GB),一个idmap_pg_dir理论上能代表512G地址空间(刚好是三级寻址空间大小,上一篇提到过)。
	在系统初始化的时候只为idmap_pg_dir预留了一个二级页表,即swap_tbl,可代表1GB空间(其余的idmap_pg_dir作为一级页表项,英应该可以用block的方式填充)。
	swap_tbl同理 4KB/8byte=512(0x200)个页表项,每隔页表项代表PMD_SIZE(2MB)的地址空间,512*2MB=1GB空间,由于没有预留pte三级页表,所以在开始阶段swap_tbl想要初始化,估计也只能初始化为block。
	swapper_pg_dir与idmap_pg_dir类似。
	 */
	//x0 = idmap_pg_dir
	mov	x0, x25
	//x6 = 内核起始位置
	add	x6, x26, #SWAPPER_DIR_SIZE
	//循环将idmap_pg_dir到内核开始位置都清零。
1:	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	stp	xzr, xzr, [x0], #16
	cmp	x0, x6
	//如果x0 < x6,则jmp 1
	b.lo	1b
	
	// x7 = 0x711 (0,1,2级页描述符的访问属性是通用的,0x711最后一位1表示用的是段映射(SECT/block),没有下一级页表了,初始化的时候这是默认属性。
	ldr	x7, =MM_MMUFLAGS

	/*
	以idmap_pg_dir为基址,创建恒等映射表, 在恒等映射表中,虚拟地址就等于物理地址。恒等映射表是为开启mmu的代码准备的,在当前内核中相关的代码有: __turn_mmu_on(), cpu_resume_mmu() and cpu_reset()。以__enable_mmu为例,其代码如下:
__turn_mmu_on:
	msr	sctlr_el1, x0           //开启mmu
	isb	                        //指令同步屏障
	br	x27                     
ENDPROC(__turn_mmu_on)
	这段代码所在的页,在恒等映射表中必须要有。因为在msr指令开启mmu之后,isb指令打断了指令流水,下一条指令br执行的时候会先根据pc+4查找指令地址,此时的pc是一个物理地址,pc+4是br x27这条指令的物理地址。内核在开启mmu之前会将恒等映射表存入TTBR0, swapper_pg_dir表存入TTBR1,br x27指令最终会到恒等映射表上寻找物理地址,恒等映射表物理地址等于虚拟地址,从而保证这条指令的正常执行。
	在内核中__turn_mmu_on(), cpu_resume_mmu() and cpu_reset()的代码一般都在同一个2MB页上,所以这里实际上映射__turn_mmu_on一个函数就够了,如果内核中有其他enable mmu的函数,则也需要加到恒等映射表中。
	*/
	//x0 = idm_tlb,二级页表地址。
	add	x0, x25, #PAGE_SIZE		
	//获取__turn_mmu_on函数的物理地址(adr是基于pc寻址,由于当前没有开启mmu,pc就是物理地址,所以adr寻址后的函数也是物理地址)
	adr	x3, __turn_mmu_on		
	//pgd = x25, tbl = x0, virt = x3。这个宏用来修改pgd的页表项,具体哪个页表项根据virt算出来,修改为tbl & 3(代表table)。这里的x3传入的是__turn_mmu_on的物理地址。
	create_pgd_entry x25, x0, x3, x5, x6
	/*
	idmap=1表示这里做恒等映射,x0=idm_tbl地址,x7为附加属性,x3为映射的起始物理地址(这个不一定是对齐的),x5这里实际上没用上,恒等映射不用start。end和start一个寄存器。这里实际上只是对x3(__turn_mmu_on)所在的2MB做了恒等映射
	*/
	create_block_map x0, x7, x3, x5, x5, idmap=1

	/*
	对内核做非恒等映射,内核起始虚拟地址是PAGE_OFFSET,映射到物理地址PHYS_OFFSET,大小为整个内核大小,用的pgd为swapper_pg_dir。
	*/
	//x0为swap_tbl的地址
	add	x0, x26, #PAGE_SIZE		// section table address
	//映射的起始虚拟地址
	mov	x5, #PAGE_OFFSET
	//pgd = x26, tbl = x0, virt = x5,修改swapper_pg_dir中某个pgd页表项的值,为 tbl &3, 哪个pgd页是由virt来决定的。
	create_pgd_entry x26, x0, x5, x3, x6
	//映射的结束位置
	ldr	x6, =KERNEL_END - 1
	//映射的起始物理地址PHYS_OFFSET
	mov	x3, x24				// phys offset
	//循环将内核整个映射到swapper_pg_dir
	create_block_map x0, x7, x3, x5, x6

	//对FDT的映射,先pass
	mov	x3, x21				// FDT phys address
	and	x3, x3, #~((1 << 21) - 1)	// 2MB aligned
	mov	x6, #PAGE_OFFSET
	sub	x5, x3, x24			    // subtract PHYS_OFFSET
	tst	x5, #~((1 << 29) - 1)   // within 512MB?
	csel	x21, xzr, x21, ne	// zero the FDT pointer
	b.ne	1f
	add	x5, x5, x6			// __va(FDT blob)
	add	x6, x5, #1 << 21	// 2MB for the FDT blob
	sub	x6, x6, #1			// inclusive range
	create_block_map x0, x7, x3, x5, x6
	ret
ENDPROC(__create_page_tables)

###create_pgd_entry

	/*
	 pgd  是要在哪个pgd中创建页表项
	 tbl  是要写入的内容(最终&3写入)
	 virt 是一个虚拟地址,往哪个页表项中写入,取决于这个虚拟地址
	 tmp1/tmp2是临时寄存器,没用。
	 create\_pgd\_entry的作用是修改pgd的页表项,具体哪个页表项根据virt算出来,修改为tbl & 3(代表table)。
	*/
	.macro	create_pgd_entry, pgd, tbl, virt, tmp1, tmp2
	//获取pgd的index, tmp1 = virt >> PGDIR_SHIFT 右移30位
	lsr	\tmp1, \virt, #PGDIR_SHIFT
	//tmp1 = tmp1 & 1FF 
	//这两句实际上是tmp1 = virt[38,30],取的是pgd的index
	and	\tmp1, \tmp1, #PTRS_PER_PGD - 1	// PGD index
	//tbl = tbl |3 这个3是代表TABLE, 表示还有下一级分页,pgd默认是table
	orr	\tmp2, \tbl, #3			// PGD entry table type
	//pgd的一个表项8byte,将tlb & 3 写入pgd[index]
	str	\tmp2, [\pgd, \tmp1, lsl #3]
	.endm

###create_block_map

	/*
	 tbl:   一个二级页表(pmd)页表的地址,这里是向二级页表项写入内容(pud为空)
	 flags: 每个映射页的附加属性
	 phys:  被映射到的起始物理地址
	 start: 映射的起始虚拟地址
	 end:   映射的结束虚拟地址
	 idmap: 是否为恒等映射
	 create_block_map是将虚拟地址[start,end]映射到物理地址phys开始的内存,映射大小必须在一个二级页表范围内(1GB),每个二级页表项附加属性flags,如果是恒等映射(idmap=1)则忽略start,直接映射[phys,end]。
	*/
	.macro	create_block_map, tbl, flags, phys, start, end, idmap=0
	//phys = phys >> BLOCK_SHIFT(21)
	lsr	\phys, \phys, #BLOCK_SHIFT
	//这里的目的是获取虚拟地址start 对应的pmd的index,正常情况下应该是(start >> 21) & 0x1ff (只取start[29,21]),如果是恒等映射(idmap = 1),则start应该=phys,所以直接取phys[29,21]就行
	.if	\idmap
	//PTRS_PER_PTE = 0x200, start = phys & 0x1ff
	and	\start, \phys, #PTRS_PER_PTE - 1	// table index
	.else
	//非恒等映射,取start[29,21]
	lsr	\start, \start, #BLOCK_SHIFT
	and	\start, \start, #PTRS_PER_PTE - 1	// table index
	.endif
	
	//phys = phys << 21 | flags (这个物理地址 & 属性标志,作为页表项内容)
	orr	\phys, \flags, \phys, lsl #BLOCK_SHIFT	// table entry
	//这里实际上是判断start和end是否是同一个寄存器,除非同一个寄存器,否则start都移位了,不可能等于end。
	.ifnc	\start,\end
	//end = end >> 21
	lsr	\end, \end, #BLOCK_SHIFT
	//end & = 0x1ff ,就是end = end[29,21];
	and	\end, \end, #PTRS_PER_PTE - 1		// table end index
	.endif
	
	//tbl[start * 8] = phys (这时候的start已经作为index了)
9999:	str	\phys, [\tbl, \start, lsl #3]		
	//如果start != end
	.ifnc	\start,\end
	//start ++; (index++)
	add	\start, \start, #1			// next entry
	//phys += 2MB;
	add	\phys, \phys, #BLOCK_SIZE		// next block
	cmp	\start, \end
	//映射下一个pmd
	b.ls	9999b
	.endif
	.endm

你可能感兴趣的:(嵌入式系统,linux-kernel,arm64,arm,linux)