6.828 操作系統(tǒng) lab3 實(shí)驗(yàn)報(bào)告

簡介


lab3 將主要實(shí)現(xiàn)能運(yùn)行被保護(hù)的用戶模式環(huán)境(protected user-mode environment,即 process)的內(nèi)核服務(wù)。我們將增加數(shù)據(jù)結(jié)構(gòu)來記錄進(jìn)程、創(chuàng)建進(jìn)程、為其裝載一個(gè)程序鏡像。我們還要讓 JOS 內(nèi)核能夠處理進(jìn)程產(chǎn)生的系統(tǒng)調(diào)用和異常。

Part A: 用戶環(huán)境和異常處理


Exercise 1. Modify mem_init() in kern/pmap.c to allocate and map the envs array. This array consists of exactly NENV instances of the Env structure allocated much like how you allocated the pages array. Also like the pages array, the memory backing envs should also be mapped user read-only at UENVS (defined in inc/memlayout.h) so user processes can read from this array.

首先,最大進(jìn)程個(gè)數(shù) NENV(1024) 以及進(jìn)程描述符 struct Env 的定義可以在 inc/env.h 中找到。同時(shí),我們?cè)?kern/env.h 以及 kern/env.c 中可以找到三個(gè)全局變量的定義:

extern struct Env *envs;        // All environments
extern struct Env *curenv;      // Current environment
static struct Env *env_free_list;   // Free environment list

我們需要將 envs 指針指向一個(gè)由 Env 結(jié)構(gòu)體組成的數(shù)組,就像我們?cè)?lab2 中對(duì) pages 指針做的一樣。同時(shí),JOS 還需要將不活動(dòng)的 Env 記錄在 env_free_list 之中,類似于 page_free_list。curenv 指針記錄著現(xiàn)在執(zhí)行的進(jìn)程。在第一個(gè)進(jìn)程運(yùn)行之前,為NULL。
在 kern/pmap.c 中添加以下兩行代碼,基本就是仿造之前對(duì) pages 的處理。

    // 分配空間并初始化
    //////////////////////////////////////////////////////////////////////
    // Make 'envs' point to an array of size 'NENV' of 'struct Env'.
    // LAB 3: Your code here.
    envs = (struct Env *) boot_alloc(NENV * sizeof(struct Env));
    memset(envs, 0, NENV * sizeof(struct Env));
    // 將虛擬內(nèi)存的 UENVS 段映射到 envs 的物理地址
    //////////////////////////////////////////////////////////////////////
    // Map the 'envs' array read-only by the user at linear address UENVS
    // (ie. perm = PTE_U | PTE_P).
    // Permissions:
    //    - the new image at UENVS  -- kernel R, user R
    //    - envs itself -- kernel RW, user NONE
    // LAB 3: Your code here.
    boot_map_region(kern_pgdir, (uintptr_t) UENVS, ROUNDUP(NENV*sizeof(struct Env), PGSIZE), PADDR(envs), PTE_U | PTE_P);

check_kern_pgdir() 成功。

Exercise 2. In the file env.c, finish coding the following functions:
env_init()
Initialize all of the Env structures in the envs array and add them to the env_free_list. Also calls env_init_percpu, which configures the segmentation hardware with separate segments for privilege level 0 (kernel) and privilege level 3 (user).
env_setup_vm()
Allocate a page directory for a new environment and initialize the kernel portion of the new environment's address space.
region_alloc()
Allocates and maps physical memory for an environment
load_icode()
You will need to parse an ELF binary image, much like the boot loader already does, and load its contents into the user address space of a new environment.
env_create()
Allocate an environment with env_alloc and call load_icode to load an ELF binary into it.
env_run()
Start a given environment running in user mode.

看上去挺復(fù)雜的一個(gè)練習(xí)。每個(gè)函數(shù)逐一說明。
env_init()
作用是初始化 envs 這個(gè)數(shù)組以及 env_free_list。需要注意的主要是鏈表的順序,要求第一個(gè)被使用是 envs[0],所以我們從后往前插入(類似于棧,后進(jìn)先出)。

void
env_init(void)
{
    // Set up envs array
    // LAB 3: Your code here.
    int i = NENV;
    while (i>0) {
        i--;
        envs[i].env_id = 0;
        envs[i].env_link = env_free_list;
        env_free_list = &env[i];
    }
    // Per-CPU part of the initialization
    env_init_percpu();
}

env_setup_vm()
新建并初始化進(jìn)程的頁目錄,一個(gè)頁目錄占用空間 4kB。需要注意兩點(diǎn):

  1. 進(jìn)程的頁目錄與內(nèi)核的頁目錄基本相同,僅需修改一下 UVPT,所以可以直接 memcpy。
  2. 需要增加頁引用。
static int
env_setup_vm(struct Env *e)
{
    int i;
    struct PageInfo *p = NULL;

    // Allocate a page for the page directory
    if (!(p = page_alloc(ALLOC_ZERO)))
        return -E_NO_MEM;

    // Now, set e->env_pgdir and initialize the page directory.
    //
    // Hint:
    //    - The VA space of all envs is identical above UTOP
    //  (except at UVPT, which we've set below).
    //  See inc/memlayout.h for permissions and layout.
    //  Can you use kern_pgdir as a template?  Hint: Yes.
    //  (Make sure you got the permissions right in Lab 2.)
    //    - The initial VA below UTOP is empty.
    //    - You do not need to make any more calls to page_alloc.
    //    - Note: In general, pp_ref is not maintained for
    //  physical pages mapped only above UTOP, but env_pgdir
    //  is an exception -- you need to increment env_pgdir's
    //  pp_ref for env_free to work correctly.
    //    - The functions in kern/pmap.h are handy.

    // LAB 3: Your code here.
    e->env_pgdir = page2kva(p);
    memcpy(e->env_pgdir, kern_pgdir, PGSIZE); // use kern_pgdir as template 
    p->pp_ref++;
    // UVPT maps the env's own page table read-only.
    // Permissions: kernel R, user R
    e->env_pgdir[PDX(UVPT)] = PADDR(e->env_pgdir) | PTE_P | PTE_U;

    return 0;
}

region_alloc()
為進(jìn)程分配內(nèi)存并完成映射。重點(diǎn)就是想到要利用 lab2 中的 page_alloc() 完成分配內(nèi)存頁, page_insert() 完成虛擬地址到物理頁的映射。

static void
region_alloc(struct Env *e, void *va, size_t len)
{
    // LAB 3: Your code here.
    // (But only if you need it for load_icode.)
    //
    // Hint: It is easier to use region_alloc if the caller can pass
    //   'va' and 'len' values that are not page-aligned.
    //   You should round va down, and round (va + len) up.
    //   (Watch out for corner-cases!)
    size_t pgnum = ROUNDUP(len, PGSIZE) / PGSIZE;
    uintptr_t va_start = ROUNDDOWN((uintptr_t)va, PGSIZE);
    struct PageInfo *pginfo = NULL;
    cprintf("Allocate size: %d, Start from: %08x\n", len, va);
    for (size_t i=0; i<pgnum; i++) {
        pginfo = page_alloc(0);
        if (! pginfo) {
            int r = -E_NO_MEM;
            panic("region_alloc: %e" , r);
        }
        int r = page_insert(e->env_pgdir, pginfo, (void *)va_start, PTE_W | PTE_U | PTE_P);
        if (r < 0) {
            panic("region_alloc: %e" , r);
        }
        cprintf("Va_start = %08x\n",va_start);
        va_start += PGSIZE;
    }
}

load_icode()
這是本 exercise 最難的一個(gè)函數(shù)。作用是將 ELF 二進(jìn)制文件讀入內(nèi)存,由于 JOS 暫時(shí)還沒有自己的文件系統(tǒng),實(shí)際就是從 *binary 這個(gè)內(nèi)存地址讀取??梢詮?boot/main.c 中找到靈感。
大概需要做的事:

  1. 根據(jù) ELF header 得出 Programm header。
  2. 遍歷所有 Programm header,分配好內(nèi)存,加載類型為 ELF_PROG_LOAD 的段。
  3. 分配用戶棧。

