RISC-V Linux匯編啟動過程分析

RISC-V linux的匯編啟動部分比較簡單,不算復雜。有兩個部分比較核心:頁表創建和重定向。頁表創建是用c語言寫的,今天先分析匯編部分,先帶大家分析整體匯編啟動流程,然后分析重定向。

注意:本文基于linux5.10.111內核

匯編啟動流程

先從整體分析匯編做的事情,有個大體框架。

路徑:arch/risc-v/kernel/head.S,入口是ENTRY(_start_kernel)

RISC-V Linux匯編啟動過程分析

從ENTRY(_start_kernel)開始進行啟動前的一些初始化,建立頁表前的主要工作:

  • 關閉所有中斷
/*?關閉所有中斷?*/ ????csrw?CSR_IE,?zero ????csrw?CSR_IP,?zero
/*?加載全局指針gp?*/ .option?push .option?norelax ????la?gp,?__global_pointer$ .option?pop
  • disable FPU
/*?禁用?FPU?以檢測內核空間中浮點的非法使用*/ ????li?t0,?SR_FS ????csrc?CSR_STATUS,?t0
  • 選擇一個核啟動
/*?選擇一個核啟動?*/ ????la?a3,?hart_lottery ????li?a2,?1 ????amoadd.w?a3,?a2,?(a3) ????bnez?a3,?.Lsecondary_start
  • 清楚bss段
/*?清除bss?*/ ????la?a3,?__bss_start ????la?a4,?__bss_stop ????ble?a4,?a3,?clear_bss_done
  • 保存hart id和dtb地址
/*?保存hatr?id和dtb地址,hart?id保存到a0,dtb地址保存到a1?*/ ????mv?s0,?a0 ????mv?s1,?a1 ????la?a2,?boot_cpu_hartid
  • 設置sp指針
????la?sp,?init_thread_union?+?THREAD_SIZE
  • 上述工作完成,會開始臨時頁表的創建,跳轉到C函數setup_vm建立臨時頁表
????mv?a0,?s1 ????call?setup_vm?//?跳轉到C函數setup_vm,setup_vm會創建臨時頁表
  • 重定向
#ifdef?CONFIG_MMU ????la?a0,?early_pg_dir ????call?relocate	//重定向,實際就是開啟MMU #endif
  • 設置異常向量地址,重載C環境
????call?setup_trap_vector /*?重載C環境?*/ ????la?tp,?init_task ????sw?zero,?TASK_TI_CPU(tp) ????la?sp,?init_thread_union?+?THREAD_SIZE
  • 最后跳轉到C函數start_kernel,開始C語言部分初始化,匯編部分執行完畢
tail?start_kernel

完整_start_kernel匯編代碼:

ENTRY(_start_kernel) 	/*?關閉所有中斷?*/ 	csrw?CSR_IE,?zero 	csrw?CSR_IP,?zero  	/*?在源碼中,這里有一個M模式處理的宏,這里沒有用到,直接跳過*/  	/*?加載全局指針gp?*/ .option?push .option?norelax 	la?gp,?__global_pointer$ .option?pop  	/*?禁用?FPU?以檢測內核空間中浮點的非法使用*/ 	li?t0,?SR_FS 	csrc?CSR_STATUS,?t0  #ifdef?CONFIG_SMP 	li?t0,?CONFIG_NR_CPUS 	blt?a0,?t0,?.Lgood_cores 	tail?.Lsecondary_park .Lgood_cores: #endif  	/*?選擇一個核啟動?*/ 	la?a3,?hart_lottery 	li?a2,?1 	amoadd.w?a3,?a2,?(a3) 	bnez?a3,?.Lsecondary_start  	/*?清除bss?*/ 	la?a3,?__bss_start 	la?a4,?__bss_stop 	ble?a4,?a3,?clear_bss_done clear_bss: 	REG_S?zero,?(a3) 	add?a3,?a3,?RISCV_SZPTR 	blt?a3,?a4,?clear_bss clear_bss_done:  	/*?保存hatr?id和dtb地址,hart?id保存到a0,dtb地址保存到a1?*/ 	mv?s0,?a0 	mv?s1,?a1 	la?a2,?boot_cpu_hartid 	REG_S?a0,?(a2)  	/*?初始化頁表,然后重定向到虛擬地址?*/ 	la?sp,?init_thread_union?+?THREAD_SIZE 	mv?a0,?s1 	call?setup_vm?//?跳轉到C函數setup_vm,setup_vm會創建臨時頁表 #ifdef?CONFIG_MMU 	la?a0,?early_pg_dir 	call?relocate	//重定向,實際就是開啟MMU #endif?/*?CONFIG_MMU?*/  	call?setup_trap_vector 	/*?重載C環境?*/ 	la?tp,?init_task 	sw?zero,?TASK_TI_CPU(tp) 	la?sp,?init_thread_union?+?THREAD_SIZE  #ifdef?CONFIG_KASAN 	call?kasan_early_init #endif 	/*?Start?the?kernel?*/ 	call?soc_early_init 	tail?start_kernel	//跳轉到C函數start_kernel,開始C語言部分初始化

匯編中非常重要的一個部分就是頁表的創建,關乎著后面的程序能不能繼續往下跑。setup_vm創建頁表后就會開始執行relocate重定向,這個重定向主要開啟mmu,下面分析relocate的匯編。

relocate

relocate重定向,就是在開啟mmu。開啟mmu的操作就是將一級頁表的地址以及權限寫到satp寄存器中,這就算開啟mmu了。

