Lab3 Part A
MIT6.828——Lab1 PartA
MIT6.828——Lab1 PartB
Lab2記憶體管理準備知識
MIT6.828——Lab2
內核維護了三個關于用戶環境的全域量
struct Env *envs = NULL; // All environments
struct Env *curenv = NULL; // The current env
static struct Env *env_free_list; // Free environment list
分別對應所有的環境,當前運行的用戶環境和空閑的環境鏈表,
Environment State
Env結構體的定義如下:
struct Env {
struct Trapframe env_tf; // Saved registers
struct Env *env_link; // Next free Env
envid_t env_id; // Unique environment identifier
envid_t env_parent_id; // env_id of this env's parent
enum EnvType env_type; // Indicates special system environments
unsigned env_status; // Status of the environment
uint32_t env_runs; // Number of times environment has run
// Address space
pde_t *env_pgdir; // Kernel virtual address of page dir
};
各個欄位的解釋如下:
env_tf:
當用戶環境暫停運行時,重要暫存器的值(保護的現場),內核也會進行用戶態內核態切換時保存這些值,用戶環境可以在之后被恢復,
env_link:
這個指標指向env_free_list的后一個空閑的Env結構體,
env_id:
唯一地確定使用這個結構體的用戶環境,用戶環境終止后,內核也許會把這個結構體分給另外一個環境,新的環境會有新的env_id值,
env_parent_id:
創建這個用戶環境的環境(parent)的env_id,構建一顆tree,
env_type:
用于區別特別的用戶環境,大多數清空下值都是ENV_TYPE_USER.
env_status:
這個變數有以下可能的取值:
ENV_FREE: 代表這個Env結構體不活躍的,應該在鏈表env_free_list中,
ENV_RUNNABLE: 對應的用戶環境已經就緒,等待被分配處理機,
ENV_RUNNING: 對應的用戶環境正在運行,
ENV_NOT_RUNNABLE: Env結構體所代表的是一個當前狀態下活躍的用戶環境,但是并未就緒,在等待IPC(Interprocess communication),
ENV_DYING: Env對應的是一個僵尸環境(Zombie environment),一個僵尸環境在下一次陷入內核時會被釋放回收(Lab4 會使用),
env_pgdir:
存放著這個環境的頁目錄的虛擬地址,
Allocating the Environment Array

需要進一步地修改mem_init()函式,分配一個envs陣列,這個陣列保存所有的環境,并進行映射,需要新增的代碼如下:
struct Env* envs = (struct Env*)boot_alloc(NENV * sizeof(struct Env));
memset(envs,0,NENV * sizeof(struct Env));
//... ...
boot_map_region(kern_pgdir,UENVS,PTSIZE,PADDR(envs),PTE_U);
Creating and Running Environments
現在需要完成如何讓用戶環境跑起來的代碼了,因為還沒有檔案系統,因此只能加載嵌入內核自身的靜態二進制映像,Lab3的makefile會生成幾個二進制檔案放在obj/user中,一些技巧將這些二進制檔案link到了內核之中,二進制檔案中會有一個特殊的符號,通過生成的這些符號可以來參考到這些代碼,