需要思考的問題:

  1. 怎么切換頁目錄?
    lcr3([頁目錄物理地址]) 將地址加載到 cr3 寄存器。
  2. 怎么更改函數(shù)入口?
    將 env->env_tf.tf_eip 設(shè)置為 elf->e_entry,等待之后的 env_pop_tf() 調(diào)用。
static void
load_icode(struct Env *e, uint8_t *binary)
{
    struct Proghdr *ph, *eph;
    struct Elf *elf = (struct Elf *)binary;
    if (elf->e_magic != ELF_MAGIC) {
        panic("load_icode: not an ELF file");
    }
    ph = (struct Proghdr *)(binary + elf->e_phoff);
    eph = ph + elf->e_phnum;

    lcr3(PADDR(e->env_pgdir));
    for (; ph<eph; ph++) {
        if (ph->p_type == ELF_PROG_LOAD) {
            if (ph->p_filesz > ph->p_memsz) {
                panic("load_icode: file size is greater than memory size");
            }
            region_alloc(e, (void *)ph->p_va, ph->p_memsz);
            memcpy((void *)ph->p_va, 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->e_entry;
    // Now map one page for the program's initial stack
    // at virtual address USTACKTOP - PGSIZE.
    
    // LAB 3: Your code here.
    region_alloc(e, (void *) USTACKTOP-PGSIZE, PGSIZE);
    lcr3(PADDR(kern_pgdir));
}

env_create()
作用是新建一個(gè)進(jìn)程。調(diào)用已經(jīng)寫好的 env_alloc() 函數(shù)即可,之后更改類型并且利用 load_icode() 讀取 ELF。

void
env_create(uint8_t *binary, enum EnvType type)
{
    // LAB 3: Your code here.
    struct Env *e;
    int r = env_alloc(&e, 0);
    if (r<0) {
        panic("env_create: %e",r);
    }
    e->env_type = type;
    load_icode(e, binary);
}

env_run()
啟動(dòng)某個(gè)進(jìn)程。注釋已經(jīng)非常詳細(xì)地說明了怎么做,主要說下 env_pop_tf() 這個(gè)函數(shù)。該函數(shù)的作用是將 struct Trapframe 中存儲(chǔ)的寄存器狀態(tài) pop 到相應(yīng)寄存器中。查看之前寫的 load_icode() 函數(shù)中的 e->env_tf.tf_eip = elf->e_entry 這一句,經(jīng)過 env_pop_tf() 之后,指令寄存器的值即設(shè)置到了可執(zhí)行文件的入口。

void
env_run(struct Env *e)
{
    // Step 1: If this is a context switch (a new environment is running):
    //     1. Set the current environment (if any) back to
    //        ENV_RUNNABLE if it is ENV_RUNNING (think about
    //        what other states it can be in),
    //     2. Set 'curenv' to the new environment,
    //     3. Set its status to ENV_RUNNING,
    //     4. Update its 'env_runs' counter,
    //     5. Use lcr3() to switch to its address space.
    // Step 2: Use env_pop_tf() to restore the environment's
    //     registers and drop into user mode in the
    //     environment.

    // Hint: This function loads the new environment's state from
    //  e->env_tf.  Go back through the code you wrote above
    //  and make sure you have set the relevant parts of
    //  e->env_tf to sensible values.

    // LAB 3: Your code here.
    // panic("env_run not yet implemented");
    if (curenv && curenv->env_status == ENV_RUNNING) {
        curenv->env_status = ENV_RUNNABLE;
    }
    curenv = e;
    e->env_status = ENV_RUNNING;
    e->env_runs++;
    lcr3(PADDR(e->env_pgdir));
    
    env_pop_tf(&e->env_tf);
}

至此結(jié)束,本次 exercise 結(jié)束后運(yùn)行并不會(huì)成功,會(huì)報(bào)錯(cuò) Triple fault。然后 gdb 停止在:

=> 0x800a1c:    int    $0x30
0x00800a1c in ?? ()

原因是此時(shí)系統(tǒng)已經(jīng)進(jìn)入用戶空間,執(zhí)行了 hello 直到使用系統(tǒng)調(diào)用。然而由于 JOS 還沒有允許從用戶態(tài)到內(nèi)核態(tài)的切換,CPU 會(huì)產(chǎn)生一個(gè)保護(hù)異常,然而這個(gè)異常也沒有程序進(jìn)行處理,于是生成了 double fault 異常,這個(gè)異常同樣沒有處理。所以報(bào)錯(cuò) triple fault。也就是說,看到執(zhí)行到了 int 這個(gè)中斷,實(shí)際上就是本次 exercise 順利結(jié)束,這個(gè)系統(tǒng)調(diào)用是為了在終端輸出字符。

處理中斷和異常

上一節(jié)中,int $0x30這個(gè)系統(tǒng)調(diào)用指令是一條死路:一旦進(jìn)程進(jìn)入用戶模式,內(nèi)核將無法再次獲得控制權(quán)。異常和中斷都是“受保護(hù)的控制權(quán)轉(zhuǎn)移” (protected control transfers),使處理器從用戶模式轉(zhuǎn)到內(nèi)核模式,用戶模式代碼無法干擾內(nèi)核或者其他進(jìn)程的運(yùn)行。區(qū)別在于,中斷是由處理器外部的異步事件產(chǎn)生;而異常是由目前處理的代碼產(chǎn)生,例如除以0。
為保證切換是被保護(hù)的,處理器的中斷、異常機(jī)制使得正在運(yùn)行的代碼無須選擇在哪里以什么方式進(jìn)入內(nèi)核。相反,處理器將保證內(nèi)核在嚴(yán)格的限制下才能被進(jìn)入。在 x86 架構(gòu)下,一共有兩個(gè)機(jī)制提供這種保護(hù):

  1. 中斷描述符表(Interrupt Descriptor Table, IDT)
    處理器將確保從一些內(nèi)核預(yù)先定義的條目才能進(jìn)入內(nèi)核,而不是由中斷或異常發(fā)生時(shí)運(yùn)行的代碼決定。
    x86 支持最多 256 個(gè)不同中斷和異常的條目。每個(gè)包含一個(gè)中斷向量,是一個(gè) 0~255 之間的數(shù)(那為什么叫向量?),代表中斷來源:不同的設(shè)備以及錯(cuò)誤類型。CPU 利用這些向量作為中斷描述符表的索引。而這個(gè)表是內(nèi)核定義在私有內(nèi)存上(用戶沒有權(quán)限),就像全局描述符表(Global Descripter Table, GDT)一樣。從表中恰當(dāng)?shù)臈l目,處理器可以獲得:
  • 需要加載到指令指針寄存器(EIP)的值,該值指向內(nèi)核中處理這類異常的代碼。
  • 需要加載到代碼段寄存器(CS)的值,其中最低兩位表示優(yōu)先級(jí)(這也是為什么說可以尋址 2^46 的空間而不是 2^48)。 在JOS 中,所有的異常都在內(nèi)核模式處理,優(yōu)先級(jí)為0 (用戶模式為3)。
  1. 任務(wù)狀態(tài)段(Task State Segment, TSS)
    處理器需要保存中斷和異常出現(xiàn)時(shí)的自身狀態(tài),例如 EIP 和 CS,以便處理完后能返回原函數(shù)繼續(xù)執(zhí)行。但是存儲(chǔ)區(qū)域必須禁止用戶訪問,避免惡意代碼或 bug 的破壞。
    因此,當(dāng) x86 處理器處理從用戶到內(nèi)核的模式轉(zhuǎn)換時(shí),也會(huì)切換到內(nèi)核棧。而 TSS 指明段選擇器和棧地址。處理器將 SS, ESP, EFLAGS, CS, EIP 壓入新棧,然后從 IDT 讀取 CS 和 EIP,根據(jù)新棧設(shè)置 ESP 和 SS。
    JOS 僅利用 TSS 來定義需要切換的內(nèi)核棧。由于內(nèi)核模式在 JOS 優(yōu)先級(jí)是 0,因此處理器用 TSS 的 ESP0 和 SS0 來定義內(nèi)核棧,無需 TSS 結(jié)構(gòu)體中的其他內(nèi)容。其中, SS0 種存儲(chǔ)的是 GD_KD(0x10),ESP0 種存儲(chǔ)的是 KSTACKTOP(0xf0000000)。相關(guān)定義在inc/memlayout.h中可以找到。

中斷和異常的類型

x86 的所有異常可以用中斷向量 0~31 表示,對(duì)應(yīng) IDT 的第 0~31 項(xiàng)。例如,頁錯(cuò)誤產(chǎn)生一個(gè)中斷向量為 14 的異常。大于 32 的中斷向量表示的都是中斷,其中,軟件中斷用 int 指令產(chǎn)生,而硬件中斷則由硬件在需要關(guān)注的時(shí)候產(chǎn)生。

一個(gè)例子

通過一個(gè)例子來理解上面的知識(shí)。假設(shè)處理器正在執(zhí)行用戶環(huán)境的代碼,遇到了"除0"異常。

  1. 處理器切換到內(nèi)核棧,利用了上文 TSS 中的 ESP0 和 SS0。
  2. 處理器將異常參數(shù) push 到了內(nèi)核棧。一般情況下,按順序 push SS, ESP, EFLAGS, CS, EIP
    +--------------------+ KSTACKTOP
    | 0x00000 | old SS | " - 4
    | old ESP | " - 8
    | old EFLAGS | " - 12
    | 0x00000 | old CS | " - 16
    | old EIP | " - 20 <---- ESP
    +--------------------+
    存儲(chǔ)這些寄存器狀態(tài)的意義是:SS(堆棧選擇器) 的低 16 位與 ESP 共同確定當(dāng)前棧狀態(tài);EFLAGS(標(biāo)志寄存器)存儲(chǔ)當(dāng)前FLAG;CS(代碼段寄存器) 和 EIP(指令指針寄存器) 確定了當(dāng)前即將執(zhí)行的代碼地址,E 代表"擴(kuò)展"至32位。根據(jù)這些信息,就能保證處理中斷結(jié)束后能夠恢復(fù)到中斷前的狀態(tài)。
  3. 因?yàn)槲覀儗⑻幚硪粋€(gè)"除0"異常,其對(duì)應(yīng)中斷向量是0,因此,處理器讀取 IDT 的條目0,設(shè)置 CS:EIP 指向該條目對(duì)應(yīng)的處理函數(shù)。
  4. 處理函數(shù)獲得程序控制權(quán)并且處理該異常。例如,終止進(jìn)程的運(yùn)行。

對(duì)于某些特殊的 x86 異常,除了以上 5 個(gè)參數(shù)以外,還需要存儲(chǔ)一個(gè) error code。

                 +--------------------+ KSTACKTOP             
                 | 0x00000 | old SS   |     " - 4
                 |      old ESP       |     " - 8
                 |     old EFLAGS     |     " - 12
                 | 0x00000 | old CS   |     " - 16
                 |      old EIP       |     " - 20
                 |     error code     |     " - 24 <---- ESP
                 +--------------------+     

例如,頁錯(cuò)誤異常(中斷向量=14)就是一個(gè)重要的例子,它就需要額外存儲(chǔ)一個(gè) error code。

嵌套的異常和中斷

內(nèi)核和用戶進(jìn)程都會(huì)引起異常和中斷。然而,僅在從用戶環(huán)境進(jìn)入內(nèi)核時(shí)才會(huì)切換棧。如果中斷發(fā)生時(shí)已經(jīng)在內(nèi)核態(tài)了(此時(shí), CS 寄存器的低 2bit 為 00) ,那么 CPU 就直接將狀態(tài)壓入內(nèi)核棧,不再需要切換棧。這樣,內(nèi)核就能處理內(nèi)核自身引起的"嵌套異常",這是實(shí)現(xiàn)保護(hù)的重要工具。
如果處理器已經(jīng)處于內(nèi)核態(tài),然后發(fā)生了嵌套異常,由于它并不進(jìn)行棧切換,所以無須存儲(chǔ) SSESP 寄存器狀態(tài)。對(duì)于不包含 error code 的異常,在進(jìn)入處理函數(shù)前內(nèi)核棧狀態(tài)如下所示:

                 +--------------------+ <---- old ESP
                 |     old EFLAGS     |     " - 4
                 | 0x00000 | old CS   |     " - 8
                 |      old EIP       |     " - 12
                 +--------------------+             

對(duì)于包含了 error code 的異常,則將 error code 繼續(xù) push 到 EIP之后。
警告:如果 CPU 處理嵌套異常的時(shí)候,無法將狀態(tài) push 到內(nèi)核棧(由于棧空間不足等原因),則 CPU 無法恢復(fù)當(dāng)前狀態(tài),只能重啟。當(dāng)然,這是內(nèi)核設(shè)計(jì)中必須避免的。

建立中斷描述符表(IDT)

通過上文,已經(jīng)了解到了建立 IDT 以及處理異常所需要的基本信息。頭文件 inc/trap.hkern/trap.h 包含了與中斷和異常相關(guān)的定義,需要仔細(xì)閱讀。其中 kern/trap.h 包含內(nèi)核私有定義,而 inc/trap.h 包含對(duì)內(nèi)核以及用戶進(jìn)程和庫都有用的定義。
每個(gè)異常和中斷都應(yīng)該在 trapentry.Strap_init() 有自己的處理函數(shù),并在 IDT 中將這些處理函數(shù)的地址初始化。每個(gè)處理函數(shù)都需要在棧上新建一個(gè) struct Trapframe(見 inc/trap.h),以其地址為參數(shù)調(diào)用 trap() 函數(shù),然后進(jìn)行異常處理。

Exercise 4. Edit trapentry.S and trap.c and implement the features described above. The macros TRAPHANDLER and TRAPHANDLER_NOEC in trapentry.S should help you, as well as the T_* defines in inc/trap.h. You will need to add an entry point in trapentry.S (using those macros) for each trap defined in inc/trap.h, and you'll have to provide _alltraps which the TRAPHANDLER macros refer to. You will also need to modify trap_init() to initialize the idt to point to each of these entry points defined in trapentry.S; the SETGATE macro will be helpful here.
Your _alltraps should:

  1. push values to make the stack look like a struct Trapframe
  2. load GD_KD into %ds and %es
  3. pushl %esp to pass a pointer to the Trapframe as an argument to trap()
  4. call trap (can trap ever return?)

Consider using the pushal instruction; it fits nicely with the layout of the struct Trapframe.
Test your trap handling code using some of the test programs in the user directory that cause exceptions before making any system calls, such as user/divzero. You should be able to get make grade to succeed on the divzero, softint, and badsegment tests at this point.

較難的一個(gè)練習(xí),首先第一步是搞明白TRAPHANDLER這段匯編代碼的意義:

#define TRAPHANDLER(name, num)  
    .globl name;        
    .type name, @function;  
    .align 2;
    name:
    /*
    *  pushl $0;    // if no error code 
    */
    pushl $(num);                           
    jmp _alltraps
  1. .global/ .globl :用來定義一個(gè)全局的符號(hào),格式如下:
    .global symbol 或者 .globl symbol
    匯編函數(shù)如果需要在其他文件調(diào)用,需要把函數(shù)聲明為全局的,此時(shí)就會(huì)用到 .global這個(gè)偽操作。
  2. .type : 用來指定一個(gè)符號(hào)的類型是函數(shù)類型或者是對(duì)象類型,對(duì)象類型一般是數(shù)據(jù), 格式如下:
    .type symbol, @object
    .type symbol, @function
  3. .align : 用來指定內(nèi)存對(duì)齊方式,格式如下:
    .align size
    表示按 size 字節(jié)對(duì)齊內(nèi)存。

這一步做了什么?光看這里很難理解,提示說是構(gòu)造一個(gè) Trapframe 結(jié)構(gòu)體來保存現(xiàn)場(chǎng),但是這里怎么直接就 push 中斷向量了?實(shí)際上,在上文已經(jīng)指出, cpu 自身會(huì)先 push 一部分寄存器(見例子所述),而其他則由用戶和操作系統(tǒng)決定。由于中斷向量是操作系統(tǒng)定義的,所以從這部分開始就已經(jīng)不屬于 cpu 的工作范疇了。

trapentry.S 中:

TRAPHANDLER_NOEC(handler0, T_DIVIDE)
TRAPHANDLER_NOEC(handler1, T_DEBUG)
TRAPHANDLER_NOEC(handler2, T_NMI)
TRAPHANDLER_NOEC(handler3, T_BRKPT)
TRAPHANDLER_NOEC(handler4, T_OFLOW)
TRAPHANDLER_NOEC(handler5, T_BOUND)
TRAPHANDLER_NOEC(handler6, T_ILLOP)
TRAPHANDLER_NOEC(handler7, T_DEVICE)
TRAPHANDLER(handler8, T_DBLFLT)
// 9 deprecated since 386
TRAPHANDLER(handler10, T_TSS)
TRAPHANDLER(handler11, T_SEGNP)
TRAPHANDLER(handler12, T_STACK)
TRAPHANDLER(handler13, T_GPFLT)
TRAPHANDLER(handler14, T_PGFLT)
// 15 reserved by intel
TRAPHANDLER_NOEC(handler16, T_FPERR)
TRAPHANDLER(handler17, T_ALIGN)
TRAPHANDLER_NOEC(handler18, T_MCHK)
TRAPHANDLER_NOEC(handler19, T_SIMDERR)
// system call (interrupt)
TRAPHANDLER_NOEC(handler48, T_SYSCALL)

該部分主要作用是聲明函數(shù)。該函數(shù)是全局的,但是在 C 文件中使用的時(shí)候需要使用 void name(); 再聲明一下。

_alltraps:
pushl %ds
pushl %es
pushal

movw $GD_KD, %ax
movw %ax, %ds
movw %ax, %es
pushl %esp
call trap

這部分較有難度,首先要搞明白,棧是從高地址向低地址生長,而結(jié)構(gòu)體在內(nèi)存中的存儲(chǔ)是從低地址到高地址。而 cpu 以及TRAPHANDLER宏已經(jīng)將壓棧工作進(jìn)行到了中斷向量部分,若要形成一個(gè) Trapframe,則還應(yīng)該依次壓入 ds, es以及 struct PushRegs中的各寄存器(倒序,可使用 pusha指令)。此后還需要更改數(shù)據(jù)段為內(nèi)核的數(shù)據(jù)段。注意,不能用立即數(shù)直接給段寄存器賦值。因此不能直接寫movw $GD_KD, %ds。

kern/trap.c 中:

void
trap_init(void)
{
    extern struct Segdesc gdt[];

    // LAB 3: Your code here.
    void handler0();
    void handler1();
    void handler2();
    void handler3();
    void handler4();
    void handler5();
    void handler6();
    void handler7();
    void handler8();

    void handler10();
    void handler11();
    void handler12();
    void handler13();
    void handler14();

    void handler16();
    void handler17();
    void handler18();
    void handler19();
    void handler48();

    SETGATE(idt[T_DIVIDE], 1, GD_KT, handler0, 0);
    SETGATE(idt[T_DEBUG], 1, GD_KT, handler1, 0);
    SETGATE(idt[T_NMI], 1, GD_KT, handler2, 0);
    SETGATE(idt[T_BRKPT], 1, GD_KT, handler3, 0);
    SETGATE(idt[T_OFLOW], 1, GD_KT, handler4, 0);
    SETGATE(idt[T_BOUND], 1, GD_KT, handler5, 0);
    SETGATE(idt[T_ILLOP], 1, GD_KT, handler6, 0);
    SETGATE(idt[T_DEVICE], 1, GD_KT, handler7, 0);
    SETGATE(idt[T_DBLFLT], 1, GD_KT, handler8, 0);

    SETGATE(idt[T_TSS], 1, GD_KT, handler10, 0);
    SETGATE(idt[T_SEGNP], 1, GD_KT, handler11, 0);
    SETGATE(idt[T_STACK], 1, GD_KT, handler12, 0);
    SETGATE(idt[T_GPFLT], 1, GD_KT, handler13, 0);
    SETGATE(idt[T_PGFLT], 1, GD_KT, handler14, 0);
    
    SETGATE(idt[T_FPERR], 1, GD_KT, handler16, 0);
    SETGATE(idt[T_ALIGN], 1, GD_KT, handler17, 0);
    SETGATE(idt[T_MCHK], 1, GD_KT, handler18, 0);
    SETGATE(idt[T_SIMDERR], 1, GD_KT, handler19, 0);

    // interrupt
    SETGATE(idt[T_SYSCALL], 0, GD_KT, handler48, 0);
    
    // Per-CPU setup 
    trap_init_percpu();
}

重點(diǎn)是兩個(gè)問題。

  1. 函數(shù)如何聲明?
    這個(gè)問題其實(shí)已經(jīng)在 trapentry.S 的注釋里回答了。注意該函數(shù)已經(jīng)是全局的了,不需要再添加 extern 畫蛇添足。
  2. SETGATE 如何使用?
    參見 inc/mmu.h 中的函數(shù)定義。
#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;       
}

