uCore lab1 操作系統實驗 challenge

uCore lab1-challenge

我胡漢三又回來了!!!
怎麼可以滿足於10/40呢!!!
接受挑戰,封印解除!!!

擴展練習 Challenge 1

擴展proj4,增加syscall功能,即增加一用戶態函數(可執行一特定系統調用:獲得時鐘計數 值),當內核初始完畢後,可從內核態返回到用戶態的函數,而用戶態的函數又通過系統調用得到內核態的服務(通過網絡查詢所需信息,可找老師諮詢。如果完成,且有興趣做代替考試的實驗,可找老師商量)。需寫出詳細的設計和分析報告。完成出色的可獲得適當加分。

提示: 規範一下 challenge 的流程。

kern_init 調用 switch_test,該函數如下:

static void switch_test(void) { 
    print_cur_status(); // print 當前 cs/ss/ds 等寄存器狀態 
    cprintf("+++ switch to user mode +++\n"); 
    switch_to_user(); // switch to user mode 
    print_cur_status(); 
    cprintf("+++ switch to kernel mode +++\n"); 
    switch_to_kernel(); // switch to kernel mode 
    print_cur_status(); 
} 

主要要完成的代碼是在 trap 裏面處理,T_SWITCH_TO* 中斷,並設置好返回的狀態。

在 lab1 裏面完成代碼以後,執行 make grade 應該能夠評測結果是否正確。

實驗思路

這個擴展練習是要求設置兩個中斷處理程序。

一個是可以實現從內核態轉換爲用戶態的程序。另一個是實現從用戶態轉換到內核態的程序。

說白了就是要弄明白:

  • int指令和iret指令到底做了什麼
  • cpu是如何表示特權級狀態的

int指令進行下面一些步驟:(來自xv6中文文檔)

  • CPU根據中斷向量,從 IDT 中獲得第 n 箇中斷描述符,中斷描述符裏保存着中斷服務例程的段選擇子
  • CPU使用IDT查到的中斷服務例程的段選擇子從GDT中取得相應的段描述符,段描述符裏保存了中斷服務例程的段基址和屬性信息,此時CPU就得到了中斷服務例程的起始地址,並跳轉到該地址
  • CPU會根據CPL和中斷服務例程的段描述符的DPL(DPL 是描述符中記錄的特權級)信息確認是否發生了特權級的轉換。比如當前程序正運行在用戶態,而中斷程序是運行在內核態的,則意味着發生 了特權級的轉換,這時CPU會從當前程序的TSS信息(該信息在內存中的起始地址存在TR寄存器中)裏取得該程序的內核棧地址,即包括內核態的ss和esp的值,並立即將系統當前使用的棧切換成新的內核棧。這個棧就是即將運行的中斷服務程序要使用的棧
  • 緊接着就將當前程序使用的用戶態的ss和esp壓到新的內核棧中保存起來
  • 依次將%eflags %cs %eip errorCode壓棧
  • 置 %cs 和 %eip 爲描述符中的值,開始執行中斷程序

補充一些有關於TTS的信息:

TSS可以留在內存中的任何位置。 任務寄存器(TR)的特殊段寄存器包含一個段選擇器,該段選擇器指向駐留在GDT中的有效TSS段描述符。 因此,要使用TSS,必須在函數gdt_init中執行以下操作:

  • 在GDT中創建TSS描述符條目
  • 根據需要將足夠的信息添加到內存中的TSS
  • 用該段的段選擇器加載TR寄存器

TSS中有幾個字段,用於在特權級別發生更改時指定新的堆棧指針。 但是在我們的os內核中,只有字段SS0和ESP0是有用的。字段SS0包含CPL = 0的堆棧段選擇器,而ESP0包含CPL = 0的新ESP值。當在保護模式下發生中斷時,x86 CPU將在TSS中查找SS0和ESP0並加載其值 分別進入SS和ESP。

/* task state segment format (as described by the Pentium architecture book) */
struct taskstate {
    uint32_t ts_link;        // old ts selector
    uintptr_t ts_esp0;        // stack pointers and segment selectors
    uint16_t ts_ss0;        // after an increase in privilege level
    uint16_t ts_padding1;
    uintptr_t ts_esp1;
    uint16_t ts_ss1;
    uint16_t ts_padding2;
    uintptr_t ts_esp2;
    uint16_t ts_ss2;
    uint16_t ts_padding3;
    uintptr_t ts_cr3;        // page directory base
    uintptr_t ts_eip;        // saved state from last task switch
    uint32_t ts_eflags;
    uint32_t ts_eax;        // more saved state (registers)
    uint32_t ts_ecx;
    uint32_t ts_edx;
    uint32_t ts_ebx;
    uintptr_t ts_esp;
    uintptr_t ts_ebp;
    uint32_t ts_esi;
    uint32_t ts_edi;
    uint16_t ts_es;            // even more saved state (segment selectors)
    uint16_t ts_padding4;
    uint16_t ts_cs;
    uint16_t ts_padding5;
    uint16_t ts_ss;
    uint16_t ts_padding6;
    uint16_t ts_ds;
    uint16_t ts_padding7;
    uint16_t ts_fs;
    uint16_t ts_padding8;
    uint16_t ts_gs;
    uint16_t ts_padding9;
    uint16_t ts_ldt;
    uint16_t ts_padding10;
    uint16_t ts_t;            // trap on task switch
    uint16_t ts_iomb;        // i/o map base address
};