#ifdef?CONFIG_MMU ????la?a0,?early_pg_dir?//跳轉到relocate前,先把第一級頁表early_pg_dir的地址存入a0 ????call?relocate		//跳轉到relocate,開啟MMU #endif

relocate有兩次開啟mmu的操作,第一次開啟mmu使用的是setup_vm()建立的trampoline_gd_dir頁表,這頁表保存的是kernel的前2M內存。第二次開啟MMU使用的是early_pg_dir頁表,這個頁表映射了整個kernel內存以及dtb的4M空間。

如果trampoline_pg_dir或者early_pg_dir這兩個頁表的映射沒弄好的話,開啟MMU的時候就會失敗,所以頁表的建立十分關鍵。頁表創建后續再深究,下面分析relocate匯編代碼。

  • 計算返回地址

    返回地址就是ra加上虛擬地址和物理地址之間的偏移量,這個是固定偏移量。PAGE_OFFSET是kernel入口地址對應的虛擬地址,_start就是kernel入口地址的虛擬地址,PAGE_OFFSET – _start就得到它們之間的偏移,然后再和ra相加,就是返回地址。

/*?Relocate?return?address?*/ 	li?a1,?PAGE_OFFSET 	la?a2,?_start 	sub?a1,?a1,?a2 	add?ra,?ra,?a1
  • 將異常入口1f的虛擬地址寫入stvec寄存器

    因為一旦開啟MMU,地址都變成了虛擬地址,原來訪問的都是物理地址,開啟MMU時,地址發生了改變,VA != PA,從而進入異常,所以要先設置異常入口地址,此時的異常入口為1f。

/*?Point?stvec?to?virtual?address?of?intruction?after?satp?write?*/ 	la?a2,?1f 	add?a2,?a2,?a1 	csrw?CSR_TVEC,?a2
  • 提前計算切換到early_pg_dir頁表要寫入satp的值

再進入relocate之前,就已經把early_pg_dir賦值給a0了,所以a0是early_pg_dir。srl是邏輯右移,mmu使用的是sv39,虛擬地址39位,物理地址56位:

RISC-V Linux匯編啟動過程分析低12位是偏移量,所以PAGE_SHIFT等于12,將early_pg_dir地址右移12位存到a2。根據satp寄存器定義:

RISC-V Linux匯編啟動過程分析

MODE等于0x8代表使用sv39 mmu,0x0代表不進行地址翻譯,即不開啟MMU。這里STAP_MODE為sv39,即0x8。將early_pg_dir地址和SATP_MODE進行或運算后,即可得到寫入satp寄存器的值,最后保存到a2。

/*?Compute?satp?for?kernel?page?tables,?but?don't?load?it?yet?*/ 	srl?a2,?a0,?PAGE_SHIFT 	li?a1,?SATP_MODE	//sv39?mmu 	or?a2,?a2,?a1
  • 第一次開啟MMU,使用trampoline_pg_dir頁表

satp值的計算和上述是一樣的。開啟MMU之前,通過sfence.vma命令先刷新TLB。此時開啟MMU,就會進入下面的標號為1的匯編段

	la?a0,?trampoline_pg_dir 	srl?a0,?a0,?PAGE_SHIFT 	or?a0,?a0,?a1 	sfence.vma	 	csrw?CSR_SATP,?a0

進入異常1f段,重新設置異常入口為.Lsecondary_park,然后切換到early_pg_dir頁表,相當于第二次開啟MMU。此時,如果之前建立的early_pg_dir頁表不對,則會就進入.Lsecondary_park。.Lsecondary_park里面是個wfi指令,是個死循環

完整relocate匯編代碼:

relocate: 	/*?Relocate?return?address?*/ 	li?a1,?PAGE_OFFSET 	la?a2,?_start 	sub?a1,?a1,?a2 	add?ra,?ra,?a1  	/*?Point?stvec?to?virtual?address?of?intruction?after?satp?write?*/ 	la?a2,?1f 	add?a2,?a2,?a1 	csrw?CSR_TVEC,?a2  	/*?Compute?satp?for?kernel?page?tables,?but?don't?load?it?yet?*/ 	srl?a2,?a0,?PAGE_SHIFT 	li?a1,?SATP_MODE 	or?a2,?a2,?a1  	/* 	?*?Load?trampoline?page?directory,?which?will?cause?us?to?trap?to 	?*?stvec?if?VA?!=?PA,?or?simply?fall?through?if?VA?==?PA.??We?need?a 	?*?full?fence?here?because?setup_vm()?just?wrote?these?PTEs?and?we?need 	?*?to?ensure?the?new?translations?are?in?use. 	?*/ 	la?a0,?trampoline_pg_dir 	srl?a0,?a0,?PAGE_SHIFT 	or?a0,?a0,?a1 	sfence.vma 	csrw?CSR_SATP,?a0 .align?2 1: 	/*?Set?trap?vector?to?spin?forever?to?help?debug?*/ 	la?a0,?.Lsecondary_park 	csrw?CSR_TVEC,?a0  	/*?Reload?the?global?pointer?*/ .option?push .option?norelax 	la?gp,?__global_pointer$ .option?pop  	/* 	?*?Switch?to?kernel?page?tables.??A?full?fence?is?necessary?in?order?to 	?*?avoid?using?the?trampoline?translations,?which?are?only?correct?for 	?*?the?first?superpage.??Fetching?the?fence?is?guarnteed?to?work 	?*?because?that?first?superpage?is?translated?the?same?way. 	?*/ 	csrw?CSR_SATP,?a2 	sfence.vma  	ret

總結

? 版權聲明
THE END
喜歡就支持一下吧
點贊5 分享