gate
這是一個(gè) struct Gatedesc。
istrap
該中斷是 trap(exception) 則為1,是 interrupt 則為0。
sel
代碼段選擇器。進(jìn)入內(nèi)核的話是 GD_KT。
off
相對(duì)于段的偏移,簡單來說就是函數(shù)地址。
dpl(Descriptor Privileged Level)
權(quán)限描述符。

Question 1
What is the purpose of having an individual handler function for each exception/interrupt? (i.e., if all exceptions/interrupts were delivered to the same handler, what feature that exists in the current implementation could not be provided?)

每個(gè)異常和中斷處理方式不同,例如 除0 異常不會(huì)返回程序繼續(xù)執(zhí)行,而 I/O 操作中斷會(huì)返回程序繼續(xù)執(zhí)行。用一個(gè)handler難以實(shí)現(xiàn)。

Question 2
Did you have to do anything to make the user/softint program behave correctly? The grade script expects it to produce a general protection fault (trap 13), but softint's code says int $14. Why should this produce interrupt vector 13? What happens if the kernel actually allows softint's int $14 instruction to invoke the kernel's page fault handler (which is interrupt vector 14)?

user/softint.c內(nèi)容如下:

// buggy program - causes an illegal software interrupt

#include <inc/lib.h>

void
umain(int argc, char **argv)
{
    asm volatile("int $14");    // page fault
}

