RISC-V linux的匯編啟動部分比較簡單,不算復雜。有兩個部分比較核心:頁表創建和重定向。頁表創建是用c語言寫的,今天先分析匯編部分,先帶大家分析整體匯編啟動流程,然后分析重定向。
注意:本文基于linux5.10.111內核
匯編啟動流程
先從整體分析匯編做的事情,有個大體框架。
路徑:arch/risc-v/kernel/head.S,入口是ENTRY(_start_kernel)

從ENTRY(_start_kernel)開始進行啟動前的一些初始化,建立頁表前的主要工作:
-
關閉所有中斷
/*?關閉所有中斷?*/ ????csrw?CSR_IE,?zero ????csrw?CSR_IP,?zero
-
加載全局指針gp
/*?加載全局指針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位:
低12位是偏移量,所以PAGE_SHIFT等于12,將early_pg_dir地址右移12位存到a2。根據satp寄存器定義:

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
總結