-
第一個函式env_init(),需要初始化所有的Env結構,將其掛入鏈表,也呼叫env_init_percpu來配置底層的資訊,
void env_init(void) { // Set up envs array // LAB 3: Your code here. for(int i=NENV-1;i>=0;i++){ envs[i].env_id=0; envs[i].env_status=ENV_FREE; envs[i].env_link=env_free_list; env_free_list=&envs[i]; } // Per-CPU part of the initialization env_init_percpu(); }與lab2的pages陣列處理類似,注意鏈表的順序,
-
第二個函式env_setup_vm(),為新的環境分配頁目錄,并且初始化
static int env_setup_vm(struct Env *e) { //------------------------------------------ // 源代碼中的注釋此處為了篇幅,很多詳細說明都略去了 // 詳細的資訊,請自行閱讀源代碼 //------------------------------------------ int i; struct PageInfo *p = NULL; // 給頁目錄的分配一個物理頁來存盤 if (!(p = page_alloc(ALLOC_ZERO))) return -E_NO_MEM; // 得到頁目錄的虛擬地址所在 e->env_pgdir = (pde_t*)page2kva(p); // 要求的自增參考計數 p->pp_ref++; // 這部分的頁目錄值,和kern_pgdir是一致的 // 因此 也可以使用 // memcpy(e->env_pgdir,kern_pgdir,PGSIZE); for(i=0;i<PDX(UTOP);i++){ e->env_pgdir[i]=0; } for(i=PDX(UTOP);i<NPDENTRIES;i++){ e->env_pgdir[i]=kern_pgdir[i]; } // 唯一和kern_pgdir不一樣的是對于自身的映射 e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U; return 0; }設定完頁目錄,用戶環境繼承了內核的地址映射,對于后續而言,每個用戶行程都能有自己的虛擬地址空間,且共享內核,
-
第三個函式region_alloc(),作用是為環境分配物理空間,分配物理空間,就是之前說的分配物理頁,使用的是page_alloc(),分配物理也,然后更改頁表,
static void region_alloc(struct Env *e, void *va, size_t len) { void * beigin =ROUNDDOWN(va,PGSIZE); void * end = ROUNDUP(va+len,PGSIZE); for(;beigin<end;beigin+=PGSIZE){ // 申請物理頁 struct PageInfo* apage=page_alloc(0); if(!apage){ panic("region_alloc fail ,out of memory!"); } // 安裝到頁表 page_insert(e->env_pgdir,apage,beigin,PTE_U|PTE_W); } } -
第四個函式 load_icode(),用來決議一個ELF映像,像Lab1中bootloader做的一樣,并把映像加載到新環境的用戶空間,在撰寫時,如下幾點值得注意:
- 閱讀boot/main.c 來得到靈感
- 只有p_type=ELF_PROG_LOAD的段才需要被被加載
- ph->p_va 是需要被加載到的虛地址
- ph->p_memsz 是整個在記憶體中占的大小,也是我們申請空間時的大小
- 從 binary + ph->p_offset 開始的ph->p_filesz位元組需要被復制到ph->p_va處
- 需要考慮一些ELF頭的入口點處理
- 這個程序在進行環境處理時,因為需要映射新的頁,因此需要切換頁目錄
- 哪些地方會產生panic?
static void load_icode(struct Env *e, uint8_t *binary) { struct Proghdr *ph,*end_ph; struct Elf * elf_header = (struct Elf*)binary; if(elf_header->e_magic!=ELF_MAGIC){ panic("not a elf format file"); } ph=(struct Proghdr*)((uint8_t*)elf_header+elf_header->e_phoff); end_ph=ph+elf_header->e_phnum; lcr3(PADDR(e->env_pgdir)); for(;ph<end_ph;ph++){ if(ph->p_type==ELF_PROG_LOAD){ if(ph->p_memsz-ph->p_filesz<0){ panic("p_memsz < p_filesz"); } region_alloc(e,(void*)ph->p_va,ph->p_memsz); memcpy((void*)ph->p_va,(void*)binary+ph->p_offset,ph->p_filesz); memset((void*)(ph->p_va+ph->p_filesz),0,ph->p_memsz-ph->p_filesz); } } e->env_tf.tf_eip=elf_header->e_entry; region_alloc(e,(void*)(USTACKTOP-PGSIZE),PGSIZE); lcr3(PADDR(kern_pgdir)); } -
第五個函式env_create(),用來分配環境并加載ELF檔案,實作很簡單,使用env_alloc獲得一個新的環境,然后用load_icode加載,
void env_create(uint8_t *binary, enum EnvType type) { struct Env* new_env; int r; if((r=env_alloc(&new_env,0))!=0){ panic("env alloc fail in env creat :%e",r); } new_env->env_type=type; load_icode(new_env,binary); } -
第六個函式env_run(),在用戶態中開始運行一個環境,這部分函式只要按照注釋完成即可,
void env_run(struct Env *e) { if((curenv!=NULL) && curenv->env_status==ENV_RUNNING){ curenv->env_type=ENV_RUNNABLE; } curenv=e; e->env_status=ENV_RUNNING; e->env_runs++; lcr3(PADDR(e->env_pgdir)); //保存環境 env_pop_tf(&e->env_tf); }
有一個函式也值得討論,那就是env_pop_tf(),相關的結構和定義如下:
struct PushRegs {
/* registers as pushed by pusha */
uint32_t reg_edi;
uint32_t reg_esi;
uint32_t reg_ebp;
uint32_t reg_oesp; /* Useless */
uint32_t reg_ebx;
uint32_t reg_edx;
uint32_t reg_ecx;
uint32_t reg_eax;
} __attribute__((packed));
struct Trapframe {
struct PushRegs tf_regs;
uint16_t tf_es;
uint16_t tf_padding1;
uint16_t tf_ds;
uint16_t tf_padding2;
uint32_t tf_trapno;
/* below here defined by x86 hardware */
uint32_t tf_err;
uintptr_t tf_eip;
uint16_t tf_cs;
uint16_t tf_padding3;
uint32_t tf_eflags;
/* below here only when crossing rings, such as from user to kernel */
uintptr_t tf_esp;
uint16_t tf_ss;
uint16_t tf_padding4;
} __attribute__((packed));
void
env_pop_tf(struct Trapframe *tf)
{
asm volatile(
"\tmovl %0,%%esp\n" // esp指向tf結構,彈出時會彈到tf里
"\tpopal\n" // 彈出tf_regs中值到各通用暫存器
"\tpopl %%es\n" // 彈出tf_es 到 es暫存器
"\tpopl %%ds\n" // 彈出tf_ds 到 ds暫存器
"\taddl $0x8,%%esp\n" // 跳過tf_trapno和tf_err
"\tiret\n" // 中斷回傳 彈出tf_eip,tf_cs,tf_eflags,tf_esp,tf_ss到相應暫存器
: : "g" (tf) : "memory");
panic("iret failed"); /* mostly to placate the compiler */
}
運行make qemu-gdb和make gdb,然后斷點打在env_pop_tf,執行到iret指令,在iret之前
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xf01d1030 0xf01d1030
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0xf01038e2 0xf01038e2 <env_pop_tf+31>
eflags 0x96 [ PF AF SF ]
cs 0x8 8
ss 0x10 16
ds 0x23 35
es 0x23 35
fs 0x23 35
gs 0x23 35
可以看到此時的cs為00001 000,是我們GDT中的第一個段,內核段,在iret之后
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0x0 0
esp 0xeebfe000 0xeebfe000
ebp 0x0 0x0
esi 0x0 0
edi 0x0 0
eip 0x800020 0x800020
eflags 0x2 [ ]
cs 0x1b 27
ss 0x23 35
ds 0x23 35
es 0x23 35
fs 0x23 35
gs 0x23 35
cs=0X1b=0001 1011,所以是GDT中的第三個描述符(user code segment),權限為3(用戶態),
在obj/user/hello.asm找到
800b93: cd 30 int $0x30
syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
斷點設定在此處,由于系統呼叫還沒有實作,這里往下執行就會觸發triple fault,
可以有如下的函式呼叫圖:
- start (kern/entry.S)
- i386_init (kern/init.c)
-
cons_init
-
mem_init
-
env_init
-
trap_init (still incomplete at this point)
-
env_create
-
env_alloc
- env_setup_vm
-
load_icode
- region_alloc
-
-
env_run
- env_pop_tf
-
User stack and Kernel stack
這里提前說明一下關于用戶堆疊和內核堆疊,以及這倆的切換程序,在后續行程等地方,這一套機制都很受用,

這是涉及到特權級切換的情況,用戶程式的堆疊和內核的堆疊,組合形成一套堆疊,這個程序ss,sp,eflags,cs,eip在中斷發生時由處理器壓入,通用暫存器部分需要自己實作,詳情可以參考哈工大李治軍老師關于作業系統的課程,
Handling Interrupts and Exceptions
Part of 80386 Programmer's Manual

這是這部分開頭練習的要求,這里就來讀一讀8086程式員手冊,
首先便是中斷和溢位的分類:

一般地不刻意區分這些術語(在這套體系中),
NMI和Exception都分配了唯一的中斷號,系統保留0~31這32個中斷號(因此,如果用戶自定義中斷,中斷號應從32開始),

如果一定要區分的話,exception被分為faults, traps和aborts, 區分的標準是這些exception如何被通知,何時重新執行造成溢位的指令,

下一個話題是中斷描述符表IDT,每個中斷或者溢位的服務程式都和IDT中的8B中斷描述符相關聯,和GDT,LDT不同,IDT的第一個描述符并不是空的,

IDT中的描述符有三種類別:任務們,中斷門,陷阱門(由type欄位標識),

至于中斷服務程式的定位,就是在查GDT或LDT之前,多查一次IDT

而中斷服務程式如果和當前代碼之間存在特權級的轉移,那么堆疊的變化在上文已經說明了,
An Example
講前文的諸多小知識拼湊起來,通過一個例子來過一遍整個程序,
處理器正在用戶空間執行代碼,遇到了一條除以零的指令,由此引發溢位:
- 處理器切換到內核堆疊(由SS0 ESP0進行內核堆疊的定位),此時內核堆疊為空,
- 內核堆疊壓入一系列溢位現場,進行現場保護

- 因為正在處理除以零溢位,因此中斷向量0被索引到了,因此處理器讀取IDT的第0項,將cs:eip指向中斷處理程式,
- 處理程式獲得控制權并處理該溢位,比如說該程式終止該用戶環境的運行,
某些特定的x86溢位,除了會壓入上面的經典5個欄位,還會壓入error code,在處理堆疊時,不要忘了跳過這個欄位,如果需要的話,

Setting Up the IDT
經過了理論部分,現在到了該實作IDT的時候了,


首先是trapentry.S, 在這個檔案中提供了如下兩個宏:
作用是壓入中斷號,跳轉到_alltraps;其中對于壓入錯誤碼的使用TRAPHANDLER,對于不壓入錯誤碼的使用TRAPHANDLER_NOEC,此處入口的name應該是一個函式的名字,正如內部宣告:.type name, @function; /* symbol type is function */
#define TRAPHANDLER(name, num) \
.globl name; /* define global symbol for 'name' */ \
.type name, @function; /* symbol type is function */ \
.align 2; /* align function definition */ \
name: /* function starts here */ \
pushl $(num); \
jmp _alltraps
#define TRAPHANDLER_NOEC(name, num) \
.globl name; \
.type name, @function; \
.align 2; \
name: \
pushl $0; \
pushl $(num); \
jmp _alltraps
閱讀注釋,可以完善該檔案:
_alltraps中的push %esp 相當于傳遞了一個Trapframe結構,因為經典的5個欄位由處理器自動壓入,而_alltraps中壓入的順序,正好可以與Trapframe結構對應起來,因此trap函式可以獲得Trapframe資訊,
/*
* Lab 3: Your code here for generating entry points for the different traps.
*/
TRAPHANDLER_NOEC(int0,0);
TRAPHANDLER_NOEC(int1,1);
TRAPHANDLER_NOEC(int2,2);
TRAPHANDLER_NOEC(int3,3);
TRAPHANDLER_NOEC(int4,4);
TRAPHANDLER_NOEC(int5,5);
TRAPHANDLER_NOEC(int6,6);
TRAPHANDLER_NOEC(int7,7);
TRAPHANDLER(int8,8);
TRAPHANDLER(int10,10);
TRAPHANDLER(int11,11);
TRAPHANDLER(int12,12);
TRAPHANDLER(int13,13);
TRAPHANDLER(int14,14);
TRAPHANDLER_NOEC(int16,16);
TRAPHANDLER_NOEC(__syscall,T_SYSCALL);
/*
* Lab 3: Your code here for _alltraps
*/
_alltraps:
pushl %ds
pushl %es
pushal
push $GD_KD
popl %ds
push $GD_KD
popl %es
pushl %esp
call trap
下面要建立IDT,首先關于門描述符,在mmu.h中提供了相關的工具

// Gate descriptors for interrupts and traps
struct Gatedesc {
unsigned gd_off_15_0 : 16; // low 16 bits of offset in segment
unsigned gd_sel : 16; // segment selector
unsigned gd_args : 5; // # args, 0 for interrupt/trap gates
unsigned gd_rsv1 : 3; // reserved(should be zero I guess)
unsigned gd_type : 4; // type(STS_{TG,IG32,TG32})
unsigned gd_s : 1; // must be 0 (system)
unsigned gd_dpl : 2; // descriptor(meaning new) privilege level
unsigned gd_p : 1; // Present
unsigned gd_off_31_16 : 16; // high bits of offset in segment
};
// Set up a normal interrupt/trap gate descriptor.
// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
// see section 9.6.1.3 of the i386 reference: "The difference between
// an interrupt gate and a trap gate is in the effect on IF (the
// interrupt-enable flag). An interrupt that vectors through an
// interrupt gate resets IF, thereby preventing other interrupts from
// interfering with the current interrupt handler. A subsequent IRET
// instruction restores IF to the value in the EFLAGS image on the
// stack. An interrupt through a trap gate does not change IF."
// - sel: Code segment selector for interrupt/trap handler
// - off: Offset in code segment for interrupt/trap handler
// - dpl: Descriptor Privilege Level -
// the privilege level required for software to invoke
// this interrupt/trap gate explicitly using an int instruction.
#define SETGATE(gate, istrap, sel, off, dpl) \
{ \
(gate).gd_off_15_0 = (uint32_t) (off) & 0xffff; \
(gate).gd_sel = (sel); \
(gate).gd_args = 0; \
(gate).gd_rsv1 = 0; \
(gate).gd_type = (istrap) ? STS_TG32 : STS_IG32; \
(gate).gd_s = 0; \
(gate).gd_dpl = (dpl); \
(gate).gd_p = 1; \
(gate).gd_off_31_16 = (uint32_t) (off) >> 16; \
}
因此trap_init()函式如下
void
trap_init(void)
{
extern struct Segdesc gdt[];
// LAB 3: Your code here.
void int0();
void int1();
void int2();
void int3();
void int4();
void int5();
void int6();
void int7();
void int8();
void int10();
void int11();
void int12();
void int13();
void int14();
void int16();
void _syscall_();
SETGATE(idt[0],0,GD_KT,int0,0);
SETGATE(idt[1],0,GD_KT,int1,0);
SETGATE(idt[2],0,GD_KT,int2,0);
SETGATE(idt[3],0,GD_KT,int3,0);
SETGATE(idt[4],0,GD_KT,int4,0);
SETGATE(idt[5],0,GD_KT,int5,0);
SETGATE(idt[6],0,GD_KT,int6,0);
SETGATE(idt[7],0,GD_KT,int7,0);
SETGATE(idt[8],0,GD_KT,int8,0);
SETGATE(idt[10],0,GD_KT,int10,0);
SETGATE(idt[11],0,GD_KT,int11,0);
SETGATE(idt[12],0,GD_KT,int12,0);
SETGATE(idt[13],0,GD_KT,int13,0);
SETGATE(idt[14],0,GD_KT,int14,0);
SETGATE(idt[16],0,GD_KT,int16,0);
SETGATE(idt[T_SYSCALL],0,GD_KT,_syscall_,0);
// Per-CPU setup
trap_init_percpu();
}
至此,函式的呼叫關系如圖:

當遇到中斷時,會呼叫trap:

trap會列印出相關的資訊,
現在可以開始測驗了:

實驗三的A部分到此完結,下一篇文章,關于PartA 的一些問題和PartB
轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/351886.html
標籤:Linux
上一篇:Red Hat Enterprise Linux (RHEL) 9 更新了什么,即 Rocky Linux 9 和 AlmaLinux 9 展望
下一篇:Linux系統編程之檔案IO