grade-lab3中對(duì)應(yīng)的評(píng)分標(biāo)準(zhǔn)如下:

@test(10)
def test_softint():
    r.user_test("softint")
    r.match('Welcome to the JOS kernel monitor!',
            'Incoming TRAP frame at 0xefffffbc',
            'TRAP frame at 0xf.......',
            '  trap 0x0000000d General Protection',
            '  eip  0x008.....',
            '  ss   0x----0023',
            '.00001000. free env 0000100')

可以看出,該程序代碼中希望能產(chǎn)生一個(gè)缺頁異常(int $14),實(shí)際上評(píng)判卻說明產(chǎn)生的是通用保護(hù)異常(int $13)。這是因?yàn)槟壳跋到y(tǒng)運(yùn)行在用戶態(tài),權(quán)限級(jí)別為 3,而 INT 指令是系統(tǒng)指令,權(quán)限級(jí)別為 0,因此會(huì)首先引發(fā) Gerneral Protection Excepetion。即 trap 13。

Part B: 缺頁錯(cuò)誤,斷點(diǎn)異常以及系統(tǒng)調(diào)用


處理缺頁錯(cuò)誤

缺頁錯(cuò)誤異常,中斷向量 14 (T_PGFLT),是一個(gè)非常重要的異常類型,lab3 以及 lab4 都強(qiáng)烈依賴于這個(gè)異常處理。當(dāng)程序遇到缺頁異常時(shí),它將引起異常的虛擬地址存入 CR2 控制寄存器( control register)。在 trap.c 中,我們已經(jīng)提供了page_fault_handler() 函數(shù)用來處理缺頁異常。