/* gdt_init - initialize the default GDT and TSS */
static void gdt_init(void) {
    // 設置TSS,以便在從用戶態切到內核態時能夠獲得正確的堆棧。 
    // 但是這裏並不安全,這只是一個臨時值,它將在lab2中設置爲KSTACKTOP
    ts.ts_esp0 = (uint32_t)&stack0 + sizeof(stack0);
    ts.ts_ss0 = KERNEL_DS;

    // 初始化gdt的TSS字段
    gdt[SEG_TSS] = SEG16(STS_T32A, (uint32_t)&ts, sizeof(ts), DPL_KERNEL);
    gdt[SEG_TSS].sd_s = 0;

    // 重新加載所有段寄存器
    lgdt(&gdt_pd);

    // 加載TSS
    ltr(GD_TSS);
}

說白了,開始就是要知道怎麼找中斷程序在哪:

  • 拿着中斷向量vector作爲索引,去查IDT
  • 查到了,誒,有段選子、屬性、偏移,但我不認得段選子啊;再拿着段選子去查GDT
  • 查到了,誒,找到段的基址,加上偏移

其中還有一個很重要的問題,那就是特權問題

產生中斷後,CPU一定不會將運行控制從高特權環轉向低特權環,特權級必須要麼保持不變(當操作系統內核自 己被中斷的時候),或被提升(當用戶態程序被中斷的時候)。無論哪一種情況,作爲結果的CPL(Current Privilege Level)必須等於目的代碼段的DPL。如果CPL發生了改變,一個堆棧切換操作(通過TSS完成)就會發生。

如果中斷是被用戶態程序中的指令所觸發的(比如軟件執行INT n生產的中斷),還會增加一個額外的檢查:門的DPL必須具有與CPL相同或更低的特權。這就防止了用戶代碼隨意觸發中斷。如果這些檢查失敗,會產生一個一般保護異常(general-protection exception)

特權級0-3,內核態0,用戶態3,數字越小,特權越高

至於iret指令的動作也是類似的,因爲int指令和iret指令是一對的,其指令的步驟如下:

  • 將 %eip %cs %eflags 彈棧
  • 若發生特權級轉換,將 %esp %ss 彈棧
  • 如果此次處理的是帶有錯誤碼(errorCode)的異常,要求相關的中斷服務例程 在調用iret返回之前添加出棧代碼主動彈出errorCode。

說了這麼多,還是虛虛的啊,那我們結合代碼說。

中斷處理實現

  • 外設基本初始化設置

Lab1實現了中斷初始化和對鍵盤、串口、時鐘外設進行中斷處理。

  • 中斷初始化設置

操作系統如果要正確處理各種不同的中斷事件,就需要安排應該由哪個中斷服務例程負責處理特定的中斷事件。系統將所有的中斷事件統一進行了編號(0~255),這個編號稱爲中斷向量。以ucore爲例,操作系統內核啓動以後,會通過 idt_init 函數初始化 idt 表 (參見trap.c),而其中 vectors 中存儲了中斷處理程序的入口地址。vectors 定義在 vector.S 文件中,通過一個工具程序 vector.c 生成。其中僅有 System call 中斷的權限爲用戶權限 (DPL_USER),即僅能夠使用 int 0x80 指令。此外還有對 tickslock 的初始化,該鎖用於處理時鐘中斷。

vector.S 文件通過 vectors.c 自動生成,其中定義了每個中斷的入口程序和入口地址 (保存在vectors 數組中)。其中,中斷可以分成兩類:一類是壓入錯誤編碼的 (error code),另一類不壓入錯誤編碼。對於第二類, vector.S 自動壓入一個 0。此外,還會壓入相應中斷的中斷號。在壓入兩個必要的參數之後,中斷處理函數跳轉到統一的入口 alltraps 處。

  • 中斷的處理過程

trap函數(定義在trap.c中)是對中斷進行處理的過程,所有的中斷在經過中斷入口函數 __alltraps預處理後 (定義在 trapasm.S中) ,都會跳轉到這裏。在處理過程中,根據不同的中斷類型,進行相應的處理。在相應的處理過程結束以後,trap將會返回,被中斷的程序會繼續運行。整個中斷處理流程大致如下:

