2.xv6啟動流程

INnoVation-V2發表於2024-04-21

xv6啟動流程

xv6核心地址空間

image-20220921022817320

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

image-20240122015643431

mstatus(Machine Status):是一個控制和狀態暫存器CSR,儲存處理器當前狀態。包含以下位欄位

  • MPP (Machine Previous Privilege): 指示在發生異常或中斷之前的特權級別,當使用mret返回時,處理器會切換回原來的級別,在第11 12位
    • 00: User Mode
    • 01: Supervisor Mode
    • 11: 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:儲存從核心返回使用者態時的地址,指令執行操作如下

  1. 發生異常(中斷、故障等)。
  2. 處理器將當前的PC值(即異常發生前正在執行的指令地址)寫入 mepc 暫存器。
  3. 進入核心處理異常
  4. 異常處理完畢後,返回到 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 模式下被啟用或禁用。

  1. SEIE (Supervisor External Interrupt Enable):

    外部中斷。當 SEIE 位被設定為 1 時,允許外部中斷在 Supervisor 模式下觸發。

  2. STIE (Supervisor Timer Interrupt Enable):

    定時器中斷。當 STIE 位被設定為 1 時,允許定時器中斷在 Supervisor 模式下觸發。

  3. 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

只能在機器模式下執行,用於返回到呼叫之前的上下文環境,指令執行的操作如下:

  1. mepc 暫存器中取出先前儲存的返回地址,載入到 pc 中。
  2. 切換回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


  1. https://zhuanlan.zhihu.com/p/440183052 ↩︎

相關文章