ARM64的启动过程之(四):打开MMU
原文地址:http://www.wowotech.net/linux_kenrel/turn-on-mmu.html
一、前言
經過漫長的前戲,我們終于迎來了打開MMU的時刻,本文主要描述打開MMU以及跳轉到start_kernel之前的代碼邏輯。這一節完成之后,我們就會離開痛苦的匯編,進入人民群眾喜聞樂見的c代碼了。
二、打開MMU前后的概述
對CPU以及其執行的程序而言,打開MMU是一件很有意思的事情,好象從現實世界一下子走進了奇妙的虛幻世界,本節,我們一起來看看內核是如何“穿越”的。下面這張圖描述了兩個不同的世界:
當沒有打開MMU的時候,cpu在進行取指以及數據訪問的時候是直接訪問物理內存或者IO memory。雖然64bit的CPU理論上擁有非常大的address space,但是實際上用于存儲kernel image的物理main memory并沒有那么大,一般而言,系統的main memory在低端的一小段物理地址空間中,如上圖右側的圖片所示。當打開MMU的時候,cpu對memory系統的訪問不能直接觸及物理空間,而是需要通過一系列的Translation table進行翻譯。虛擬地址空間分成三段,低端是0x00000000_00000000~0x0000FFFF_FFFFFFFF,用于user space。高端是0xFFFF0000_00000000~0xFFFFFFFF_FFFFFFFF,用于kernel space。中間的一段地址是無效地址,對其訪問會產生MMU fault。虛擬地址空間如上圖右側的圖片所示。
Linker感知的是虛擬地址,在將內核的各個object文件鏈接成一個kernel image的時候,kernel image binary code中訪問的都是虛擬地址,也就是說kernel image應該運行在Linker指定的虛擬地址空間上。問題來了,kernel image運行在那個地址上呢?實際上,將kernel image放到kernel space的首地址運行是一個最直觀的想法,不過由于各種原因,具體的arch在編譯內核的時候,可以指定一個offset(TEXT_OFFSET),對于ARM64而言是512KB(0x00080000),因此,編譯后的內核運行在0xFFFF8000_00080000的地址上。系統啟動后,bootloader會將kernel image copy到main memory,當然,和虛擬地址空間類似,kernel image并沒有copy到main memory的首地址,也保持了一個同樣size的offset?,F在,問題又來了:在kernel的開始運行階段,MMU是OFF的,也就是說kernel image是直接運行在物理地址上的,但是實際上kernel是被linker鏈接到了虛擬地址上去的,在這種情況下,在沒有turn on MMU之前,kernel能正常運行嗎?可以的,如果kernel在turn on MMU之前的代碼都是PIC的,那么代碼實際上是可以在任意地址上運行的。你可以仔細觀察turn on MMU之前的代碼,都是位置無關的代碼。
OK,解決了MMU turn on之前的問題,現在我們可以準備“穿越”了。真正打開MMU就是一條指令而已,就是將某個system register的某個bit設定為1之類的操作。這樣我們可以把相關指令分成兩組,turn on mmu之前的綠色指令和之后的橘色指令,如下圖所示:
由于現代CPU的設計引入了pipe, super scalar,out-of-order execution,分支預測等等特性,實際上在turn on MMU的指令執行的那個時刻,該指令附近的指令的具體狀態有些混亂,可能綠色指令執行的數據加載在實際在總線上發起bus transaction的時候已經啟動了MMU,本來它是應該訪問physical address space的。而也有可能橘色的指令提前執行,導致其發起的memory操作在MMU turn on之前就完成。為了解決這些混亂,可以采取一種投機取巧的辦法,就是建立一致性映射:假設kernel image對應的物理地址段是A~B這一段,那么在建立頁表的時候就把A~B這段虛擬地址段映射到A~B這一段的物理地址。這樣,在turn on MMU附近的指令是毫無壓力的,無論你是通過虛擬地址還是物理地址,訪問的都是同樣的物理memory。
還有一種方法,就是清楚的隔離turn on MMU前后的指令,那就是使用指令同步工具,如下:
指令屏障可以清清楚楚的把指令的執行劃分成三段,第一段是綠色指令,在執行turn on mmu指令執行之前全部完成,隨后啟動turn on MMU的指令,隨后的指令屏障可以確保turn on MMU的指令完全執行完畢(整個計算機系統的視圖切換到了虛擬世界),這時候才啟動橘色指令的取指、譯碼、執行等操作。
三、打開MMU的代碼
具體打開MMU的代碼在__enable_mmu函數中如下:
__enable_mmu:?
??? ldr??? x5, =vectors?
??? msr??? vbar_el1, x5 ---------------------------(1)?
??? msr??? ttbr0_el1, x25??????????? // load TTBR0 -----------------(2)?
??? msr??? ttbr1_el1, x26??????????? // load TTBR1?
??? isb?
??? msr??? sctlr_el1, x0 ---------------------------(3)?
??? isb?
??? br??? x27 -------------跳轉到__mmap_switched執行,不設定lr寄存器?
ENDPROC(__enable_mmu)
傳入該函數的參數有四個,一個是x0寄存器,該寄存器中保存了打開MMU時候要設定的SCTLR_EL1的值(在__cpu_setup函數中設定),第二個是個是x25寄存器,保存了idmap_pg_dir的值。第三個參數是x26寄存器,保存了swapper_pg_dir的值。最后一個參數是x27,是執行完畢該函數之后,跳轉到哪里去執行(__mmap_switched)。
(1)VBAR_EL1, Vector Base Address Register (EL1),該寄存器保存了EL1狀態的異常向量表。在ARMv8中,發生了一個exception,首先需要確定的是該異常將送達哪一個exception level。如果一個exception最終送達EL1,那么cpu會跳轉到這里向量表來執行。具體異常的處理過程由其他文檔描述,這里就不說了。
(2)idmap_pg_dir是為turn on MMU準備的一致性映射,未來將會用于用戶空間的進程,在進程切換的時候,其地址空間的切換實際就是修改TTBR0的值。TTBR1用于kernel space,所有的內核線程都是共享一個空間就是swapper_pg_dir。
(3)打開MMU。實際上在這條指令的上下都有isb指令,理論上已經可以turn on MMU之前之后的代碼執行順序嚴格的定義下來,其實我感覺不必要再啟用idmap_pg_dir的那些頁表了,當然,這只是猜測。
四、通向start_kernel
我痛恨匯編,如果能不使用匯編那絕對不要使用匯編,還好我們馬上就要投奔start_kernel:
__mmap_switched:?
??? adr_l??? x6, __bss_start?
??? adr_l??? x7, __bss_stop
1:??? cmp??? x6, x7?
??? b.hs??? 2f?
??? str??? xzr, [x6], #8 ---------------clear BSS?
??? b??? 1b?
2:?
??? adr_l??? sp, initial_sp, x4 -----------建立和swapper進程的鏈接?
??? str_l??? x21, __fdt_pointer, x5??????? // Save FDT pointer?
??? str_l??? x24, memstart_addr, x6??????? // Save PHYS_OFFSET?
??? mov??? x29, #0?
??? b??? start_kernel?
ENDPROC(__mmap_switched)
這段代碼分成兩個部分,一部分是清BSS,另外一部分是為進入c代碼做準備(主要是stack)。clear BSS段就是把未初始化的全局變量設定為0的初值,沒有什么可說的。要進入start_kernel這樣的c代碼,沒有stack可不行,那么如何設定stack呢?熟悉kernel的人都知道,用戶空間的進程當陷入內核態的時候,stack切換到內核棧,實際上就是該進程的thread info內存段(4K或者8K)的頂部。對于swapper進程,原理是類似的:
.set??? initial_sp, init_thread_union + THREAD_START_SP
如果說之前的代碼執行都處于一個孤魂野鬼的狀態,“adr_l??? sp, initial_sp, x4”指令執行之后,初始化代碼終于找到了歸宿,初始化代碼有了自己的thread info,有了自己的task struct,有了自己的pid,有了進程(內核線程)應該擁有的一切,從此之后的代碼歸屬idle進程,pid等于0的那個進程。
為了方便后面的代碼的訪問,這里還初始化了兩個變量,分別是__fdt_pointer(設備樹信息,物理地址)和memstart_addr(kernel image所在的物理地址,一般而言是main memory的首地址)。 memstart_addr主要用于main memory中物理地址和虛擬地址的轉換,具體可以參考__virt_to_phys和__phys_to_virt的實現。
五、參考文獻
1、ARM Architecture Reference Manual
change log:
1、2015-11-30,強調了初始化代碼和idle進程的連接
2、2015-12-2,修改了物理空間和虛擬空間的視圖
原創文章,轉發請注明出處。蝸窩科技
標簽: 打開MMU
?作業系統之前的程式 for rpi2 (1) - mmu (0) : 位址轉換|ARM64的啟動過程之(三):為打開MMU而進行的CPU初始化?評論:
amusion?2015-10-28 11:25 無意中看到了這個網站,拜讀了幾篇文章后,真是很欽佩啊,文章分析的很深入,不知能否寫些和SMP先關的分析 回復 linuxer?
2015-10-29 08:59 @amusion:這位客官,本站暫時不接受“點菜”,呵呵~~~開玩笑的,大家工作都很忙,業余時間寫寫文章,讓自己爽一下,所以,想到哪里寫到哪里,SMP的代碼分布在各個內核的子系統中,其實不是很好寫的。 回復 kitty?
2015-10-27 17:31 博主真的是辛苦了,像博主這樣靜下心搞鉆研,如此認真隱忍的,太少了。 回復 linuxer?
2015-10-27 18:30 @kitty:不辛苦,如果真心熱愛的話就不辛苦,^_^
喜歡鉆研的人很多,只是沒有聚合在一起,蝸窩這個網站就是因此而設立的,歡迎每一個沉醉于技術的人。 回復 kitty?
2015-10-30 10:38 @linuxer:hi linuxer,看你寫的power相關文章,寫的非常詳細。但power相關架構,很多要進行調整了,在今年年底,linuro要發布一個新的調度器架構EAS,會把DVFS和cpu idle添加CFS調度器中,而themal也會被IPA機制取代,這是新的研究方向,感興趣的話,可以一起學習。 回復 mobz?
2015-10-27 14:16 Hi linuxer,看到你最近有對系統啟動做詳細的分析,正好遇到個問題,想請教你下,就是在內核里的函數kernel_execve,有下面這樣一段匯編代碼,最終將調用到ret_to_user嗎?是個什么流程?
????asm(????"add????r0, %0, %1\n\t"
????????"mov????r1, %2\n\t"
????????"mov????r2, %3\n\t"
????????"bl memmove\n\t"????/* copy regs to top of stack */
????????"mov????r8, #0\n\t" /* not a syscall */
????????"mov????r9, %0\n\t" /* thread structure */
????????"mov????sp, r0\n\t" /* reposition stack pointer */
????????"b??ret_to_user"
????????:
????????: "r" (current_thread_info()),
??????????"Ir" (THREAD_START_SP - sizeof(regs)),
??????????"r" (®s),
??????????"Ir" (sizeof(regs))
????????: "r0", "r1", "r2", "r3", "r8", "r9", "ip", "lr", "memory"); 回復 linuxer?
2015-10-27 17:47 @mobz:無論是userspace還是kernel space,都有執行程序的需求。例如在內核空間,當把控制權轉交給userspace的時候,需要執行/sbin/init(也有可能是其他程序)。用戶空間的使用場景更多,你在terminal上輸入某一個程序的命令行的時候,shell程序會fork,然后調用execve來執行程序。
所謂執行某一個二進制程序其實就是內核態的loader將當前進程的地址空間(text,data,bss和stack)銷毀,使用新的可執行程序的image來創建新的進程的過程,因此返回調用函數是沒有任何意義的(實際上也不存在了)。但是,內核的loader總是要把控制權交給這個新創建的進程,因此,loader在這個新進程的內核棧上模擬了一次陷入內核的過程,在內核棧上構建了一個“現場”,然后調用ret_to_user返回userspace,開始新的進程的執行,當然,CPU的PC值會設定為二進制程序image的入口函數。 回復 mobz?
2015-10-27 20:03 @linuxer:ret_to_user會調用到arch_ret_to_user r1, lr 這里,可是我還是沒搞懂這個arch_ret_to_user又是如何實現的?代碼中怎么也搜索不到??還是我沒看懂。
因為我經常遇到內核啟動到Freeing init memory后就卡住的問題.定位發現應該就是卡在返回用戶空間來是執行init進程的時候出現的問題,就像類是bootargs設置錯誤導致
ENTRY(ret_to_user)
ret_slow_syscall:
????disable_irq???????????? @ disable interrupts
ENTRY(ret_to_user_from_irq)
????ldr r1, [tsk, #TI_FLAGS]
????tst r1, #_TIF_WORK_MASK
????bne work_pending
no_work_pending:
#if defined(CONFIG_IRQSOFF_TRACER)
????asm_trace_hardirqs_on
#endif
????/* perform architecture specific actions before user return */
????arch_ret_to_user r1, lr
????restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user) 回復 linuxer?
2015-10-28 08:58 @mobz:我正在讀的是4.1.10版本中的ARM64的代碼,其中其實都沒有kernel_execve這個函數,也沒有arch_ret_to_user。
看起來arm平臺有arch_ret_to_user的定義,在linux/arch/arm/kernel/entry-common.S文件中:
#ifdef CONFIG_NEED_RET_TO_USER
#include <mach/entry-macro.S>
#else
????.macro??arch_ret_to_user, tmp1, tmp2
????.endm
#endif 回復 mobz?
2015-10-28 09:49 @linuxer:恩,是的,但是這個是怎么實現從return to userspace的?沒看懂只有定義,實現呢?不理解這里 回復 linuxer?
2015-10-28 12:28 @mobz:與其說arch_ret_to_user是architecture specific,不如說是ARM arch下,machine specific。對于有些特殊的arm machine(例如ARCH_IOP13XX),在返回用戶空間的時候需要特別的操作,但是對于大部分的ARM處理器,arch_ret_to_user是空的。 回復 mobz?
2015-10-28 13:39 @linuxer:那這么說來,其實從內核到用戶空間就是:下面這三條語句(指令)了咯???
????disable_irq
????ldr r1, [tsk, #TI_FLAGS]
????tst r1, #_TIF_WORK_MASK linuxer?
2015-10-28 14:41 @linuxer:后面不是有一個restore_user_regs嘛,用于從內核棧恢復userspace的上下文 mobz?
2015-10-28 18:46 @linuxer:在你最新的答復上回復不了,就回復在這里吧,restore_user_regs 這個和arch_ret_to_user在ARM平臺上也是一樣的,都是空,我并沒在代碼中找到相關的內容,倒是在FRV平臺里找到有 linuxer?
2015-10-29 08:35 @linuxer:在arch/arm/kernel/entry-head.S中有定義(我的內核版本是4.1.10,其他版本應該類似吧)。 passerby?
2015-10-27 09:10 我在paging_init中的map_mem
for_each_memblock(memory, reg) {
????????phys_addr_t start = reg->base;
????????phys_addr_t end = start + reg->size;
????????if (start >= end)
????????????break;
????????????????create_mapping(start, __phys_to_virt(start), end - start,
????????????????????false);
????}
對所有的memblock進行映射,這里就包括了在匯編中映射好的kernel image。 回復 linuxer?
2015-10-27 15:30 @passerby:要回答這個問題,需要理解memory blocks模塊(mm/memblock.c文件)。
該模塊定義了一個全局變量:
struct memblock memblock;
這個全局變量用來管理系統中的所有的memory blocks。這些blocks分成兩種:
1、reserved
2、memory
從邏輯上講,無論那種類型的block,其address region不能是overlap的(因此會有memory region的分裂和合并的操作)。
在函數arm64_memblock_init函數中會調用memblock_reserve(__pa(_text), _end - _text)將kernel image的這段memory region加入“reserved”那種類型的memory blocks。
而你說的for_each_memblock(memory, reg) ,僅僅是遍歷“memory”那種類型的memroy blocks。而一旦你reserved了kernel image對應的那一段memory region,它是不會出現在memory類型的blocks中,因此不會再次mapping。
BTW,我非常簡單的過了一下memblock.c的代碼,很多是從邏輯上推導的,可能有誤。 回復 passerby?
2015-10-27 16:52 @linuxer:對于不做mapping有點疑問,因為其他的reserved的內存也需要被做映射啊。比如
91???????? cont_splash_mem: splash_region@83000000 {
??92???????????? linux,reserve-contiguous-region;
??93???????????? linux,reserve-region;
??94???????????? reg = <0x0 0x83000000 0x0 0x2000000>;
??95???????????? label = "cont_splash_mem";
??96???????? };
kernel image可以不做,但是其他的reserved空間并沒有做mapping。在其他地方我并沒有看到對reserverd做mapping的地方,只有在這里有看到。 回復 linuxer?
2015-10-27 18:26 @passerby:通過device tree中的reserved-memory節點可以定義若干的reserved memory block,這些保留的memory region基本上被認為是for特定驅動使用的,因此,我的觀點是:內核不會進行mapping,使用這些memory的driver應該負責進行mapping。 回復 passerby?
2015-10-26 11:18 @linuxer,有個小問題,在匯編中建立的頁表是將整個內核都做了映射嗎? 回復 linuxer?
2015-10-26 12:14 @passerby:是的,是將整個kernel image進行了映射,開始和結束的定義是:
#define KERNEL_START????_text
#define KERNEL_END????_end
當然,和整個內核空間比,這一段實際上也不大的。 回復 passerby?
2015-10-26 13:05 @linuxer:那映射完了,在start_kernel又會做一次page_init映射。這個時候匯編中做的映射會被拋棄掉重新做?不過不是,那這部分page怎么告訴伙伴系統這部分空間已經被占用的? 回復 linuxer?
2015-10-26 19:02 @passerby:那映射完了,在start_kernel又會做一次page_init映射。這個時候匯編中做的映射會被拋棄掉重新做?
------------------------------------
在start_kernel中不會拋棄”匯編時代“的映射,因為沒有必要,都已經建立了又何必毀掉呢。
那這部分page怎么告訴伙伴系統這部分空間已經被占用的?
------------------------------
伙伴系統其實不需要管理kernel image占用的內存,這是通過arm64_memblock_init函數中的下面代碼實現:
memblock_reserve(__pa(_text), _end - _text); 回復 donkey?
2015-12-30 17:05 @linuxer:有個問題想請教一下,我最近在看ARM64啟動的代碼,現在正在研究跳到內核后正式頁表的創建流程。在paging_init函數的map_mem函數中會對memory類型的內存創建頁表,但是我怎么都找不到memory類型的內存是怎么加到memblock中的。只是在arm64_memblock_init中看到把一些內存區域加到了reserved類型的memblock中,但并沒有什么代碼把內存加到memory類型的memblock中啊? 回復 linuxer?
2015-12-30 17:59 @donkey:我沒有看代碼,不過推測是在device tree相關的代碼中。 回復 donkey?
2015-12-31 11:19 @linuxer:果然是高手,按照你的提示,我又仔細研究了一下代碼流程,發現了這樣一條函數調用流程:
setup_arch-->setup_machine_fdt-->early_init_dt_scan-->early_init_dt_scan_nodes-->early_init_dt_add_memory_arch-->memblock_add
經過這個流程,感覺memory類型的內存區域就有著落了,呵呵!再次感謝! donkey?
2015-12-31 11:23 @linuxer:剛才那個流程不知道為什么沒顯示完全,再補充一下:
...->early_init_dt_add_memory_arch-->memblock_add
總結
以上是生活随笔為你收集整理的ARM64的启动过程之(四):打开MMU的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: ARM64的启动过程之(三):为打开MM
- 下一篇: ARM64的启动过程之(五):UEFI