Exercise 5.
Modify trap_dispatch() to dispatch page fault exceptions to page_fault_handler(). You should now be able to get make grade to succeed on the faultread, faultreadkernel, faultwrite, and faultwritekernel tests. If any of them don't work, figure out why and fix them. Remember that you can boot JOS into a particular user program using make run-x or make run-x-nox.

較為簡單,實(shí)際上就是在trap_dispatch()中根據(jù) trap number 進(jìn)行一個(gè)處理分配。目前只需要加入缺頁異常即可完成該 exercise。

static void
trap_dispatch(struct Trapframe *tf)
{
    // Handle processor exceptions.
    // LAB 3: Your code here.
    switch (tf->tf_trapno) {
        case T_PGFLT:
            page_fault_handler(tf);
            break;
        default:
        // Unexpected trap: The user process or the kernel has a bug.
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
    }
}

在后續(xù)的程序中,還會(huì)對(duì)缺頁異常的處理進(jìn)行完善。

斷點(diǎn)異常

斷點(diǎn)異常,中斷向量 3 (T_BRKPT) 允許調(diào)試器給程序加上斷點(diǎn)。原理是暫時(shí)把程序中的某個(gè)指令替換為一個(gè) 1 字節(jié)大小的 int3軟件中斷指令。在 JOS 中,我們將它實(shí)現(xiàn)為一個(gè)偽系統(tǒng)調(diào)用。這樣,任何程序(不限于調(diào)試器)都能使用斷點(diǎn)功能。

Exercise 6.
Modify trap_dispatch() to make breakpoint exceptions invoke the kernel monitor. You should now be able to get make grade to succeed on the breakpoint test.

跟之前的練習(xí)實(shí)現(xiàn)方法是一樣的。另外需要找到在 kern/monitor.c 中的 void monitor(struct TrapFrame *tf)函數(shù)。改寫 trap_dispatch 函數(shù),加入斷點(diǎn)處理。

static void
trap_dispatch(struct Trapframe *tf)
{
    // Handle processor exceptions.
    // LAB 3: Your code here.
    switch (tf->tf_trapno) {
        case T_PGFLT:
            page_fault_handler(tf);
            break;
        case T_BRKPT:
            monitor(tf);
            break;
        default:
        // Unexpected trap: The user process or the kernel has a bug.
        print_trapframe(tf);
        if (tf->tf_cs == GD_KT)
            panic("unhandled trap in kernel");
        else {
            env_destroy(curenv);
            return;
        }
    }
}

第一次運(yùn)行發(fā)現(xiàn)并沒有通過檢驗(yàn),報(bào)的是通用保護(hù)異常。一看是權(quán)限問題。把 Exercise 4 中的

SETGATE(idt[T_BRKPT], 1, GD_KT, handler3, 0);

改為:

SETGATE(idt[T_BRKPT], 1, GD_KT, handler3, 3);

即可完成。

Question 3
The break point test case will either generate a break point exception or a general protection fault depending on how you initialized the break point entry in the IDT (i.e., your call to SETGATE from trap_init). Why? How do you need to set it up in order to get the breakpoint exception to work as specified above and what incorrect setup would cause it to trigger a general protection fault?

其實(shí)就是我描述的權(quán)限問題。

Question 4
What do you think is the point of these mechanisms, particularly in light of what the user/softint test program does?

inc/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
};

優(yōu)先級(jí)低的代碼無法訪問優(yōu)先級(jí)高的代碼,優(yōu)先級(jí)高低由 gd_dpl 判斷。數(shù)字越小越高。

系統(tǒng)調(diào)用

用戶進(jìn)程通過系統(tǒng)調(diào)用來讓內(nèi)核為他們服務(wù)。當(dāng)用戶進(jìn)程召起一次系統(tǒng)調(diào)用,處理器將進(jìn)入內(nèi)核態(tài),處理器以及內(nèi)核合作存儲(chǔ)用戶進(jìn)程的狀態(tài),內(nèi)核將執(zhí)行適當(dāng)?shù)拇a來完成系統(tǒng)調(diào)用,最后返回用戶進(jìn)程繼續(xù)執(zhí)行。實(shí)現(xiàn)細(xì)節(jié)各個(gè)系統(tǒng)有所不同。
JOS 內(nèi)核使用 int 指令來觸發(fā)一個(gè)處理器中斷。特別的,我們使用 int $0x30 作為系統(tǒng)調(diào)用中斷。它并不能由硬件產(chǎn)生,因此使用它不會(huì)產(chǎn)生歧義。
應(yīng)用程序會(huì)把系統(tǒng)調(diào)用號(hào) (與中斷向量不是一個(gè)東西) 以及系統(tǒng)調(diào)用參數(shù)傳遞給寄存器。這樣,內(nèi)核就不用在用戶?;蛘咧噶盍骼锊樵冞@些信息。系統(tǒng)調(diào)用號(hào)將存放于%eax,參數(shù)(至多5個(gè))會(huì)存放于%edx, %ecx,%ebx, %edi 以及 %esi,調(diào)用結(jié)束后,內(nèi)核將返回值放回到%eax。之所以用 %eax 來傳遞返回值,是由于系統(tǒng)調(diào)用導(dǎo)致了棧的切換。

Exercise 7.
Add a handler in the kernel for interrupt vector T_SYSCALL. You will have to edit kern/trapentry.S and kern/trap.c's trap_init(). You also need to change trap_dispatch() to handle the system call interrupt by calling syscall() (defined in kern/syscall.c) with the appropriate arguments, and then arranging for the return value to be passed back to the user process in %eax. Finally, you need to implement syscall() in kern/syscall.c. Make sure syscall() returns -E_INVAL if the system call number is invalid. You should read and understand lib/syscall.c (especially the inline assembly routine) in order to confirm your understanding of the system call interface. Handle all the system calls listed in inc/syscall.h by invoking the corresponding kernel function for each call.

又是一個(gè)比較燒腦的練習(xí),kern 中有一套 syscall.h syscall.c,inclib中又有一套syscall.h syscall.c。需要理清這兩者之間的關(guān)系。
inc/syscall.h

#ifndef JOS_INC_SYSCALL_H
#define JOS_INC_SYSCALL_H

/* system call numbers */
enum {
    SYS_cputs = 0,
    SYS_cgetc,
    SYS_getenvid,
    SYS_env_destroy,
    NSYSCALLS
};

#endif /* !JOS_INC_SYSCALL_H */

這個(gè)頭文件主要定義了系統(tǒng)調(diào)用號(hào),實(shí)際就是一個(gè) enum 而已。

lib/syscall.c

// System call stubs.

#include <inc/syscall.h>
#include <inc/lib.h>

static inline int32_t
syscall(int num, int check, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
    int32_t ret;

    // Generic system call: pass system call number in AX,
    // up to five parameters in DX, CX, BX, DI, SI.
    // Interrupt kernel with T_SYSCALL.
    //
    // The "volatile" tells the assembler not to optimize
    // this instruction away just because we don't use the
    // return value.
    //
    // The last clause tells the assembler that this can
    // potentially change the condition codes and arbitrary
    // memory locations.

    asm volatile("int %1\n"
             : "=a" (ret)
             : "i" (T_SYSCALL),
               "a" (num),
               "d" (a1),
               "c" (a2),
               "b" (a3),
               "D" (a4),
               "S" (a5)
             : "cc", "memory");

    if(check && ret > 0)
        panic("syscall %d returned %d (> 0)", num, ret);

    return ret;
}

