初學者學習Linux系統地址轉換時,如果只是學習理論,又或者研讀程式碼,那可能感覺比較枯燥。此時如果可以利用某些工具實際觀察一下地址轉換的過程,那可能會給枯燥的核心學習帶來些微的樂趣。crash tool是一款核心除錯工具,常用來分析核心崩潰問題。我們可以手動觸發核心崩潰,然後借用該工具來分析當時系統的執行情況,當然也包括記憶體的執行情況。
本文基於ARMv8 AArch64 (簡稱ARM64)架構,Linux 4.14核心來講述。首先回顧一下記憶體訪問的相關知識點。
1、ARM記憶體訪問的硬體架構
ARM有MMU部件,現代作業系統一般都會啟用MMU來訪問記憶體。啟用MMU之後,多程式就有了可能,每個程式可以維護各自私有的虛擬地址空間,無需關心實體記憶體佈局。
2、虛擬地址空間到實體地址空間的對映
虛擬地址到實體地址的對映是通過查表的機制來實現,下圖是一種典型的地址對映佈局。核心空間地址的高16位(bit[63:48])為全1,其轉換表的基地址存放在TTBR1_EL1暫存器中;使用者空間地址的高16位(bit[63:48])為全0,其轉換表的基地址存放在TTBR0_EL0暫存器中。
除了高16位外,剩下的48位,也並不全用作虛擬地址空間,使用多少位是可以配置的,比如Linux系統的核心一般做如下配置,表示有39位虛擬地址空間。
CONFIG_ARM64_VA_BITS=39
39位虛擬地址空間,核心空間範圍為0xFFFFFF80_00000000 ~ 0xFFFFFFFF_FFFFFFFF,使用者空間範圍為0x00000000_00000000 ~ 0x0000007F_FFFFFFFF。
3、轉換表的格式
轉換表有4個級別,level 0 ~ level 3。
如下圖所示,當bit[1:0]為2b'11時,表示該表項是Table descriptor,指向下一級轉換表的地址。而當 bit[1:0]為2b'01時,表示Block entry,不指向下一級轉換表,而是直接輸出block address。當表項處於level 3時,即使bit[1:0]為2b'11,也不再指向下一級轉換表,而是輸出block address。
4、地址轉換的過程
如下圖所示,以39位虛擬地址為例,來了解地址轉換過程。圖片是取自ARMv8官方文件,建議放大了看。
記憶體中維護著三個轉換表,從虛擬地址轉換成實體地址,要經過三次查表的過程。
39位虛擬地址被分成了4部分,作用如下:
-
bit[38:30] —— 索引第一級表中的表項
-
bit[29:21] —— 索引第二級表中的表項
-
bit[20:12] —— 索引第三級表中的表項
-
bit[11:0] —— 頁內偏移
第一步,TTBR暫存器中存放了第一級轉換表的起始地址,虛擬地址的bit[38:30]的值表示要查詢轉換表中的第幾項,這個值左移三位(64位地址,每個表項佔用8位元組),再加上第一級轉換表的地址,就是該表項的地址。該表項存放的是第二級轉換表的起始地址。
第二步,用第二級轉換表的起始地址,再結合虛擬地址的bit[29:21],與第一步計算方法類似,可以計算出第二級表項的地址。第二級表項中存放了第三級轉換表的起始地址。
第三步,用第三級轉換表的起始地址,再結合虛擬地址的bit[20:12],與第一步計算方法類似,可以計算出第三級表項的地址。第三級表項存放的是最終的頁面描述符,有頁面的實體地址資訊,用這個地址再加上虛擬地址的bit[11:0],就是該虛擬地址對應的實體地址。
5、Linux核心中的關鍵資料結構
mm_struct結構體是記憶體描述符,核心用它來維護一個程式的地址空間的所有資訊。這個結構體中包含了一個重要成員:pgd指標,pgd的名稱是頁全域性目錄,指向的是第一級轉換表的的起始地址。
每個程式的task_struct結構體中,都包含了記憶體描述符。
init_mm全域性變數,是核心本身的記憶體描述符。
有了以上知識點做支撐後,就可以用crash tool來驗證自己的理解了。
6、用crash tool觀察地址轉換
手動觸發核心崩潰的shell指令是:
echo "c" > /proc/sysrq-trigger
crash tool使用示例如下:
crash_arm64 vmlinux dumpfile -m phys_offset=0x80000000
進入crash tool環境後,我們選擇1號程式,也就是init程式來分析。首先用bt命令看一下1號程式當前的呼叫棧。
我們選擇該程式TASK的地址和SP暫存器指向的地址來進行實際分析。TASK的高位地址全為1,為核心空間的虛擬地址;SP地址高位全為0,為使用者空間的地址。
先分析使用者空間的虛擬地址 0000007feeac5cb0,將它分解如下:
bit[38:30] 0x1ff ,左移三位是 0xff8
bit[29:21] 0x175,左移三位是 0xba8
bit[20:12] 0x0c5,左移三位是 0x628
bit[11:0] 0xcb0
怎麼找到它對應的實體地址呢?首先要找到第一級轉換表所在的位置,也就是pgd的位置。我們在前面提到過,程式的記憶體描述中有pgd指標。所以我們可以先通過crash tool的task命令檢視記憶體描述符的地址,再通過struct命令檢視記憶體描述符中pgd指標的值。
知道了第一級轉換表所在的位置,結合虛擬地址的bit[38:30],就可以算出虛擬地址在第一級轉換表中所對應的表項位置:0xffffffc01bf60000 + 0xff8 = 0xffffffc01bf60ff8。用rd命令可以讀取這個表項的值,這個值裡面含有第二級轉換表的起始地址資訊。
讀出的值是99c00003,bit[1:0]=2b'11,表示該表項型別為Table descriptor,指向下一級(第二級)轉換表,起始地址是99c00000。結合虛擬地址的bit[29:21],可以算出虛擬地址在第二級轉換表中的表項地址:0x99c00000 + 0xba8 = 0x99c00ba8。該地址是實體地址,需要用rd -p命令讀取其值,這個值裡面含有第三級轉換表的起始地址資訊。
讀出的值是99c04003,表項型別也為Table descriptor。結合虛擬地址的bit[20:12],可以算出虛擬地址在第三級轉換表中對應的表項地址:0x99c04000 + 0x628 = 0x99c04628。三級表項存放的是頁面描述符資訊,不再指向下一級轉換表。
從0x99c04628地址讀出的值是00e800008594bf53,結合第4節的地址轉換過程圖,bit[47:12]存放的是地址資訊,與虛擬地址的bit[11:0](頁內偏移)結合後,就構成了實際的實體地址:0x8594b000 + 0xcb0 = 0x8594bcb0。這個地址就是0x0000007feeac5cb0所對應的實體地址。
上述過程我們手動計算出了實體地址,那如何知道有沒有算對呢?其實crash tool提供了vtop這個命令,可以直接顯示虛擬地址到實體地址的轉換結果,如下圖所示。可以看到vtop命令的結果和我們手動計算的結果一致,說明我們對轉換過程的理解是正確的。
再來分析核心空間的虛擬地址 ffffffc01bf60000,將它分解如下:
bit[38:30] 0x100,左移三位是 0x800
bit[29:21] 0x0df,左移三位是 0x6f8
bit[20:12] 0x160,左移三位是 0xb00
bit[11:0] 0x000
首先我們要知道核心空間的 pgd,結合前面第4節講的Linux關鍵資料結構,核心空間的 pgd 是儲存在 init_mm 變數中,用p命令列印變數結果,可以看到pgd指標的值。
每一級表項的計算過程與使用者空間例子類似,可以得到如下觀察結果:
注意,第二級表項的值是00f800008be00f11,bit[1:0]=2b'01,表示Block entry,不再指向下一級轉換表,而是指示實體地址,地址值為9be00000。
兩級轉換表對應的是虛擬地址的bit[38:30]和bit[29:21],又沒有第三級轉換表,因此剩下的bit[20:0]都是頁內偏移。所以計算出最終實體地址為:0x9be00000 + 0x160000 = 0x9bf60000。也就是說, ffffffc01bf60000的實體地址是9bf60000。
用vtop命令驗證,和手動計算的結果一致。
------ END ------
作者:bigfish99
部落格:https://www.cnblogs.com/bigfish0506/
公眾號:大魚嵌入式