(1)產生中斷後,CPU 跳轉到相應的中斷處理入口 (vectors)。如果特權級發生變化,必須將當前的ss和esp壓棧;然後是EFLAGS;清除標誌觸發器TF和IF;CS和EIP也跟着壓進去;接着在棧中壓入相應的error_code(是否存在與異常號相關) 以及 trap_no,然後跳轉到 alltraps 函數入口:

# 這是執行INT指令後的內核棧狀態
(high)
[..........]
[ss]
[esp]
[eflags]
[eip]		
[error_code](不一定存在)← esp
[..........]
(low)

# 這是trapframe
(high)
[..........]
[ss]
[esp]
[eflags]
[eip]		
[error_code](不一定存在)
[trap_no]
[ds]
[es]
[fs]
[gs]
[eax]
[ecx]
[edx]
[ebx]
[oesp]
[ebp]
[esi]
[edi]  ← esp
[..........]
(low)

在棧中保存當前被打斷程序的 trapframe結構(參見過程trapasm.S)。設置 kernel的數據段寄存器,最後壓入 esp,作爲 trap 函數參數(struct trapframe* tf)並跳轉到中斷處理函數 trap 處:

trapentry.s

# push registers to build a trap frame
# therefore make the stack look like a struct trapframe
pushl %ds
pushl %es
pushl %fs
pushl %gs
pushal

# load GD_KDATA into %ds and %es to set up data segments for kernel
movl $GD_KDATA, %eax
movw %ax, %ds
movw %ax, %es

# push %esp to pass a pointer to the trapframe as an argument to trap()
pushl %esp

# call trap(tf), where tf=%esp
call trap

# pop the pushed stack pointer
popl %esp

# return falls through to trapret...
.globl __trapret
__trapret:
# restore registers from stack
popal

# restore %ds, %es, %fs and %gs
popl %gs
popl %fs
popl %es
popl %ds

# get rid of the trap number and error code
addl $0x8, %esp
iret

trap.h

struct trapframe {
    // ------------low addr---------------
    struct pushregs tf_regs;
    uint16_t tf_gs;
    uint16_t tf_padding0;
    uint16_t tf_fs;
    uint16_t tf_padding1;
    uint16_t tf_es;
    uint16_t tf_padding2;
    uint16_t tf_ds;
    uint16_t tf_padding3;   
    uint32_t tf_trapno;
    uint32_t tf_err;
    uintptr_t tf_eip;
    uint16_t tf_cs;
    uint16_t tf_padding4;
    uint32_t tf_eflags;
    uintptr_t tf_esp;
    uint16_t tf_ss;
    uint16_t tf_padding5;
    // ------------high addr-------------
} __attribute__((packed));

熟悉嗎,朋友?

(2)詳細的中斷分類以及處理流程如下:根據中斷號對不同的中斷進行處理。其中,若中斷號是IRQ_OFFSET + IRQ_TIMER 爲時鐘中斷,則把ticks 將增加一。若中斷號是IRQ_OFFSET + IRQ_COM1 爲串口中斷,則顯示收到的字符。 若中斷號是IRQ_OFFSET + IRQ_KBD 爲 鍵盤中斷,則顯示收到的字符。若爲其他中斷且產生在內核狀態,則掛起系統。

(3)結束 trap 函數的執行後,通過 ret 指令返回到 alltraps 執行過程。從棧中恢復所有寄存器的值。調整 esp 的值:跳過棧中的 trap_no 與error_code,使esp指向中斷返回 eip,通過 iret 調用恢復 cs、eflag以及 eip,繼續執行。

坑點

32位保護模式下中斷髮生時的壓棧情況

中斷是可以在任何特權級別下發生的,不同特權級別下處理器使用不同的棧,如果涉及到特權級變化,還要壓入 SS 和 ESP 寄存器。

(1)當中斷髮生時,低特權級向高特權級轉化時的壓棧現象(用戶→內核)

我們是否能訪問這個目標段描述符,要做的就是將找到中斷描述符時當前的CPL與目標段描述符的DPL進行對比。

這裏我們討論的是CPL特權級比DPL低的情況,即數值上CPL > DPL。這表示我們要往高特權級棧上轉移,也意味着我們最後需要恢復舊棧,所以處理器先臨時保存一下舊棧的SS和ESP(SS是堆棧段寄存器,因爲換了一個棧,所以其也要變,ESP相當於在棧上的索引),然後加載新的特權級和DPL相同的段,將其加載到SS和ESP中,然後將之前保存的舊棧的SS和ESP壓到新棧中

(2)當中斷髮生時,無特權級轉化時的壓棧現象(內核→用戶)

此時由於不會切換棧,就不用保存SS和ESP