void
sys_cputs(const char *s, size_t len)
{
    syscall(SYS_cputs, 0, (uint32_t)s, len, 0, 0, 0);
}

int
sys_cgetc(void)
{
    return syscall(SYS_cgetc, 0, 0, 0, 0, 0, 0);
}

int
sys_env_destroy(envid_t envid)
{
    return syscall(SYS_env_destroy, 1, envid, 0, 0, 0, 0);
}

envid_t
sys_getenvid(void)
{
     return syscall(SYS_getenvid, 0, 0, 0, 0, 0, 0);
}

這是系統(tǒng)調(diào)用的通用模板,不同的系統(tǒng)調(diào)用 (例如sys_cputs, sys_cgetc) 都會(huì)以不同參數(shù)調(diào)用 syscall 函數(shù)。為了了解 syscall 函數(shù)到底做了什么,需要看懂其中的內(nèi)聯(lián)匯編部分。

補(bǔ)充知識(shí):GCC內(nèi)聯(lián)匯編
其語法固定為:
asm volatile (“asm code”:output:input:changed);

    asm volatile("int %1\n"
             : "=a" (ret)
             : "i" (T_SYSCALL),
               "a" (num),
               "d" (a1),
               "c" (a2),
               "b" (a3),
               "D" (a4),
               "S" (a5)
             : "cc", "memory");
限定符 意義
"m"、"v"、"o" 內(nèi)存單元
"r" 任何寄存器
"q" 寄存器eax、ebx、ecx、edx之一
"i"、"h" 直接操作數(shù)
"E"、"F" 浮點(diǎn)數(shù)
"g" 任意
"a"、"b"、"c"、"d" 分別表示寄存器eax、ebx、ecx和edx
"S"、"D" 寄存器esi、edi
"I" 常數(shù) (0至31)

除了這些約束之外, 輸出值還包含一個(gè)約束修飾符:

輸出修飾符 描述
+ 可以讀取和寫入操作數(shù)
= 只能寫入操作數(shù)
% 如果有必要操作數(shù)可以和下一個(gè)操作數(shù)切換
& 在內(nèi)聯(lián)函數(shù)完成之前, 可以刪除和重新使用操作數(shù)

根據(jù)表格內(nèi)容,可以看出該內(nèi)聯(lián)匯編作用就是引發(fā)一個(gè)int中斷,中斷向量為立即數(shù) T_SYSCALL,同時(shí),對(duì)寄存器進(jìn)行操作。看懂這,就清楚了,這一部分應(yīng)該不需要我們改動(dòng),因?yàn)槲覀兲幚淼氖侵袛嘁呀?jīng)產(chǎn)生后的部分。當(dāng)然,還有另一種更簡單的思路,inc/ 目錄下的,其實(shí)都是操作系統(tǒng)留給用戶的接口,所以才會(huì)在里面看到 stdio.h,assert.h 等文件。那么,要進(jìn)行系統(tǒng)調(diào)用肯定也是先調(diào)用 inc/ 中的那個(gè),具體處理應(yīng)該是在 kern/ 中實(shí)現(xiàn)。

kern/trap.c
首先不要忘記在 trap_init 中設(shè)置好入口,并且權(quán)限設(shè)為3,使得用戶進(jìn)程能夠產(chǎn)生這個(gè)中斷。

    SETGATE(idt[T_SYSCALL], 0, GD_KT, handler48, 3);

另外就是 trap_dispatch 函數(shù)中加入相應(yīng)的處理方法:

        case T_SYSCALL:
            tf->tf_regs.reg_eax = syscall(tf->tf_regs.reg_eax, 
                            tf->tf_regs.reg_edx,
                            tf->tf_regs.reg_ecx,
                            tf->tf_regs.reg_ebx,
                            tf->tf_regs.reg_edi,
                            tf->tf_regs.reg_esi);
            break;

由于已經(jīng)通過 lib/syscall.c 處理,tf 結(jié)構(gòu)體中存儲(chǔ)的寄存器狀態(tài)已經(jīng)記錄了系統(tǒng)調(diào)用號(hào),系統(tǒng)調(diào)用參數(shù)等等?,F(xiàn)在我們就可以利用這些信息調(diào)用 kern/syscall.c 中的函數(shù)了。

kern/syscall.c

我們?cè)?kern/trap.c 中調(diào)用的實(shí)際上就是這里的 syscall 函數(shù),而不是 lib/syscall.c 中的那個(gè)。想明白這一點(diǎn),設(shè)置參數(shù)也就很簡單了,注意返回值的處理。

int32_t
syscall(uint32_t syscallno, uint32_t a1, uint32_t a2, uint32_t a3, uint32_t a4, uint32_t a5)
{
    // Call the function corresponding to the 'syscallno' parameter.
    // Return any appropriate return value.
    // LAB 3: Your code here.

    // panic("syscall not implemented");
    
    int32_t retVal = 0;
    switch (syscallno) {
    case SYS_cputs:
        sys_cputs((const char *)a1, a2);
        break;
    case SYS_cgetc:
        retVal = sys_cgetc();
        break;
    case SYS_env_destroy:
        retVal = sys_env_destroy(a1);
        break;
    case SYS_getenvid:
        retVal = sys_getenvid() >= 0;
        break;
    default:
        retVal = -E_INVAL;
    }
    return retVal;
}

至此,本 exercise 結(jié)束,運(yùn)行 make grade 可以通過 testbss,運(yùn)行 make run-hello 可以打印出 hello world,緊接著提示了頁錯(cuò)誤。
通過 exercise 7,可以看出 JOS系 統(tǒng)調(diào)用的步驟為:

  1. 用戶進(jìn)程使用 inc/ 目錄下暴露的接口
  2. lib/syscall.c 中的函數(shù)將系統(tǒng)調(diào)用號(hào)及必要參數(shù)傳給寄存器,并引起一次 int $0x30 中斷
  3. kern/trap.c 捕捉到這個(gè)中斷,并將 TrapFrame 記錄的寄存器狀態(tài)作為參數(shù),調(diào)用處理中斷的函數(shù)
  4. kern/syscall.c 處理中斷

用戶進(jìn)程啟動(dòng)

用戶進(jìn)程從 lib/entry.S 開始運(yùn)行。經(jīng)過一些設(shè)置,調(diào)用了 lib/libmain.c 下的 libmain() 函數(shù)。在 libmain() 中,我們需要把全局指針 thisenv 指向該程序在 envs[] 數(shù)組中的位置。
libmain() 會(huì)調(diào)用 umain,即用戶進(jìn)程的main函數(shù)。在user/hello.c中,可以看到其內(nèi)容為:

void
umain(int argc, char **argv)
{
    cprintf("hello, world\n");
    cprintf("i am environment %08x\n", thisenv->env_id);  // 之前就在這里報(bào)錯(cuò),因?yàn)閠hisenv = 0
}

在 Exercise 8 中,我們將設(shè)置好 thisenv,這樣就能正常運(yùn)行用戶進(jìn)程了。這也是我們第一次用到內(nèi)存的 UENVS 區(qū)域。

Exercise 8.
Add the required code to the user library, then boot your kernel. You should see user/hello print "hello, world" and then print "i am environment 00001000". user/hello then attempts to "exit" by calling sys_env_destroy() (see lib/libmain.c and lib/exit.c). Since the kernel currently only supports one user environment, it should report that it has destroyed the only environment and then drop into the kernel monitor. You should be able to get make grade to succeed on the hello test.

