標籤:ida 指令碼 ima blog init font mmu 定位 range
一、前言
本文沒有什麼架構性的東西,就是按照__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, lr
mov x0, x25-----------------------------(2)
add x1, x26, #SWAPPER_DIR_SIZE
bl __inval_cache_range
mov 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 1b
ldr 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"
#endif
adrp x5, __idmap_text_end-------------------------(7)
clz x5, x5
cmp x5, TCR_T0SZ(VA_BITS) ----------------------(8)
b.ge 1f
adr_l x6, idmap_t0sz---------------------------(9)
str x5, [x6]
dmb sy
dc ivac, x6
create_table_entry x0, x3, EXTRA_SHIFT, EXTRA_PTRS, x5, x6--------(10)
1:
#endif
create_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_range
mov 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