Lab1: BOOT!!!
(參考來源:上海交通大學並行與分散式系統研究所+作業系統實驗)
Creative Commons Attribution 4.0 License
1. Prerequisite
國內大部分的映象都壞掉了,我們只能自己部署一個加速的映象。
-
首先fork這個專案,記得給一顆星:第三方docker加速
-
然後前往cloudflare官網:workers & pages > overview > Pages > Connect to Git
點選save and deploy即可。
成功後可以看到以下介面:
我們的開發是在ubuntu 22.04.2 LTS上進行,架構為aarch64. 採用snap安裝docker。直接在application中搜尋docker即可安裝。注意賦予其全部的許可權。(不是為什麼要snap啊)
接下來進行換源。需要進行如下操作:
sudo vim /var/snap/docker/current/config/daemon.json
注意:wq!強制儲存。
Tips: apt 安裝則需要進行這一步:
sudo vim /etc/docker/daemon.json
後將以下這一段加入:
{
"log-level": "error",
"storage-driver": "overlay2",
"registry-mirrors": [
"https://docker.fxxk.dedyn.io"
]
}
即可完成操作。
- 每次編譯的時候,如果遇到問題,則需要執行
sudo chmod -R 777
。不管是make後還是進行除錯前。 - 在debug配置過程中,vscode中的midebuggerpath可以透過
which gdb
獲取新的路徑進行修改。
Environment Settings:
利用命令返回到我們的實驗資料夾下。
進行
make build
make qemu
進行拉取映象和編譯工作。
接下來透過make qemu檢視是否可以執行。
OK. 我們就完成了這份基礎的環境搭建。
和先前一樣,我們在一個終端make qemu-gdb
,在另一個終端make gdb
。
2. 啟動0號核
_start
函式(位於 kernel/arch/aarch64/boot/raspi3/init/start.S
)是 ChCore 核心啟動時執行的第一塊程式碼。由於 QEMU 在模擬機器啟動時會同時開啟 4 個 CPU 核心,於是 4 個核會同時開始執行 _start
函式。而在核心的初始化過程中,我們通常需要首先讓其中一個核進入初始化流程,待進行了一些基本的初始化後,再讓其他核繼續執行。
思考題 1:閱讀
_start
函式的開頭,嘗試說明 ChCore 是如何讓其中一個核首先進入初始化流程,並讓其他核暫停執行的。
我們僅需要閱讀前三行程式碼即可。尋找到kernel/arch/aarch64/boot/raspi3/init/start.S 檔案,我們有:
mrs x8, mpidr_el1
and x8, x8, #0xFF
cbz x8, primary
mrs
讀入mpidr_el1系統級暫存器的狀態。mpidr_el1是一個64位暫存器,其結構如下:
我們可以獲得在x8中的值,這裡表示採用CPUID=0,單執行緒核。
在MPIDR_EL1中,親和力(Affinity)具有{Aff0,Aff1,Aff2,Aff3}四個等級,並且0等級的重要度最高。因此判斷是否為單核啟動,只需要取其低8位的數字,即將x8與0xFF進行AND操作後,當值為0時,進入primary
函式中,進行初始化操作。
其餘核心則依次進入wait_for_bss_clear
和wait_until_smp_enabled
被暫時掛起,等待基本初始化完成後繼續執行。
/* Wait for bss clear */
wait_for_bss_clear:
adr x0, clear_bss_flag
ldr x1, [x0]
cmp x1, #0
bne wait_for_bss_clear
3. 切換異常級
練習題 2:在
arm64_elX_to_el1
函式的LAB 1 TODO 1
處填寫一行彙編程式碼,獲取 CPU 當前異常級別。
首先我們先閱讀以下start.S 程式碼:
...
.extern arm64_elX_to_el1
...
BEGIN_FUNC(_start)
mrs x8, mpidr_el1
and x8, x8, #0xFF
cbz x8, primary
...
primary:
/* Turn to el1 from other exception levels. */
bl arm64_elX_to_el1
/* Prepare stack pointer and jump to C. */
adr x0, boot_cpu_stack
add x0, x0, #INIT_STACK_SIZE
mov sp, x0
b init_c
...
AArch64 架構中,特權級被稱為異常級別(Exception Level,EL),四個異常級別分別為 EL0、EL1、EL2、EL3,其中 EL3 為最高異常級別,常用於安全監控器(Secure Monitor),EL2 其次,常用於虛擬機器監控器(Hypervisor),EL1 是核心常用的異常級別,也就是通常所說的核心態,EL0 是最低異常級別,也就是通常所說的使用者態。
為了使 arm64_elX_to_el1
函式具有通用性,我們沒有直接寫死從 EL3 降至 EL1 的邏輯,而是首先判斷當前所在的異常級別,並根據當前異常級別的不同,跳轉到相應的程式碼執行。
因此我們需要從系統暫存器中獲取當前的狀態級別。我們只需要執行:
mrs x9, CURRENTEL
即可。
可以看到我們的異常級分別透過EL0->0 EL1->4 EL2->8 EL3->12表示。我們目前位於異常級3.
有關currentEL:僅有兩個bit用於標識異常級,也就是EL。分別為0,1,2,3.
(為什麼64個bit只用了兩個。。。)
練習題 3:在
arm64_elX_to_el1
函式的LAB 1 TODO 2
處填寫大約 4 行彙編程式碼,設定從 EL3 跳轉到 EL1 所需的elr_el3
和spsr_el3
暫存器值。
eret
指令可用於從高異常級別跳到更低的異常級別,在執行它之前我們需要設定
設定 elr_elx
(異常連結暫存器)和 spsr_elx
(儲存的程式狀態暫存器),分別控制eret
執行後的指令地址(PC)和程式狀態(包括異常返回後的異常級別)。
elr_el3
的正確設定應使得控制流在 eret
後從 arm64_elX_to_el1
返回到 _start
繼續執行初始化。 spsr_el3
的正確設定應正確遮蔽 DAIF 四類中斷,並且將 SP 正確設定為 EL1h
. 在設定好這兩個系統暫存器後,不需要立即 eret
.
我們需要繼續向下閱讀原始碼。我們有.Lin_el2
函式用於在EL2異常級進行某些程序操作,可以忽略。在.Lno_gic_sr
和.Ltarget
中,我們有
.Lno_gic_sr:
// Set EL1 to 64bit.
mov x9, HCR_EL2_RW
msr hcr_el2, x9
// Set the return address and exception level.
adr x9, .Ltarget
msr elr_el2, x9
mov x9, SPSR_ELX_DAIF | SPSR_ELX_EL1H
msr spsr_el2, x9
isb
eret
.Ltarget:
ret
可以看到,
- 首先需要將返回的函式地址載入到x9當中,其次再將返回的地址載入到
elr_el3
中,因為這是在降低異常級時所需要讀取和返回的函式地址,也就是_start
。 - 其次,我們還需要儲存程式狀態以遮蔽中斷,這個是透過儲存
SPSR_ELX_DAIF | SPSR_ELX_EL1H
來進行的,在遮蔽DAIF四類中斷訊號的情況下載入EL1H,因此採用異或的方式。 SPSR_ELX_DAIF
中儲存了DAIF的bit資訊,再透過異或的方式就可以“過濾”掉SPSR_ELX_EL1H
中存在的中斷訊號資訊。
新增程式碼:
// Set the return address and exception level.
/* LAB 1 TODO 2 BEGIN */
/* BLANK BEGIN */
adr x9, .Ltarget
msr elr_el3, x9
mov x9, SPSR_ELX_DAIF | SPSR_ELX_EL1H
msr spsr_el3, x9
/* BLANK END */
/* LAB 1 TODO 2 END */
新增程式碼並且rebuild以後,我們成功返回了_start
。
4. 跳轉到C語言程式碼
降低異常級別到 EL1 後,我們準備從彙編跳轉到 C 程式碼,在此之前我們先設定棧(SP)。因此,_start
函式在執行 arm64_elX_to_el1
後,即設定核心啟動階段的棧,並跳轉到第一個 C 函式 init_c
。
思考題 4:說明為什麼要在進入 C 函式之前設定啟動棧。如果不設定,會發生什麼?
我們有
/* Prepare stack pointer and jump to C. */
adr x0, boot_cpu_stack
add x0, x0, #INIT_STACK_SIZE
mov sp, x0
其中,INIT_STACK_SIZE位於consts.h
的定義中。#define INIT_STACK_SIZE 0x1000
分配的棧大小為1000.
如果不設定啟動棧,在發生異常或者需要傳遞引數的情況下,機器將無法獲得引數/從錯誤中恢復,造成無法儲存上下文資訊和傳遞引數的問題。
進入 init_c
函式後,第一件事首先透過 clear_bss
函式清零了 .bss
段,該段用於儲存未初始化的全域性變數和靜態變數(具體請參考附錄)。
思考題 5:在實驗 1 中,其實不呼叫
clear_bss
也不影響核心的執行,請思考不清理.bss
段在之後的何種情況下會導致核心無法工作。
我們需要觀察clear_bss
函式。其位於init_c.c中。我們有:
static void clear_bss(void)
{
u64 bss_start_addr;
u64 bss_end_addr;
u64 i;
bss_start_addr = (u64)&_bss_start;
bss_end_addr = (u64)&_bss_end;
for (i = bss_start_addr; i < bss_end_addr; ++i)
*(char *)i = 0;
clear_bss_flag = 0;
}
在這其中,我們初始化了起始地址和終止地址,並將其指向的記憶體內容都更改為0.因此以下情況可能會導致未經過clear_bss_flag的核心出錯:
- 當初始化依賴於起始地址和終止地址內的內容時,未清零的情況可能會導致不可預測的錯誤;
- 儲存了某些錯誤的值導致系統讀取到了錯誤的內容,引發崩潰。
- 不相容問題,某些未清零的值可能會導致跨平臺出錯。
5. 初始化串列埠輸出
到目前為止我們仍然只能透過 GDB 追蹤核心的執行過程,而無法看到任何輸出,這無疑是對我們寫作業系統的積極性的一種打擊。因此在 init_c
中,我們啟用樹莓派的 UART 串列埠,從而能夠輸出字元。
在 kernel/arch/aarch64/boot/raspi3/peripherals/uart.c
已經給出了 early_uart_init
和 early_uart_send
函式,分別用於初始化 UART 和傳送單個字元(也就是輸出字元)。
練習題 6:在
kernel/arch/aarch64/boot/raspi3/peripherals/uart.c
中LAB 1 TODO 3
處實現透過 UART 輸出字串的邏輯。
我們只需要先初始化後再一個個傳入字元即可。
void uart_send_string(char *str)
{
/* LAB 1 TODO 3 BEGIN */
/* BLANK BEGIN */
early_uart_init();
unsigned int index=0;
while(str[index]!='\0')
{
early_uart_send(str[index++]);
}
/* BLANK END */
/* LAB 1 TODO 3 END */
}
我們可以看到
成功輸出了字串。
6. 啟用 MMU
在核心的啟動階段,還需要配置啟動頁表(init_kernel_pt
函式),並啟用 MMU(el1_mmu_activate
函式),使可以透過虛擬地址訪問記憶體,從而為之後跳轉到高地址作準備(核心通常執行在虛擬地址空間 0xffffff0000000000
之後的高地址)。
關於配置啟動頁表的內容由於包含關於頁表的細節,將在本實驗下一部分實現,目前直接啟用 MMU。
在 EL1 異常級別啟用 MMU 是透過配置系統暫存器 sctlr_el1
實現的(Arm Architecture Reference Manual D13.2.118)。具體需要配置的欄位主要包括:
- 是否啟用 MMU(
M
欄位) - 是否啟用對齊檢查(
A
SA0
SA
nAA
欄位) - 是否啟用指令和資料快取(
C
I
欄位)
練習題 7:在
kernel/arch/aarch64/boot/raspi3/init/tools.S
中LAB 1 TODO 4
處填寫一行彙編程式碼,以啟用 MMU。
這裡我們從上面的三條規則可以看到需要檢查是否啟用欄位。我們再根據原始碼的閱讀可以發現:
mrs x8, sctlr_el1
/* Enable MMU */
/* LAB 1 TODO 4 BEGIN */
/* BLANK BEGIN */
/* BLANK END */
/* LAB 1 TODO 4 END */
/* Disable alignment checking */
bic x8, x8, #SCTLR_EL1_A
bic x8, x8, #SCTLR_EL1_SA0
bic x8, x8, #SCTLR_EL1_SA
orr x8, x8, #SCTLR_EL1_nAA
/* Data accesses Cacheable */
orr x8, x8, #SCTLR_EL1_C
/* Instruction access Cacheable */
orr x8, x8, #SCTLR_EL1_I
msr sctlr_el1, x8
進行需要的操作時bic或orr,接下來我們再瞭解兩個函式。
-
bic
-
orr
可以看到bic用於清零,也就是禁用某些欄位。而orr用於啟用欄位。因此我們需要啟用mmu,則需要
orr x8, x8, #SCTLR_EL1_M
由於沒有配置啟動頁表,在啟用 MMU 後,核心會立即發生地址翻譯錯誤(Translation Fault),進而嘗試跳轉到異常處理函式(Exception Handler),
該異常處理函式的地址為異常向量表基地址(vbar_el1
暫存器)加上 0x200
。
此時我們沒有設定異常向量表(vbar_el1
暫存器的值是0),因此執行流會來到 0x200
地址,此處的程式碼為非法指令,會再次觸發異常並跳轉到 0x200
地址。
使用 GDB 除錯,在 GDB 中輸入 continue
後,待核心輸出停止後,按 Ctrl-C,可以觀察到核心在 0x200
處無限迴圈。
如圖所示。
7. 核心啟動頁表
AArch64地址翻譯
在 AArch64 架構的 EL1 異常級別存在兩個頁表基址暫存器:ttbr0_el1
[1] 和 ttbr1_el1
[2],分別用作虛擬地址空間低地址和高地址的翻譯。那麼什麼地址範圍稱為“低地址”,什麼地址範圍稱為“高地址”呢?這由 tcr_el1
翻譯控制暫存器[3]控制,該暫存器提供了豐富的可配置性,可決定 64 位虛擬地址的高多少位為 0
時,使用 ttbr0_el1
指向的頁表進行翻譯,高多少位為 1
時,使用 ttbr1_el1
指向的頁表進行翻譯[4]。一般情況下,我們會將 tcr_el1
配置為高低地址各有 48 位的地址範圍,即,0x0000_0000_0000_0000
~0x0000_ffff_ffff_ffff
為低地址,0xffff_0000_0000_0000
~0xffff_ffff_ffff_ffff
為高地址。
瞭解瞭如何決定使用 ttbr0_el1
還是 ttbr1_el1
指向的頁表,再來看地址翻譯過程如何進行。通常我們會將系統配置為使用 4KB 翻譯粒度、4 級頁表(L0 到 L3),同時在 L1 和 L2 頁表中分別允許對映 2MB 和 1GB 大頁(或稱為塊)[5],因此地址翻譯的過程如下圖所示:
其中,當對映為 1GB 塊或 2MB 塊時,圖中 L2、L3 索引或 L3 索引的位置和低 12 位共同組成塊內偏移。
每一級的每一個頁表佔用一個 4KB 物理頁,稱為頁表頁(Page Table Page),其中有 512 個條目,每個條目佔 64 位。AArch64 中,頁表條目稱為描述符(descriptor)[6],最低位(bit[0])為 1
時,描述符有效,否則無效。有效描述符有兩種型別,一種指向下一級頁表(稱為表描述符),另一種指向物理塊(大頁)或物理頁(稱為塊描述符或頁描述符)。在上面所說的地址翻譯配置下,描述符結構如下(“Output address”在這裡即實體地址,一些地方稱為物理頁幀號(Page Frame Number,PFN)):
L0、L1、L2 頁表描述符
L3 頁表描述符
思考題 8:請思考多級頁表相比單級頁錶帶來的優勢和劣勢(如果有的話),並計算在 AArch64 頁表中分別以 4KB 粒度和 2MB 粒度對映 0~4GB 地址範圍所需的實體記憶體大小(或頁表頁數量)。
我們有:
- 4KB粒度
(1) 0~4GB虛擬地址範圍需要\(2^2\cdot2^{30}B\),也就是32位。
(2) 頁表大小為4KB,則具有頁內偏移:\(2^2\cdot2^{10}B\),也就是12位。
(3) 則需要用20位的物理頁號。根據aarch64系統結構,每級列表最多有\(2^9=512\)個頁表項。這樣就需要\(2^{2+9+9}\)的三級頁表結構。
(4) 這樣我們就有:1個0級頁表(1個頁表項),1個1級頁表(4個頁表項),\(4\)個2級頁表(滿表),\(4\cdot 2^9\)個3級頁表(滿表)。(\(\mathbf{0+0+2+9}+9\))這樣我們就有\(4KB+4KB+16KB+8MB\)的記憶體要求。 - 2MB粒度
(1) 0~4GB虛擬地址範圍需要\(2^2\cdot2^{30}B\),也就是32位。
(2) 頁表大小為\(2MB\),也就是\(2\cdot 2^{20}B\),也就是21位。
(3) 這樣就需要11位的物理頁號。這樣就需要\(2^{2+9}\)的二級頁表。
(4) 這樣的頁表空間則為:1個0級頁表(1個頁表項),1個1級頁表(1個頁表項),4個二級頁表(滿表),總共24KB的記憶體要求。
頁表描述符中除了包含下一級頁表或物理頁/塊的地址,還包含對記憶體訪問進行控制的屬性(attribute)。這裡涉及到太多細節,本文件限於篇幅只介紹最常用的幾個頁/塊描述符中的屬性欄位:
欄位 | 位 | 描述 |
---|---|---|
UXN | bit[54] | 置為 1 表示非特權態無法執行(Unprivileged eXecute-Never) |
PXN | bit[53] | 置為 1 表示特權態無法執行(Privileged eXecute-Never) |
nG | bit[11] | 置為 1 表示該描述符在 TLB 中的快取只對當前 ASID 有效 |
AF | bit[10] | 置為 1 表示該頁/塊在上一次 AF 置 0 後被訪問過 |
SH | bits[9:8] | 表示可共享屬性[7] |
AP | bits[7:6] | 表示讀寫等資料訪問許可權[8] |
AttrIndx | bits[4:2] | 表示記憶體屬性索引,間接指向 mair_el1 暫存器中配置的屬性[9],用於控制將物理頁對映為正常記憶體(normal memory)或裝置記憶體(device memory),以及控制 cache 策略等 |
配置核心啟動頁表
有了關於頁表配置的前置知識,我們終於可以開始配置核心的啟動頁表了。
作業系統核心通常執行在虛擬記憶體的高地址(如前所述,0xffff_0000_0000_0000
之後的虛擬地址)。透過對核心頁表的配置,將虛擬記憶體高地址對映到核心實際所在的實體記憶體,在執行核心程式碼時,PC 暫存器的值是高地址,對全域性變數、棧等的訪問都使用高地址。在核心執行時,除了需要訪問核心程式碼和資料等,往往還需要能夠對任意實體記憶體和外設記憶體(MMIO)進行讀寫,這種讀寫同樣透過高地址進行。
因此,在核心啟動時,首先需要對核心自身、其餘可用實體記憶體和外設記憶體進行虛擬地址對映,最簡單的對映方式是一對一的對映,即將虛擬地址 0xffff_0000_0000_0000 + addr
對映到 addr
。需要注意的是,在 ChCore 實驗中我們使用了 0xffff_ff00_0000_0000
作為核心虛擬地址的開始(注意開頭 f
數量的區別),不過這不影響我們對知識點的理解。
在樹莓派 3B+ 機器上,實體地址空間分佈如下[10]:
實體地址範圍 | 對應裝置 |
---|---|
0x00000000 ~0x3f000000 |
實體記憶體(SDRAM) |
0x3f000000 ~0x40000000 |
共享外設記憶體 |
0x40000000 ~0xffffffff |
本地(每個 CPU 核獨立)外設記憶體 |
現在將目光轉移到 kernel/arch/aarch64/boot/raspi3/init/mmu.c
檔案,我們需要在 init_kernel_pt
為核心配置從 0x00000000
到 0x80000000
(0x40000000
後的 1G,ChCore 只需使用這部分地址中的本地外設)的對映,其中 0x00000000
到 0x3f000000
對映為 normal memory,0x3f000000
到 0x80000000
對映為 device memory,其中 0x00000000
到 0x40000000
以 2MB 塊粒度對映,0x40000000
到 0x80000000
以 1GB 塊粒度對映。
思考題 9: 請結合上述地址翻譯規則,計算在練習題 10 中,你需要對映幾個 L2 頁表條目,幾個 L1 頁表條目,幾個 L0 頁表條目。頁表頁需要佔用多少實體記憶體?
- 我們從
0x0000_0000
到0x3fff_ffff
的對映需要用到30位的虛擬地址。這其中,頁大小是2MB,則其頁偏移量為21位。那麼我們就需要使用2級頁表進行對映。二級頁表條目則需要\(9=(0+0+0)+9\)位。這樣我們就有1個0級頁表(1個頁表項),1個1級頁表(4個頁表項),1個二級頁表(滿表)。12KB. - 從
0x4000_0000
到0x8000_0000
對映則需要32位的虛擬地址,其中,頁大小是1GB,則採用單級頁表。頁內偏移位30位,則需要一級頁表條目\(2\)位。這樣我們就需要1個0級頁表(1個頁表項),1個一級頁表(4個頁表項)。 8KB.
練習題 10:在
init_kernel_pt
函式的LAB 1 TODO 5
處配置核心高地址頁表(boot_ttbr1_l0
、boot_ttbr1_l1
和boot_ttbr1_l2
),以 2MB 粒度對映。
我們可以首先看一下MMU.c中的部分:
void init_kernel_pt(void)
{
u64 vaddr = PHYSMEM_START;
/* TTBR0_EL1 0-1G */
boot_ttbr0_l0[GET_L0_INDEX(vaddr)] = ((u64)boot_ttbr0_l1) | IS_TABLE
| IS_VALID | NG;
boot_ttbr0_l1[GET_L1_INDEX(vaddr)] = ((u64)boot_ttbr0_l2) | IS_TABLE
| IS_VALID | NG;
/* Normal memory: PHYSMEM_START ~ PERIPHERAL_BASE */
/* Map with 2M granularity */
for (; vaddr < PERIPHERAL_BASE; vaddr += SIZE_2M) {
boot_ttbr0_l2[GET_L2_INDEX(vaddr)] =
(vaddr) /* low mem, va = pa */
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| NG /* Mark as not global */
| INNER_SHARABLE /* Sharebility */
| NORMAL_MEMORY /* Normal memory */
| IS_VALID;
}
/* Peripheral memory: PERIPHERAL_BASE ~ PHYSMEM_END */
/* Map with 2M granularity */
for (vaddr = PERIPHERAL_BASE; vaddr < PHYSMEM_END; vaddr += SIZE_2M) {
boot_ttbr0_l2[GET_L2_INDEX(vaddr)] =
(vaddr) /* low mem, va = pa */
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| NG /* Mark as not global */
| DEVICE_MEMORY /* Device memory */
| IS_VALID;
}
/* TTBR1_EL1 0-1G */
/* LAB 1 TODO 5 BEGIN */
/* Step 1: set L0 and L1 page table entry */
/* BLANK BEGIN */
/* BLANK END */
/* Step 2: map PHYSMEM_START ~ PERIPHERAL_BASE with 2MB granularity */
/* BLANK BEGIN */
/* BLANK END */
/* Step 2: map PERIPHERAL_BASE ~ PHYSMEM_END with 2MB granularity */
/* BLANK BEGIN */
/* BLANK END */
/* LAB 1 TODO 5 END */
/*
* Local peripherals, e.g., ARM timer, IRQs, and mailboxes
*
* 0x4000_0000 .. 0xFFFF_FFFF
* 1G is enough (for Mini-UART). Map 1G page here.
*/
vaddr = KERNEL_VADDR + PHYSMEM_END;
boot_ttbr1_l1[GET_L1_INDEX(vaddr)] = PHYSMEM_END | UXN /* Unprivileged
execute never
*/
| ACCESSED /* Set access flag */
| NG /* Mark as not global */
| DEVICE_MEMORY /* Device memory */
| IS_VALID;
}
提示:你只需要將
addr
(0x00000000
到0x80000000
) 按照要求的頁粒度一一對映到KERNEL_VADDR + addr
(vaddr
) 上。vaddr
對應的實體地址是vaddr - KERNEL_VADDR
. Attributes 的設定請參考給出的低地址頁表配置。
從提示中我們可以看到,我們只需要將下標中的vaddr對映到高地址,即vaddr=vaddr+KERNEL_VADDR
中,然後將真實的實體地址,即paddr=vaddr-KERNEL_VADDR
賦給對映頁表即可。注意高地址需要的暫存器位ttbr1.
這樣我們就有:
/* TTBR1_EL1 0-1G */
/* LAB 1 TODO 5 BEGIN */
/* Step 1: set L0 and L1 page table entry */
/* BLANK BEGIN */
vaddr = PHYSMEM_START + KERNEL_VADDR; // the started entry of the vaddr.
boot_ttbr1_l0[GET_L0_INDEX(vaddr)] = ((u64)boot_ttbr1_l1) | IS_TABLE
| IS_VALID | NG;
boot_ttbr1_l1[GET_L1_INDEX(vaddr)] = ((u64)boot_ttbr1_l2) | IS_TABLE
| IS_VALID | NG;
/* BLANK END */
// 上面的部分我們將高地址負責的ttbr1的l0級頁表和l1級頁筆哦對映到高地址。
/* Step 2: map PHYSMEM_START ~ PERIPHERAL_BASE with 2MB granularity */
/* BLANK BEGIN */
for (; vaddr < PERIPHERAL_BASE + KERNEL_VADDR; vaddr += SIZE_2M) {
boot_ttbr1_l2[GET_L2_INDEX(vaddr)] =
(vaddr - KERNEL_VADDR) /* high mem, va - Kva = pa */
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| NG /* Mark as not global */
| INNER_SHARABLE /* Sharebility */
| NORMAL_MEMORY /* Normal memory */
| IS_VALID;
}
/* BLANK END */
// 修改Normal Memory對映部分,注意是高地址對映,需要修改起始虛擬地址和終止的虛擬地址,以及對映到的真實實體地址。
/* Step 2: map PERIPHERAL_BASE ~ PHYSMEM_END with 2MB granularity */
/* BLANK BEGIN */
for (vaddr = PERIPHERAL_BASE + KERNEL_VADDR; vaddr < PHYSMEM_END + KERNEL_VADDR; vaddr += SIZE_2M) {
boot_ttbr1_l2[GET_L2_INDEX(vaddr)] =
(vaddr - KERNEL_VADDR) /* lhigh mem, va - Kva = pa */
| UXN /* Unprivileged execute never */
| ACCESSED /* Set access flag */
| NG /* Mark as not global */
| DEVICE_MEMORY /* Device memory */
| IS_VALID;
}
// 修改Device Memory對映部分,注意是高地址對映,需要修改起始虛擬地址和終止的虛擬地址,以及對映到的真實實體地址。
/* BLANK END */
/* LAB 1 TODO 5 END */
配置完成,make build
之後進行make qemu
我們就可以得到以下介面:
成功啟動。
思考題 11:請思考在
init_kernel_pt
函式中為什麼還要為低地址配置頁表,並嘗試驗證自己的解釋。
我們需要回顧一下核心啟動的過程。
- 我們逐個初始化CPU的每個核。
- 我們需要獲取CPU當前異常級別並且將異常級別下降到核心級EL1。
- 設定啟動棧準備跳轉到C語言。
- 初始化串列埠輸出,這樣我們才能顯示/回顯資訊。
- 配置啟動頁表
init_kernel_pt
並啟動MMUel1_mmu_active
.
我們看到我們在配置啟動頁表後就需要進入到啟用MMU階段。這樣我們就需要觀察一下,當缺少了低地址配置後會出現什麼情況。
可以發現,我們在0x8815c和0x88160處需要將ttbr0_el1進行配置,也就是低地址頁表基址暫存器。此時啟用的mmu在進行頁表配置時會出現segmentation fault,即定址錯誤。這是因為,目前執行的函式執行在低地址空間,在返回時,需要獲得返回的虛擬地址的翻譯。未配置低地址空間會導致對el1_mmu_active
函式儲存的返回出現錯誤。
錯誤的情況
完成 init_kernel_pt
函式後,ChCore 核心便可以在 el1_mmu_activate
中將 boot_ttbr1_l0
等實體地址寫入實際暫存器(如 ttbr1_el1
),隨後啟用 MMU 後繼續執行,並透過 start_kernel
跳轉到高地址,進而跳轉到核心的 main
函式(位於 kernel/arch/aarch64/main.c
, 尚未釋出,以 binary 提供)。
思考題 12:在一開始我們暫停了三個其他核心的執行,根據現有程式碼簡要說明它們什麼時候會恢復執行。思考為什麼一開始只讓 0 號核心執行初始化流程?
提示:
secondary_boot_flag
將在 main 函式執行完時鐘,排程器,鎖的初始化後被設定。
為了保證多個核的執行,在作業系統執行過程中需要載入好時鐘,排程器(排程策略),以及多執行緒鎖的相關程式。這些程式都位於低地址中,而核心需要在高地址以核心態el1執行。因此我們需要先初始化0號核(其中一個核心),建立好其他策略程式所在的實體記憶體到高地址記憶體的對映,在確認相關策略和程式載入完畢後,才能逐個啟動新的核心,防止作業系統的核心混亂。
make grade!!!
😆
Arm Architecture Reference Manual, D13.2.144 ↩︎
Arm Architecture Reference Manual, D13.2.147 ↩︎
Arm Architecture Reference Manual, D13.2.131 ↩︎
Arm Architecture Reference Manual, D5.2 Figure D5-13 ↩︎
作業系統:原理與實現 ↩︎
Arm Architecture Reference Manual, D5.3 ↩︎
Arm Architecture Reference Manual, D5.5 ↩︎
Arm Architecture Reference Manual, D5.4 ↩︎
Arm Architecture Reference Manual, D13.2.97 ↩︎
bcm2836-peripherals.pdf & Raspberry Pi Hardware - Peripheral Addresses ↩︎