原以為是個(gè)很簡單的練習(xí),然而我代碼寫好了卻無法運(yùn)行成功。這個(gè)練習(xí)重在檢查以前的代碼,之前很多代碼雖然通過了 make grade,卻不一定正確。
lib/libmain.c 中把 thisenv = 0 改為:

    thisenv = &envs[ENVX(sys_getenvid())];

即可通過。記錄我自己犯的錯(cuò)誤如下:

  1. kern/syscall.c 的函數(shù) syscall() 中:
    case SYS_getenvid:
        // retVal = sys_getenvid() >= 0; 錯(cuò)誤,應(yīng)該返回獲取的id
        // 返回值不僅是用于判斷執(zhí)行成功與否,也可能攜帶信息
        retVal = sys_getenvid();
        break;
  1. kern/env.c 的函數(shù) region_alloc 中,我原先的寫法為:
static void
region_alloc(struct Env *e, void *va, size_t len)
{
    
    size_t pgnum = ROUNDUP(len, PGSIZE) / PGSIZE;
    uintptr_t va_start = ROUNDDOWN((uintptr_t)va, PGSIZE);
    struct PageInfo *pginfo = NULL;
    for (size_t i=0; i<pgnum; i++) {
        pginfo = page_alloc(0);
        if (! pginfo) {
            int r = -E_NO_MEM;
            panic("region_alloc: %e" , r);
        }
        int r = page_insert(e->env_pgdir, pginfo, (void *)va_start, PTE_W | PTE_U | PTE_P);
        if (r < 0) {
            panic("region_alloc: %e" , r);
        }
        va_start += PGSIZE;
    }
}

大致思想是先根據(jù) len 求出要插入多少頁,再挨個(gè)插入。但是這樣存在一個(gè)極大的隱患,在這個(gè) Exercise 8 中暴露出來了。錯(cuò)誤情況為:

Incoming TRAP frame at 0xf0106600
Incoming TRAP frame at 0xf0106588
Incoming TRAP frame at 0xf0106510
Incoming TRAP frame at 0xf0106498
Incoming TRAP frame at 0xf0106420
...
qemu: fatal: Trying to execute code outside RAM or ROM at 0xf00b80a0

將分配過程輸出后可以看到問題關(guān)鍵:

Allocate size: 00000fe4, Start from: 00800020
page size = round_up(4068 / 4096) = 1
insert page at 00800000
region allocation completed...

可以看出,這里只插入了一頁,然而實(shí)際上應(yīng)該要插入兩頁才合適。因?yàn)轫撁鎸?duì)齊不是根據(jù)某個(gè)地址來做的,而是對(duì)整個(gè)內(nèi)存的對(duì)齊,類似于 0x008000200x00801014這段內(nèi)存,雖然長度不足一頁,但實(shí)際上橫跨了 0x00800000~0x00801000 以及 0x00801000~0x00802000 ,而按照之前的寫法,沒有插入 0x00801000~0x00802000 這一段內(nèi)存,必然會(huì)導(dǎo)致出錯(cuò)。分析出原因,改動(dòng)就很容易了,將實(shí)現(xiàn)改為根據(jù)最初地址和最終地址來進(jìn)行對(duì)齊即可。

static void
region_alloc(struct Env *e, void *va, size_t len)
{

    uintptr_t va_start = ROUNDDOWN((uintptr_t)va, PGSIZE);
    uintptr_t va_end = ROUNDUP((uintptr_t)va + len, PGSIZE);
    struct PageInfo *pginfo = NULL;
    for (int cur_va=va_start; cur_va<va_end; cur_va+=PGSIZE) {
        pginfo = page_alloc(0);
        if (!pginfo) {
            int r = -E_NO_MEM;
            panic("region_alloc: %e" , r);
        }
        cprintf("insert page at %08x\n",cur_va);
        page_insert(e->env_pgdir, pginfo, (void *)cur_va, PTE_U | PTE_W | PTE_P);
    }
}

經(jīng)過一系列的改動(dòng),運(yùn)行成功。自己挖的坑還是得自己填。

頁錯(cuò)誤 & 內(nèi)存保護(hù)

內(nèi)存保護(hù)是操作系統(tǒng)的關(guān)鍵功能,它確保了一個(gè)程序中的錯(cuò)誤不會(huì)導(dǎo)致其他程序或是操作系統(tǒng)自身的崩潰。
操作系統(tǒng)通常依賴硬件的支持來實(shí)現(xiàn)內(nèi)存保護(hù)。操作系統(tǒng)會(huì)告訴硬件哪些虛擬地址可用哪些不可用。當(dāng)某個(gè)程序想訪問不可用的內(nèi)存地址或不具備權(quán)限時(shí),處理器將在出錯(cuò)指令處停止程序,然后陷入內(nèi)核。如果錯(cuò)誤可以處理,內(nèi)核就處理并恢復(fù)程序運(yùn)行,否則無法恢復(fù)。
作為可以修復(fù)的錯(cuò)誤,設(shè)想某個(gè)自動(dòng)生長的棧。在許多系統(tǒng)中內(nèi)核首先分配一個(gè)頁面給棧,如果某個(gè)程序訪問了頁面外的空間,內(nèi)核會(huì)自動(dòng)分配更多頁面以讓程序繼續(xù)。這樣,內(nèi)核只用分配程序需要的棧內(nèi)存給它,然而程序感覺仿佛可以擁有任意大的棧內(nèi)存。
系統(tǒng)調(diào)用也為內(nèi)存保護(hù)帶來了有趣的問題。許多系統(tǒng)調(diào)用接口允許用戶傳遞指針給內(nèi)核,這些指針指向待讀寫的用戶緩沖區(qū)。內(nèi)核處理系統(tǒng)調(diào)用的時(shí)候會(huì)對(duì)這些指針解引用。這樣就帶來了兩個(gè)問題:

  1. 內(nèi)核的頁錯(cuò)誤通常比用戶進(jìn)程的頁錯(cuò)誤嚴(yán)重得多,如果內(nèi)核在操作自己的數(shù)據(jù)結(jié)構(gòu)時(shí)發(fā)生頁錯(cuò)誤,這就是一個(gè)內(nèi)核bug,會(huì)引起系統(tǒng)崩潰。因此,內(nèi)核需要記住這個(gè)錯(cuò)誤是來自用戶進(jìn)程。
  2. 內(nèi)核比用戶進(jìn)程擁有更高的內(nèi)存權(quán)限,用戶進(jìn)程給內(nèi)核傳遞的指針可能指向一個(gè)只有內(nèi)核能夠讀寫的區(qū)域,內(nèi)核必須謹(jǐn)慎避免解引用這類指針,因?yàn)檫@樣可能導(dǎo)致內(nèi)核的私有信息泄露或破壞內(nèi)核完整性。

我們將對(duì)用戶進(jìn)程傳給內(nèi)核的指針做一個(gè)檢查來解決這兩個(gè)問題。內(nèi)核將檢查指針指向的是內(nèi)存中用戶空間部分,頁表也允許內(nèi)存操作。

Exercise 9.
Change kern/trap.c to panic if a page fault happens in kernel mode.
Hint: to determine whether a fault happened in user mode or in kernel mode, check the low bits of the tf_cs.
Read user_mem_assert in kern/pmap.c and implement user_mem_check in that same file.
Change kern/syscall.c to sanity check arguments to system calls.
Boot your kernel, running user/buggyhello. The environment should be destroyed, and the kernel should not panic. You should see:

[00001000] user_mem_check assertion failure for va 00000001
[00001000] free env 00001000
Destroyed the only environment - nothing more to do!

Finally, change debuginfo_eip in kern/kdebug.c to call user_mem_check on usd, stabs, and stabstr.

kern/trap.c 中加入判斷頁錯(cuò)誤來源。原理見 IDT 表部分的講解。