說白了:

  • 內核→用戶,不壓棧
  • 用戶→內核,壓棧

這種中斷返 回是用 iret 指令實現的。注意在返回的時候,其errorCode不會自動跳過,所以需要我們手動跳過。(險些trapentry.S幫我們寫了)

【指令手冊原文】

the IRET instruction pops the return instruction pointer, return code segment selector, and EFLAGS image from the stack to the EIP, CS, and EFLAGS registers, respectively, and then resumes execution of the interrupted program or procedure. If the return is to another privilege level, the IRET instruction also pops the stack pointer and SS from the stack, before resuming program execution.

翻譯:

IRET指令將返回指令指針,返回代碼段選子和EFLAGS鏡像分別從堆棧彈出到EIP,CS和EFLAGS寄存器,然後繼續執行被中斷的程序或過程。 如果返回到另一個特權級別,則IRET指令還會在繼續執行程序之前從堆棧中彈出堆棧指針和SS。

所以內核→用戶那邊需要我們自個兒壓!

代碼

因此,內核→用戶:

static void lab1_switch_to_user(void) {
    //LAB1 CHALLENGE 1 : TODO
	asm volatile (
        // 自己壓
	    "pushl %%ss \n"
        "pushl %%esp \n"
	    "int %0 \n"
	    "movl %%ebp, %%esp"
	    : 
	    : "i"(T_SWITCH_TOU)
	);
}

case T_SWITCH_TOU:
    if (tf->tf_cs != USER_CS) {
        tf->tf_cs = USER_CS;
        tf->tf_ds = tf->tf_es = tf->tf_ss = USER_DS;
        tf->tf_eflags |= FL_IOPL_MASK;
    }
    break;

用戶→內核:

static void lab1_switch_to_kernel(void) {
    //LAB1 CHALLENGE 1 :  TODO
	asm volatile (
	    "int %0 \n"
	    "movl %%ebp, %%esp \n"
	    : 
	    : "i"(T_SWITCH_TOK)
	);
}

case T_SWITCH_TOK:
    if (tf->tf_cs != KERNEL_CS) {
        tf->tf_cs = KERNEL_CS;
        tf->tf_ds = tf->tf_es = tf->tf_ss = KERNEL_DS;
        tf->tf_eflags &= ~FL_IOPL_MASK;
    }
    break;

跑一下

...
++ setup timer interrupts
0: @ring 0
0:  cs = 8
0:  ds = 10
0:  es = 10
0:  ss = 10
+++ switch to  user  mode +++
1: @ring 3
1:  cs = 1b
1:  ds = 23
1:  es = 23
1:  ss = 23
+++ switch to kernel mode +++
2: @ring 0
2:  cs = 8
2:  ds = 10
2:  es = 10
2:  ss = 10
100 ticks
100 ticks
100 ticks
...

OHHHHHHHHHHHHHHHHHHHHHHHHHHH!!!

查查分:

moocos-> make grade
Check Output:            (2.3s)
  -check ring 0:                             OK
  -check switch to ring 3:                   OK
  -check switch to ring 0:                   OK
  -check ticks:                              OK
Total Score: 40/40

OHHHHHHHHHHHHHHHHHHHHHHHHHHH!!!

擴展練習 Challenge 2

用鍵盤實現用戶模式內核模式切換。具體目標是:“鍵盤輸入3時切換到用戶模式,鍵盤輸入0時切換到內核模式”。 基本思路是借鑑軟中斷(syscall功能)的代碼,並且把trap.c中軟中斷處理的設置語句拿過來。

注意:

1.關於調試工具,不建議用lab1_print_cur_status()來顯示,要注意到寄存器的值要在中斷完成後tranentry.S裏面iret結束的時候才寫回,所以再trap.c裏面不好觀察,建議用print_trapframe(tf)

2.關於內聯彙編,最開始調試的時候,參數容易出現錯誤,可能的錯誤代碼如下

asm volatile ( "sub $0x8, %%esp \n" 
"int %0 \n" 
"movl %%ebp, %%esp" 
: ) 

要去掉參數int %0 \n這一行

3.軟中斷是利用了臨時棧來處理的,所以有壓棧和出棧的彙編語句。硬件中斷本身就在內核態了,直接處理就可以了。

實驗思路

在我的理解看來,其實trap,就是軟中斷;而臨時棧,應該說的就是trapframe這個數據結構了

我們再challenge1的基礎上,先嚐試着按一下:

kbd [048] 0
kbd [000] 
kbd [051] 3
kbd [000] 

這是他原來的代碼:

case IRQ_OFFSET + IRQ_KBD:
        c = cons_getc();
        cprintf("kbd [%03d] %c\n", c, c);
        break;

emmm。。。。。先到這吧,搞了好久,先休息~

待更新。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章