ChCore-lab1

木木ちゃん發表於2024-11-09

Lab1: BOOT!!!

(參考來源:上海交通大學並行與分散式系統研究所+作業系統實驗)
Creative Commons Attribution 4.0 License

1. Prerequisite

國內大部分的映象都壞掉了,我們只能自己部署一個加速的映象。

  1. 首先fork這個專案,記得給一顆星:第三方docker加速

  2. 然後前往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"
    ]
}

即可完成操作。

  1. 每次編譯的時候,如果遇到問題,則需要執行sudo chmod -R 777。不管是make後還是進行除錯前。
  2. 在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_clearwait_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_el3spsr_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

可以看到,

  1. 首先需要將返回的函式地址載入到x9當中,其次再將返回的地址載入到elr_el3中,因為這是在降低異常級時所需要讀取和返回的函式地址,也就是_start
  2. 其次,我們還需要儲存程式狀態以遮蔽中斷,這個是透過儲存SPSR_ELX_DAIF | SPSR_ELX_EL1H來進行的,在遮蔽DAIF四類中斷訊號的情況下載入EL1H,因此採用異或的方式。
  3. 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的核心出錯:

  1. 當初始化依賴於起始地址和終止地址內的內容時,未清零的情況可能會導致不可預測的錯誤;
  2. 儲存了某些錯誤的值導致系統讀取到了錯誤的內容,引發崩潰。
  3. 不相容問題,某些未清零的值可能會導致跨平臺出錯。

5. 初始化串列埠輸出

到目前為止我們仍然只能透過 GDB 追蹤核心的執行過程,而無法看到任何輸出,這無疑是對我們寫作業系統的積極性的一種打擊。因此在 init_c 中,我們啟用樹莓派的 UART 串列埠,從而能夠輸出字元。

kernel/arch/aarch64/boot/raspi3/peripherals/uart.c 已經給出了 early_uart_initearly_uart_send 函式,分別用於初始化 UART 和傳送單個字元(也就是輸出字元)。

練習題 6:在 kernel/arch/aarch64/boot/raspi3/peripherals/uart.cLAB 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.SLAB 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,接下來我們再瞭解兩個函式。

  1. bic

  2. 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_00000x0000_ffff_ffff_ffff 為低地址,0xffff_0000_0000_00000xffff_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 地址範圍所需的實體記憶體大小(或頁表頁數量)。

我們有:

  1. 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\)的記憶體要求。
  2. 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 為核心配置從 0x000000000x800000000x40000000 後的 1G,ChCore 只需使用這部分地址中的本地外設)的對映,其中 0x000000000x3f000000 對映為 normal memory,0x3f0000000x80000000對映為 device memory,其中 0x000000000x40000000 以 2MB 塊粒度對映,0x400000000x80000000 以 1GB 塊粒度對映。

思考題 9: 請結合上述地址翻譯規則,計算在練習題 10 中,你需要對映幾個 L2 頁表條目,幾個 L1 頁表條目,幾個 L0 頁表條目。頁表頁需要佔用多少實體記憶體?

  1. 我們從0x0000_00000x3fff_ffff的對映需要用到30位的虛擬地址。這其中,頁大小是2MB,則其頁偏移量為21位。那麼我們就需要使用2級頁表進行對映。二級頁表條目則需要\(9=(0+0+0)+9\)位。這樣我們就有1個0級頁表(1個頁表項),1個1級頁表(4個頁表項),1個二級頁表(滿表)。12KB.
  2. 0x4000_00000x8000_0000對映則需要32位的虛擬地址,其中,頁大小是1GB,則採用單級頁表。頁內偏移位30位,則需要一級頁表條目\(2\)位。這樣我們就需要1個0級頁表(1個頁表項),1個一級頁表(4個頁表項)。 8KB.

練習題 10:在 init_kernel_pt 函式的 LAB 1 TODO 5 處配置核心高地址頁表(boot_ttbr1_l0boot_ttbr1_l1boot_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(0x000000000x80000000) 按照要求的頁粒度一一對映到 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 函式中為什麼還要為低地址配置頁表,並嘗試驗證自己的解釋。

我們需要回顧一下核心啟動的過程。

  1. 我們逐個初始化CPU的每個核。
  2. 我們需要獲取CPU當前異常級別並且將異常級別下降到核心級EL1。
  3. 設定啟動棧準備跳轉到C語言。
  4. 初始化串列埠輸出,這樣我們才能顯示/回顯資訊。
  5. 配置啟動頁表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!!!

😆


  1. Arm Architecture Reference Manual, D13.2.144 ↩︎

  2. Arm Architecture Reference Manual, D13.2.147 ↩︎

  3. Arm Architecture Reference Manual, D13.2.131 ↩︎

  4. Arm Architecture Reference Manual, D5.2 Figure D5-13 ↩︎

  5. 作業系統:原理與實現 ↩︎

  6. Arm Architecture Reference Manual, D5.3 ↩︎

  7. Arm Architecture Reference Manual, D5.5 ↩︎

  8. Arm Architecture Reference Manual, D5.4 ↩︎

  9. Arm Architecture Reference Manual, D13.2.97 ↩︎

  10. bcm2836-peripherals.pdf & Raspberry Pi Hardware - Peripheral Addresses ↩︎