void
page_fault_handler(struct Trapframe *tf)
{
    uint32_t fault_va;

    // Read processor's CR2 register to find the faulting address
    fault_va = rcr2();

    // Handle kernel-mode page faults.

    // LAB 3: Your code here.
    // 在這里判斷 cs 的低 2bit
    if ((tf->tf_cs & 3) == 0) panic("Page fault in kernel-mode");

    // We've already handled kernel-mode exceptions, so if we get here,
    // the page fault happened in user mode.

    // Destroy the environment that caused the fault.
    cprintf("[%08x] user fault va %08x ip %08x\n",
        curenv->env_id, fault_va, tf->tf_eip);
    print_trapframe(tf);
    env_destroy(curenv);
}

kern/pmap.c 中修改檢查用戶內(nèi)存的部分。需要注意的是由于需要存儲(chǔ)第一個(gè)訪問出錯(cuò)的地址,va 所在的頁面需要單獨(dú)處理一下,不能直接對(duì)齊。

int
user_mem_check(struct Env *env, const void *va, size_t len, int perm)
{
    // LAB 3: Your code here.
    uintptr_t start_va = ROUNDDOWN((uintptr_t)va, PGSIZE);
    uintptr_t end_va = ROUNDUP((uintptr_t)va + len, PGSIZE);
    for (uintptr_t cur_va=start_va; cur_va<end_va; cur_va+=PGSIZE) {
        pte_t *cur_pte = pgdir_walk(env->env_pgdir, (void *)cur_va, 0);
        if (cur_pte == NULL || (*cur_pte & (perm|PTE_P)) != (perm|PTE_P) || cur_va >= ULIM) {
            if (cur_va == start_va) {
                user_mem_check_addr = (uintptr_t)va;
            } else {
                user_mem_check_addr = cur_va;
            }
            return -E_FAULT;
        }
    }
    return 0;
}

kern/syscall.c 中的輸出字符串部分加入內(nèi)存檢查。

static void
sys_cputs(const char *s, size_t len)
{
    // Check that the user has permission to read memory [s, s+len).
    // Destroy the environment if not.

    // LAB 3: Your code here.
    user_mem_assert(curenv, s, len, PTE_U);
    // Print the string supplied by the user.
    cprintf("%.*s", len, s);
}

kern/kdebug.c 中的 debuginfo_eip 函數(shù)中加入內(nèi)存檢查。

        // Make sure this memory is valid.
        // Return -1 if it is not.  Hint: Call user_mem_check.
        // LAB 3: Your code here.
        if (user_mem_check(curenv, (void *)usd, sizeof(struct UserStabData), PTE_U) < 0) {
            return -1;
        }
...
        // Make sure the STABS and string table memory is valid.
        // LAB 3: Your code here.
        if (user_mem_check(curenv, (void *)stabs, stab_end-stabs, PTE_U) < 0) {
            return -1;
        }
        if (user_mem_check(curenv, (void *)stabstr, stabstr_end-stabstr, PTE_U) < 0) {
            return -1;
        }

Question
If you now run user/breakpoint, you should be able to run backtrace from the kernel monitor and see the backtrace traverse into lib/libmain.c before the kernel panics with a page fault. What causes this page fault? You don't need to fix it, but you should understand why it happens.

運(yùn)行 make run-breakpointbacktrace 得到輸出:

Stack backtrace:
         ebp efffff20  eip f0100a75  args 00000001 efffff38 f01b4000 00000000 f0172840
                 kern/monitor.c:187: monitor+276
         ebp efffff90  eip f0103833  args f01b4000 efffffbc f0105c64 00000082 00000000
                 kern/trap.c:196: trap+169
         ebp efffffb0  eip f010393b  args efffffbc 00000000 00000000 eebfdfd0 efffffdc
                 kern/syscall.c:68: syscall+0
         ebp eebfdfd0  eip 800073  args 00000000 00000000 eebfdff0 00800049 00000000
                 lib/libmain.c:27: libmain+58
Incoming TRAP frame at 0xeffffeac
kernel panic at kern/trap.c:268: Page fault in kernel-mode

從輸出的 ebp 寄存器內(nèi)容可以看出,efffff20, efffff90, efffffb0都位于內(nèi)核棧上,僅有 eebfdfd0 位于用戶棧上。
這是一個(gè)較難的問題,要想清楚搞明白,自然要依靠 gdb。這里使用需要一點(diǎn)技巧。
首先打開終端輸入

make run-breakpoint-gdb

另開一個(gè)終端輸入

make gdb

再設(shè)置斷點(diǎn)為斷點(diǎn)異常調(diào)用函數(shù)中的一句

(gdb) b kern/monitor.c:179

首先回憶一下,%ebp 中實(shí)際存放的是一個(gè)地址,該地址前后存放了許多關(guān)鍵信息。例如:

(gdb) x/8x 0xefffffb0
0xefffffb0: 0xeebfdfd0  0xf010393b  0xefffffbc  0x00000000
0xefffffc0: 0x00000000  0xeebfdfd0  0xefffffdc  0x00000000

分別是:

調(diào)用者ebp   返回地址eip  參數(shù)1  參數(shù)2
參數(shù)3       參數(shù)4        參數(shù)5  ...

查看 eebfdfd0 至用戶棧頂 eebfe000 之間 12 個(gè)字節(jié)里到底是什么內(nèi)容:

(gdb) x/12x 0xeebfdfd0
0xeebfdfd0: 0xeebfdff0  0x00800073  0x00000000  0x00000000
0xeebfdfe0: 0xeebfdff0  0x00800049  0x00000000  0x00000000
0xeebfdff0: 0x00000000  0x00800031  0x00000000  0x00000000

可以看出,按照 backtrace 的邏輯 (參見lab 1),每次希望打印出5個(gè)參數(shù),下一次期望打印出

ebp eebfdff0  eip 800031  args 00000000 00000000 (此后的內(nèi)存地址已越界)

為了證明這個(gè)猜想,修改kern/monitor.c 將打印參數(shù)改為兩個(gè)

/*
cprintf("\tebp %x  eip %x  args %08x %08x %08x %08x %08x\n", ebp, ptr_ebp[1], ptr_ebp[2], ptr_ebp[3], ptr_ebp[4], ptr_ebp[5], ptr_ebp[6]);
*/
cprintf("\tebp %x  eip %x  args %08x %08x \n", ebp, ptr_ebp[1], ptr_ebp[2], ptr_ebp[3]);

再進(jìn)行測(cè)試,則得出:

Stack backtrace:
         ebp efffff20  eip f0100a6f  args 00000001 efffff38
                 kern/monitor.c:190: monitor+276
         ebp efffff90  eip f010382d  args f01b4000 efffffbc
                 kern/trap.c:196: trap+169
         ebp efffffb0  eip f0103935  args efffffbc 00000000
                 kern/syscall.c:68: syscall+0
         ebp eebfdfd0  eip 800073  args 00000000 00000000
                 lib/libmain.c:27: libmain+58
         ebp eebfdff0  eip 800031  args 00000000 00000000
                 lib/entry.S:34: <unknown>+0
K> 

沒有再出現(xiàn) page fault,猜想正確。造成該現(xiàn)象的原因是 lib/entry.S

// Entrypoint - this is where the kernel (or our parent environment)
// starts us running when we are initially loaded into a new environment.
.text
.globl _start
_start:
    // See if we were started with arguments on the stack
    cmpl $USTACKTOP, %esp
    jne args_exist

    // If not, push dummy argc/argv arguments.
    // This happens when we are loaded by the kernel,
    // because the kernel does not know about passing arguments.
    pushl $0
    pushl $0

args_exist:
    call libmain
1:  jmp 1b

這里通過 %esp 位置來判斷是否是內(nèi)核載入的用戶環(huán)境。因?yàn)橹挥幸粋€(gè)用戶環(huán)境,所以在載入之初用戶棧為空,%esp 最初肯定指向USTACKTOP。如果是這樣,壓入兩個(gè)假參數(shù),此后調(diào)用 libmain。這就導(dǎo)致了輸出大于 2 個(gè)參數(shù)就出現(xiàn)頁錯(cuò)誤。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容