xv6啟動流程
xv6核心地址空間
1 Riscv開機
riscv在啟動時,pc被預設設定為0X1000
,之後經過以下幾條指令,跳轉到0x80000000
-
在第一個shell,開啟xv6 gdb模式
make qemu-gdb
-
開啟第二個shell,進行除錯
riscv64-unknown-elf-gdb
-
可以看到啟動時,qemu就在
0X1000
地址The target architecture is set to "riscv:rv64". warning: No executable has been specified and target does not support determining executable automatically. Try using the "file" command. 0x0000000000001000 in ?? ()
-
使用
layout asm
檢視彙編原始碼0x1000 auipc t0,0x0 0x1004 addi a2,t0,40 0x1008 csrr a0,mhartid 0x100c ld a1,32(t0) 0x1010 ld t0,24(t0) 0x1014 jr t0
-
執行到
0x1014
時,使用info reg
,可以看到t0此時的值等於0x80000000
-
從而跳轉到
0x80000000
2. xv6編譯
同時,xv6在編譯時,會把載入程式放在0x80000000
位置,從而成功進入系統
-
檢視xv6中的
kernel/kernel.ld
,可以看到. = 0x80000000;
,這一行會將初始程式放置到0x80000000
地址 -
使用
make qemu
編譯xv6 -
使用
riscv64-unknown-elf-objdump -d kernel/kernel
反編譯核心檔案,可以看到kernel: file format elf64-littleriscv Disassembly of section .text: 0000000080000000 <_entry>: 80000000: 0001a117 auipc sp,0x1a 80000004: c7010113 add sp,sp,-912 # 80019c70 <stack0> 80000008: 6505 lui a0,0x1 8000000a: f14025f3 csrr a1,mhartid 8000000e: 0585 add a1,a1,1 80000010: 02b50533 mul a0,a0,a1 80000014: 912a add sp,sp,a0 80000016: 6d0050ef jal 800056e6 <start>
-
_entry
函式被放置在了0X80000000
位置
2. kernel/entry.S
_entry:
# 設定核心棧指標
la sp, stack0 # sp<-stack0
li a0, 1024*4 # a0<-4096
csrr a1, mhartid # a1<-mhartid
addi a1, a1, 1 # a1<-a1+1
mul a0, a0, a1 # a0<-a0*a1
add sp, sp, a0 # sp<-sp+a0
call start # 最後跳轉到kernel/start.c/start函式
-
la sp, stack0
stack0定義在
kernel/start.cz
中,xv6最多支援8個cpu,每個cpu的棧大小為4KB// maximum number of CPUs #define NCPU 8 // entry.S needs one stack per CPU. __attribute__ ((aligned (16))) char stack0[4096 * NCPU];
-
csrr a1, mhartid
:-
csrr:CSR read
CSR:Control and Status Register,即控制和狀態暫存器
CSRR就是讀取控制和狀態暫存器
-
mhartid[1]:Machine Hardware Thread ID,即硬體執行緒ID暫存器
-
所以這裡就是獲取硬體執行緒ID,並儲存到a1中
-
hart(hardware thread,硬體執行緒)
:在軟體層面看來,硬體執行緒就是一個獨立的處理器。但實際上,它可能並不是一個完整的核心。因為CPU有超執行緒技術,超執行緒將一個處理器單元複用給多個硬體執行緒,每個硬體執行緒有自己獨立的一套通用暫存器等上下文資源
-
2.1 功能解析
_entry
的作用是設定棧指標
-
sp = stack0[0]的地址
-
a0 = 4096
-
a1 = mhartid,mhartid從0開始
-
a1 = a1 + 1 = mhartid + 1
-
a0 = a0 * a1 = 4096 * (mhartid + 1)
-
sp = sp + a0 = stack0[0] + a0 = stack0[0] + 4096 * (mhartid + 1)
當mhartid = 0時,sp = stack0[0] + 4096
,這是因為棧是從高地址往低地址增長的,所以第一個棧的起始地址是stack0[4096]
,往stack0[0]
增長
當mhartid = 1時,sp = stack0[0] + 4096 * 2
,第二個棧的起始地址是stack0[8192]
,往stack0[4096]
增長
其他的以此類推,就完成了棧指標的設定
3. kernel\start.c\start()
void start() {
// 將mstatus從機器模式設定為特權者模式
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
// 將main函式地址寫入mepc
w_mepc((uint64)main);
// 將satp設定為0,關閉頁表,即關閉虛擬地址轉換功能
w_satp(0);
// 把所有中斷和異常委託給S-mode
w_medeleg(0xffff);
w_mideleg(0xffff);
// 開啟中斷
w_sie(r_sie() | SIE_SEIE | SIE_STIE | SIE_SSIE);
// configure Physical Memory Protection to give
// supervisor mode access to all of physical memory.
w_pmpaddr0(0x3fffffffffffffull);
w_pmpcfg0(0xf);
// ask for clock interrupts.
timerinit();
// 將hart_id儲存到tp暫存器中
int id = r_mhartid();
w_tp(id);
// 切換到監督者模式,並跳轉到main函式
asm volatile("mret");
}
- 當前計算機處於
M-Mode
,即機器模式
3.1 mstatus
mstatus(Machine Status)
:是一個控制和狀態暫存器CSR,儲存處理器當前狀態。包含以下位欄位
- MPP (Machine Previous Privilege): 指示在發生異常或中斷之前的特權級別,當使用
mret
返回時,處理器會切換回原來的級別,在第11 12位00
: User Mode01
: Supervisor Mode11
: Machine Mode
- MPIE (Machine Previous Interrupt Enable): 記錄在發生異常或中斷之前,中斷是否被啟用。
- MIE (Machine Interrupt Enable): 允許或禁止中斷。當為1時,中斷被啟用。
- MIE (Machine Interrupt Enable): 記錄中斷是否被啟用。
unsigned long x = r_mstatus();
x &= ~MSTATUS_MPP_MASK;
x |= MSTATUS_MPP_S;
w_mstatus(x);
-
unsigned long x = r_mstatus()
:讀出mstatus的值 -
x &= ~MSTATUS_MPP_MASK
:清除MPP的狀態比如x = 1010 1011,其中第2,3位表示MPP狀態,那麼
MSTATUS_MPP_MASK = 0110 0000
,~MSTATUS_MPP_MASK=1001 1111
x &= ~MSTATUS_MPP_MASK
=>1010 1011&1001 1111=1000 1011
-
x |= MSTATUS_MPP_S
:設定MPP為監督者模式
這裡我們將mstatus
設定為監督者模式,之後跳回時,會切換到監督者模式
3.2 mepc
Machine Exception Program Counter
:儲存從核心返回使用者態時的地址,指令執行操作如下
- 發生異常(中斷、故障等)。
- 處理器將當前的PC值(即異常發生前正在執行的指令地址)寫入
mepc
暫存器。 - 進入核心處理異常
- 異常處理完畢後,返回到
mepc
儲存的地址,繼續執行。
w_mepc((uint64)main);
但是我們這裡並不是從使用者態返回,而是利用這一特性,在核心返回時,跳轉到main函式
3.3 satp
Supervisor Address Translation and Protection Register
:監督地址轉換和保護暫存器,用於控制和配置頁表的地址。它定義了當前頁表的位置和格式,以及一些與地址翻譯和保護相關的資訊,這裡就是關閉頁表功能
3.4 medeleg & mideleg
medeleg:Machine Exception Delegation Register
:用於指定被委託給更低特權級別的異常,當異常發生時,如果該異常被委託,處理器會將該異常轉交給更低特權級別的異常處理程式進行處理。 如果某個位被設定為 1,表示被委託。
mideleg:Machine Interrupt Delegation Register
:和委託異常類似,用於委託中斷
3.5 sie
sie:Supervisor Interrupt Enable
:用於控制哪些中斷可以在 Supervisor 模式下被啟用或禁用。
-
SEIE (Supervisor External Interrupt Enable):
外部中斷。當
SEIE
位被設定為 1 時,允許外部中斷在 Supervisor 模式下觸發。 -
STIE (Supervisor Timer Interrupt Enable):
定時器中斷。當
STIE
位被設定為 1 時,允許定時器中斷在 Supervisor 模式下觸發。 -
SSIE (Supervisor Soft Interrupt Enable):
軟中斷。當
SSIE
位被設定為 1 時,允許軟中斷在 Supervisor 模式下觸發。
3.5 pmpaddr0 & pmpcfg0
和PMP有關,比較複雜
https://zhuanlan.zhihu.com/p/139695407
https://www.rvmcu.com/site/nuclei_n_isa/#8-n
3.6 timerinit
解析在下方
3.7 mhartid & tp
mhartid:之前講過,就是硬體執行緒編號
tp:Thread Pointer,用於儲存執行緒本地資料的指標,沒有規定存什麼,這裡用來存執行緒編號
3.8 mret
只能在機器模式下執行,用於返回到呼叫之前的上下文環境,指令執行的操作如下:
- 從
mepc
暫存器中取出先前儲存的返回地址,載入到pc
中。 - 切換回
mstatus
中儲存的之前的機器特權狀態。
這個指令在異常處理或中斷服務結束時使用,將處理器從機器模式切換回先前的模式,例如使用者模式。這裡我們是從機器模式切到監督者模式
4.kernel\start.ct\timerinit()
設定時鐘中斷
void timerinit()
{
// 獲取CPU ID
int id = r_mhartid();
// ask the CLINT for a timer interrupt.
int interval = 1000000; // cycles; about 1/10th second in qemu.
*(uint64*)CLINT_MTIMECMP(id) = *(uint64*)CLINT_MTIME + interval;
// prepare information in scratch[] for timervec.
// scratch[0..2] : space for timervec to save registers.
// scratch[3] : address of CLINT MTIMECMP register.
// scratch[4] : desired interval (in cycles) between timer interrupts.
uint64 *scratch = &timer_scratch[id][0];
scratch[3] = CLINT_MTIMECMP(id);
scratch[4] = interval;
w_mscratch((uint64)scratch);
// set the machine-mode trap handler.
w_mtvec((uint64)timervec);
// enable machine-mode interrupts.
w_mstatus(r_mstatus() | MSTATUS_MIE);
// enable machine-mode timer interrupts.
w_mie(r_mie() | MIE_MTIE);
}
5. Kernel\main.c\main()
void main() {
if(cpuid() == 0){
// 對各個功能進行初始化
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
kinit(); // physical page allocator
kvminit(); // create kernel page table
kvminithart(); // turn on paging
procinit(); // process table
trapinit(); // trap vectors
trapinithart(); // install kernel trap vector
plicinit(); // set up interrupt controller
plicinithart(); // ask PLIC for device interrupts
binit(); // buffer cache
iinit(); // inode table
fileinit(); // file table
virtio_disk_init(); // emulated hard disk
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
while(started == 0)
;
__sync_synchronize();
printf("hart %d starting\n", cpuid());
kvminithart(); // turn on paging
trapinithart(); // install kernel trap vector
plicinithart(); // ask PLIC for device interrupts
}
scheduler();
}
Reference
https://zhuanlan.zhihu.com/p/440183052 ↩︎