Linux內存初始化(二)identity mapping和kernel image mapping
一、前言
本文沒有什麽框架性的東西,就是按照__create_page_tables代碼的執行路徑走讀一遍,記錄在初始化階段,內核是如何創建內核運行需要的頁表過程。想要了解一些概述性的、框架性的東西可以參考內存初始化文檔。
本文的代碼來自ARM64,內核版本是4.4.6,此外,閱讀本文最好熟悉ARMv8中翻譯表描述符的格式。
二、create_table_entry
這個宏定義主要是用來創建一個中間level的translation table中的描述符。如果用linux的術語,就是創建PGD、PUD或者PMD的描述符。如果用ARM64術語,就是創建L0、L1或者L2的描述符。具體創建哪一個level的Translation table descriptor是由tbl參數指定的,tbl指向了該translation table的內存。virt參數給出了要創建地址映射的那個虛擬地址,shift參數以及ptrs參數是和具體在哪一個entry中寫入描述符有關。我們知道,在定位頁表描述的時候,我們需要截取虛擬地址中的一部分做為offset(index)來定位描述符,實際上,虛擬地址右移shift,然後截取ptrs大小的bit field就可以得到entry index了。tmp1和tmp2是臨時變量。create_table_entry的代碼如下:
.macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
lsr \tmp1, \virt, #\shift
and \tmp1, \tmp1, #\ptrs - 1 // table index-------------------(1)
add \tmp2, \tbl, #PAGE_SIZE-------------------------(2)
orr \tmp2, \tmp2, #PMD_TYPE_TABLE---------------------(3)
str \tmp2, [\tbl, \tmp1, lsl #3]--------------------------(4)
add \tbl, \tbl, #PAGE_SIZE---------------------------(5)
.endm
(1)tmp1中保存virt地址對應在Translation table中的entry index。
(2)初始階段的頁表頁表定義在鏈接腳本中,如下:
BSS_SECTION(0, 0, 0)
. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;
初始階段的頁表(PGD/PUD/PMD/PTE)都是排列在一起的,每一個占用一個page。也就是說,如果create_table_entry當前操作的是PGD,那麽tmp2這時候保存了下一個level的頁表,也就是PUD了。
(3)這一步是合成描述符的數值。光有下一級translation table的地址不行,還要告知該描述符是否有效(bit 0),該描述符的類型是哪一種類型(bit 1)。對於中間level的頁表,該描述符不可能是block entry,只能是table type的描述符,因此該描述符的最低兩位是0b11。
#define PMD_TYPE_TABLE (_AT(pmdval_t, 3) << 0)
(4)這是最關鍵的一步,將描述符寫入頁表中。之所以有“lsl #3”操作,是因為一個描述符占據8個Byte。
(5)將translation table的地址移到next level,以便進行下一步設定。
三、create_pgd_entry
從字面上看,create_pgd_entry似乎是用來在PGD中創建一個描述符,但是,實際上該函數不僅僅創建PGD中的描述符,如果需要下一級的translation table,例如PUD、PMD,也需要同時建立,最終的要求是能夠完成所有中間level的translation table的建立(其實每個table中都是只建立了一個描述符),僅僅留下PTE,由其他代碼來完成。該函數需要四個參數:tbl是pgd translation table的地址,具體要創建哪一個地址的描述符由virt指定,tmp1和tmp2是臨時變量,create_pgd_entry具體代碼如下:
.macro create_pgd_entry, tbl, virt, tmp1, tmp2
create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2-------(1)
#if SWAPPER_PGTABLE_LEVELS > 3------------------------(2)
create_table_entry \tbl, \virt, PUD_SHIFT, PTRS_PER_PUD, \tmp1, \tmp2--------(3)
#endif
#if SWAPPER_PGTABLE_LEVELS > 2------------------------(4)
create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
#endif
.endm
(1)create_table_entry 在上一節已經描述了,這裏通過調用該函數在PGD中為虛擬地址virt創建一個table type的描述符。
(2)SWAPPER_PGTABLE_LEVELS這個宏定義和ARM64_SWAPPER_USES_SECTION_MAPS相關,而這個宏在蝸窩已經有一篇文章描述,這裏就不說了。SWAPPER_PGTABLE_LEVELS其實定義了swapper進程地址空間的頁表的級數,可能3,也可能是2,具體中間的Translation table有多少個level是和配置相關的,如果是section mapping,那麽中間level包括PGD和PUD就OK了,PMD是最後一個level。如果是page mapping,那麽需要PGD、PUD和PMD這三個中間level,PTE是最後一個level。當然,如果整個page level是3或者2的時候,也有可能不存在PUD或者PMD這個level。
(3)當SWAPPER_PGTABLE_LEVELS > 3的時候,需要創建PUD這一級的Translation table。
(4)當SWAPPER_PGTABLE_LEVELS > 2的時候,需要創建PMD這一級的Translation table。
上面太枯燥了,我們給出一些實例:
例子1:當虛擬地址是48個bit,4k page size,這時候page level等於4,映射關系是PGD(L0)--->PUD(L1)--->PMD(L2)--->Page table(L3)--->page,但是如果采用了section mapping(4k的page一定會采用section mapping),映射關系是PGD(L0)--->PUD(L1)--->PMD(L2)--->section。在create_pgd_entry函數中將創建PGD和PUD這兩個中間level。
例子2:當虛擬地址是48個bit,16k page size(不能采用section mapping),這時候page level等於4,映射關系是PGD(L0)--->PUD(L1)--->PMD(L2)--->Page table(L3)--->page。在create_pgd_entry函數中將創建PGD、PUD和PMD這三個中間level。
例子3:當虛擬地址是39個bit,4k page size,這時候page level等於3,映射關系是PGD(L1)--->PMD(L2)--->Page table(L3)--->page。由於是4k page,因此采用section mapping,映射關系是PGD(L1)--->PMD(L2)--->section。在create_pgd_entry函數中將創建PGD這一個中間level。
四、create_block_map
create_block_map的名字起得不錯,該函數就是在tbl指定的Translation table中建立block descriptor以便完成address mapping。具體mapping的內容是將start 到 end這一段VA mapping到phys開始的PA上去,代碼如下:
.macro create_block_map, tbl, flags, phys, start, end
lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT
lsr \start, \start, #SWAPPER_BLOCK_SHIFT
and \start, \start, #PTRS_PER_PTE - 1 // table index
orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT // table entry
lsr \end, \end, #SWAPPER_BLOCK_SHIFT
and \end, \end, #PTRS_PER_PTE - 1 // table end index
9999: str \phys, [\tbl, \start, lsl #3] // store the entry
add \start, \start, #1 // next entry
add \phys, \phys, #SWAPPER_BLOCK_SIZE // next block
cmp \start, \end
b.ls 9999b
.endm
五、__create_page_tables
1、準備階段
__create_page_tables:
adrp x25, idmap_pg_dir------------------------(1)
adrp x26, swapper_pg_dir
mov x27, lrmov x0, x25-----------------------------(2)
add x1, x26, #SWAPPER_DIR_SIZE
bl __inval_cache_rangemov x0, x25-----------------------------(3)
add x6, x26, #SWAPPER_DIR_SIZE
1: stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
stp xzr, xzr, [x0], #16
cmp x0, x6
b.lo 1bldr x7, =SWAPPER_MM_MMUFLAGS-----------------(4)
(1)取idmap_pg_dir這個符號的物理地址,保存到x25。取swapper_pg_dir這個符號的物理地址,保存到x26。這段代碼沒有什麽特別要說明的,除了adrp這條指令。adrp是計算指定的符號地址到run time PC值的相對偏移(不過,這個offset沒有那麽精確,是以4K為單位,或者說,低12個bit是0)。在指令編碼的時候,立即數(也就是 offset)占據21個bit,此外,由於偏移計算是按照4K進行的,因此最後計算出來的符號地址必須要在該指令的-4G和4G之間。由於執行該指令的 時候,還沒有打開MMU,因此通過adrp獲取的都是物理地址,當然該物理地址的低12個bit是全零的。此外,由於在鏈接腳本中 idmap_pg_dir和swapper_pg_dir是page size aligned,因此使用adrp指令也是OK的。
(2)這段代碼是要進行invalid cache的操作了,具體要操作的範圍就是identity mapping和kernel image mapping所對應的頁表區域,起始地址是idmap_pg_dir,結束地址是swapper_pg_dir+SWAPPER_DIR_SIZE。
為什麽要調用__inval_cache_range來invalidate idmap_pg_dir和swapper_pg_dir對應頁表空間的cache呢?根據boot protocol,代碼執行到此,對於cache的要求是kernel image對應的那段空間的cache line是clean到PoC的,不過idmap_pg_dir和swapper_pg_dir對應頁表空間不屬於kernel image的一部分,因此其對應的cacheline很可能有一些舊的,無效的數據,必須要清理掉。
(3)將idmap和swapper頁表內容設定為0是有意義的。實際上這些translation table中的大部分entry都是沒有使用的,PGD和PUD都是只有一個entry是有用的,而PMD中有效的entry數目是和mapping的地 址size有關。將頁表內容清零也就是意味著將頁表中所有的描述符設定為invalid(描述符的bit 0指示是否有效,等於0表示無效描述符)。
(4)要創建mapping除了需要VA和PA,還需要memory attribute的參數,這個參數定義如下:
#if ARM64_SWAPPER_USES_SECTION_MAPS
#define SWAPPER_MM_MMUFLAGS (PMD_ATTRINDX(MT_NORMAL) | SWAPPER_PMD_FLAGS)
#else
#define SWAPPER_MM_MMUFLAGS (PTE_ATTRINDX(MT_NORMAL) | SWAPPER_PTE_FLAGS)
#endif
為了理解這些定義,需要理解block type和page type的描述符的格式,大家自行對照ARMv8文檔,這裏就不貼圖了。SWAPPER_MM_MMUFLAGS這個flag其實定義了要映射地址的memory attribut。對於kernel image這一段內存,當然是普通內存,因此其中的MT_NORMAL就是表示後續的地址映射都是為normal memory而創建的。其他的flag定義如下:
#define SWAPPER_PTE_FLAGS (PTE_TYPE_PAGE | PTE_AF | PTE_SHARED)
#define SWAPPER_PMD_FLAGS (PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S)
PMD_SECT_AF(PTE_AF)中的AF是access flag的縮寫,這個bit用來表示該entry是否第一次使用(當程序訪問對應的page或者section的時候,就會使用該entry,如果從來沒有被訪問過,那麽其值等於0,否者等於1)。該bit主要被操作系統用來跟蹤一個page是否被使用過(最近是否被訪問),當該page首次被創建的時候,AF等於0,當代碼第一次訪問該page的時候,會產生MMU fault,這時候,異常處理函數應該設定AF等於1,從而阻止下一次訪問該page的時候產生MMU Fault。在這裏,kernel image對應的page,其描述符的AF bit都設定為1,表示該page當前狀態是actived(最近被訪問),因為只有用戶空間進程的page才會根據AF bit來確定哪些page被swap out,而kernel image對應的page是always actived的。
PMD_SECT_S(PTE_SHARED)對應shareable attribute bits,這個兩個bits定義了該page的shareable attribute。那麽是shareable attribute呢?shareable attribute定義了memory location被多個系統中的bus master共享的屬性。具體定義如下:
SH[1:0] | Normal memory |
00 | Non-shareable |
01 | 無效 |
10 | outer shareable |
11 | inner shareble |
這裏memory attribute中SH被設定為0b11,即inner shareable。如果一個page被標註為inner shareable,那麽在inner shareable domain中,所有的bus mast訪問該page中的memory都是coherent的(HW會處理cache coherence問題),軟件不需要考慮cache。一般而言,所有cpu core組成了inner shareable domain,也就是說,對於kernel direct mapping而言,其對應的內存對所有的cpu core的訪問都是coherent的。
memory attribute中其他的flag都沒有顯式指定,也就是說它們的值都是0,我們可以簡單過一下。AP的值是0,表示該page對kernel mode(EL1)是read/write的,對於userspace(EL0),是不允許訪問的。nG bit是0,表示該地址翻譯是全局的,不是process-specific的,這也合理,內核page的映射當然是全局的了。
2、建立identity mapping
mov x0, x25 -------------------------(1)
adrp x3, __idmap_text_start --------------------(2)#ifndef CONFIG_ARM64_VA_BITS_48---------------------(3)
#define EXTRA_SHIFT (PGDIR_SHIFT + PAGE_SHIFT - 3)-----------(4)
#define EXTRA_PTRS (1 << (48 - EXTRA_SHIFT)) ---------------(5)
#if VA_BITS != EXTRA_SHIFT-------------------------(6)
#error "Mismatch between VA_BITS and page size/number of translation levels"
#endifadrp x5, __idmap_text_end-------------------------(7)
clz x5, x5
cmp x5, TCR_T0SZ(VA_BITS) ----------------------(8)
b.ge 1fadr_l x6, idmap_t0sz---------------------------(9)
str x5, [x6]
dmb sy
dc ivac, x6create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6--------(10)
1:
#endifcreate_pgd_entry x0, x3, x5, x6-----------------------(11)
mov x5, x3 // __pa(__idmap_text_start)
adr_l x6, __idmap_text_end // __pa(__idmap_text_end)
create_block_map x0, x7, x3, x5, x6---------------------(12)
(1)x0保存了idmap_pg_dir變量的物理地址,也就是identity mapping的PGD。
(2)x3保存了__idmap_text_start的物理地址,對於identity mapping而言,x3也保存了虛擬地址,因為虛擬地址是等於物理地址的。
(3)基本上創建identity mapping是沒有什麽大問題的,但是,如果物理內存的地址位於非常高的位置,那麽在進行identity mapping就有問題了,因為有可能你配置的VA_BITS不夠大,超出了虛擬地址的範圍。這時候,就需要擴展virtual address range了。當然,如果配置了48bits的VA_BITS就不存在這樣的問題了,因為ARMv8最大支持的VA BITS就是48個,根本不可能擴展了。
(4)在虛擬地址地址不是48 bit,而系統內存的物理地址又放到了非常非常高的位置,這時候,為了完成identity mapping,我們必須要擴展虛擬地址,那麽擴展多少呢?擴展到48個bit。擴展之後,增加了一個EXTRA的level,地址映射關系是EXTRA--->PGD--->……,其中EXTRA_SHIFT等於(PGDIR_SHIFT + PAGE_SHIFT - 3)。
(5)擴展之後,地址映射多個一個level,我們稱之EXTRA level,該level的Translation table中有多少個entry呢?EXTRA_PTRS給出了答案。
(6)其實現行的linux kernel中,對地址映射是有要求的,即要求PGD是滿的。例如:48 bit的虛擬地址,4k的page size,對應的映射關系是PGD(9-bit)+PUD(9-bit)+PMD(9-bit)+PTE(9-bit)+page offset(12-bit),對於42bit的虛擬地址,64k的page size,對應的映射關系是PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。這兩種例子有一個共同的特點就是PGD中的entry數目都是滿的,也就是說索引到PGD的bit數目都是PAGE_SIZE-3。如果不滿足這個關系,linux kernel會認為你的配置是有問題的。註意:這是內核的要求,實際上ARM64的硬件沒有這麽要求。
正因為正確的配置下,PGD都是滿的,因此擴展之後EXTRA_SHIFT一定是等於VA_BITS的,否則一定是你的配置有問題。我們延續上一個實例來說明如何擴展虛擬地址的bit數目。對於42bit的虛擬地址,64k的page size,擴展之後,虛擬地址是48個bit,地址映射關系是EXTRA(6-bit)+ PGD(13-bit)+ PTE(13-bit)+ page offset(16-bit)。
(7)x5保存了__idmap_text_end的物理地址,之所以這麽做是因為需要確定identity mapping的最高的物理地址,計算該物理地址的前導0有多少個,從而可以判斷該地址是否是位於物理地址空間中比較高的位置。
(8)宏定義TCR_T0SZ可以計算給定虛擬地址數目下,前導0的個數。如果虛擬地址是48的話,那麽前導0是16個。如果當前物理地址的前導0的個數(x5的值)還有小於當前配置虛擬地址的前導0的個數,那麽就需要擴展。
(9)OK,現在進入需要擴展的分支,當然,具體要配置虛擬地址是通過TCR_EL1寄存器中的T0SZ域進行的,現在還不是時候(具體的設定在__cpu_setup函數中),這裏,我們只要設定idmap_t0sz這個變量值就OK了,在__cpu_setup函數中會從該變量取值並設定到TCR_EL1寄存器中的。代碼中,x6是idmap_t0sz變量的物理地址,x5是物理地址前導0的個數,將其保存到idmap_t0sz變量中。
(10)創建extra translation table的entry。具體傳遞的參數如下:
x0:頁表地址idmap_pg_dir
x3:準備映射的虛擬地址(雖然x3保存的是物理地址,但是identity mapping嘛,VA和PA都是一樣的)
EXTRA_SHIFT:正常建立最高level mapping的時候, shift是PGDIR_SHIFT,但是,由於物理地址位置太高,需要額外的映射,因此這裏需要再加上一個level的mapping,因此shift需要PGDIR_SHIFT + (PAGE_SHIFT - 3)。
EXTRA_PTRS:增加了一個level的Translation table,我們需要確定這個增加level的Translation table中包含的描述符的數目,EXTRA_PTRS給出了這個參數。
(11)create_pgd_entry這個函數上面解釋過了,建立各個中間level的table描述符。
(12)創建最後一個level translation table的entry。該entry可能是page descriptor,也可能是block descriptor,具體傳遞的參數如下:
x0:指向最後一個level的translation table
x7:要創建映射的memory attribute
x3:物理地址
x5:虛擬地址的起始地址(其實和x3一樣)
x6:虛擬地址的結束地址
3、創建kernel direct mapping
mov x0, x26 ------------------------(1)
mov x5, #PAGE_OFFSET-----------------------(2)
create_pgd_entry x0, x5, x3, x6---------------------(3)
ldr x6, =KERNEL_END // __va(KERNEL_END)
mov x3, x24 // phys offset
create_block_map x0, x7, x3, x5, x6 -------------------(4)
mov x0, x25
add x1, x26, #SWAPPER_DIR_SIZE
dmb sy
bl __inval_cache_rangemov lr, x27
ret
(1)swapper_pg_dir其實就是swapper進程(pid等於0的那個,其實就是idle進程)的地址空間,這時候,x0指向了內核地址空間的PGD的基地址。
(2)PAGE_OFFSET是kernel image的首地址,對於48bit的VA而言,該地址是0xffff8000-00000000
(3)創建PAGE_OFFSET(即kernel image首地址,虛擬地址)對應中間level的table描述符。
(4)創建PAGE_OFFSET~KERNEL_END之間地址映射的最後一個level的描述符。
參考文獻:
1、ARMv8技術手冊
2、Linux 4.4.6內核源代碼
Linux內存初始化(二)identity mapping和kernel image mapping