Operating System CSE 506 Lab 3 - Interrupts

閒聊

2020 年過去了,因為年底各種考試和工作, Lab3 下半部份延遲到現在才完成。雖然拖蠻久的,但是往好的方向看就是有在持續進行下去,沒有放棄就是好結局!最近心境上有蠻多變化的,其中本來對於目前工作內容很疑惑,覺得跟純軟生活落差太大,但是在工作中慢慢地發現自己對於 security 領域的認知嚴重不足,如果能好好學習 security 相關知識,對於自己和未來發展還是挺有幫助的,結合 security 與 embedded or cloud 去發展,是一個有趣又有挑戰性的目標,因此目前就朝著這方向努力,希望今年可以在 COSCUP 分享 security 相關議題。

Lab 3 Interrupts

在開始實作 interrupt 之前,先結合上一篇 CSE 506 Lab 3 - User Environments(Processes) environment(process) 的觀念,綜觀一下 interrupt 結合 environment context switch 的 interrupt handle 過程。

為了讓 interrupt handler 在安全獨立的環境執行,如果當前執行的環境是在 user mode,則會從 user stack context switch 到新的 privileged stack,把當前正在執行的 program state push 到 privileged stack 上,如果此 interrupt 有 error code,也會把 error code push 上去,接著執行 interrupt handler。

Some exceptions will push a 32-bit error code on to the top of the stack, which provides additional information about the error. This value must be pulled from the stack before returning control back to the currently running program. (i.e. before calling IRET)

interrupt handler 執行完後,privileged stack 上的 program state push 到 user stack 上,接著 context switch ,回復當前的 program state 繼續執行下去。

有了上述概念之後,我們就可以來實作 interrupt 流程。

Setup Interrupts

1. Setting Up the IDT

每個 interrupt 都有 idenifer,當 interrupt 觸發時,會透過查找 IDT (Interrupt Descriptor Table) 來取的對應的 interrupt handler address,因此我們需要先設定 IDT。

在 x86 中有多個預設好的 identifer,本次實驗中就先以這幾項為例子:

9.1 Identifying Interrupts

identifer description
0 Divide error
1 Debug exceptions
2 Nonmaskable interrupt
3 Breakpoint (one-byte INT 3 instruction)
4 Overflow (INTO instruction)
5 Bounds check (BOUND instruction)
6 Invalid opcode
7 Coprocessor not available
8 Double fault
9 (reserved)
10 Invalid TSS
11 Segment not present
12 Stack exception
13 General protection
14 Page fault
15 (reserved)
16 Coprecessor error
17-31 (reserved)
32-255 Available for external interrupts via INTR pin

首先先建立代表 IDT array。

1struct Gatedesc idt[256] = { { 0 } };

接著把定義好的 interrupt handler 放到對應的 IDT index 中。

 1// Set up a normal interrupt/trap gate descriptor.
 2// - istrap: 1 for a trap (= exception) gate, 0 for an interrupt gate.
 3// - sel: Code segment selector for interrupt/trap handler
 4// - off: Offset in code segment for interrupt/trap handler
 5// - dpl: Descriptor Privilege Level -
 6//	  the privilege level required for software to invoke
 7//	  this interrupt/trap gate explicitly using an int instruction.
 8#define SETGATE(gate, istrap, sel, off, dpl)			\
 9{	                        \
10    (gate).gd_off_15_0 = (uint64_t) (off) & 0xffff;		\
11	(gate).gd_ss = (sel);					\
12	(gate).gd_ist = 0;					\
13	(gate).gd_rsv1 = 0;					\
14	(gate).gd_type = (istrap) ? STS_TG64 : STS_IG64;	\
15	(gate).gd_s = 0;					\
16	(gate).gd_dpl = (dpl);					\
17	(gate).gd_p = 1;					\
18	(gate).gd_off_31_16 = ((uint64_t) (off) >> 16) & 0xffff;		\
19    (gate).gd_off_32_63 = ((uint64_t) (off) >> 32) & 0xffffffff;       \
20    (gate).gd_rsv2 = 0;               \
21}
22
23SETGATE(idt[T_DIVIDE], 0, GD_KT, DIVIDE_F, 0);
24SETGATE(idt[T_DEBUG], 0, GD_KT, DEBUG_F, 0);
25SETGATE(idt[T_NMI], 0, GD_KT, NMI_F, 0);
26SETGATE(idt[T_BRKPT], 0, GD_KT, BRKPT_F, 3);  // user mode
27SETGATE(idt[T_OFLOW], 0, GD_KT, OFLOW_F, 0);
28SETGATE(idt[T_BOUND], 0, GD_KT, BOUND_F, 0);
29SETGATE(idt[T_ILLOP], 0, GD_KT, ILLOP_F, 0);
30SETGATE(idt[T_DEVICE], 0, GD_KT, DEVICE_F, 0);
31SETGATE(idt[T_DBLFLT], 0, GD_KT, DBLFLT_F, 0);
32SETGATE(idt[T_TSS], 0, GD_KT, TSS_F, 0);
33SETGATE(idt[T_SEGNP], 0, GD_KT, SEGNP_F, 0);
34SETGATE(idt[T_STACK], 0, GD_KT, STACK_F, 0);
35SETGATE(idt[T_GPFLT], 0, GD_KT, GPFLT_F, 0);
36SETGATE(idt[T_PGFLT], 0, GD_KT, PGFLT_F, 0);
37SETGATE(idt[T_FPERR], 0, GD_KT, FPERR_F, 0);
38SETGATE(idt[T_ALIGN], 0, GD_KT, ALIGN_F, 0);
39SETGATE(idt[T_MCHK], 0, GD_KT, MCHK_F, 0);
40SETGATE(idt[T_SIMDERR], 0, GD_KT, SIMDERR_F, 0);
41SETGATE(idt[T_SYSCALL], 0, GD_KT, SYSCALL_F, 3); // user mode

T_DIVIDEDIVIDE_F 這類變數是在其他 file 中定義好的 identifer 和對應 interrupt handler function,就先不提。重點放在如何建立 IDT。我們在這邊使用到 SETGATE ,這個 macro 是用來對 IDT Descriptor 附上指定的數值,具體的 IDT Descriptor format 可以參考:

其中很重要的是 Descriptor Privilege Level(DPL) 這個參數,它代表這個 interrupt handler 能不能從 user space 呼叫,畢竟 user 如果能任意呼叫 interrupt,那就有可能對系統造成破壞。而從上面 code 中可以看到,我們允許 user mode 的 interrupt 包含:system call 和 breakpoint 這兩項。

接著使用 LIDT instruction 來配置 IDT register (IDTR)。

1struct Pseudodesc idt_pd = {0,0};
2
3idt_pd.pd_lim = sizeof(idt)-1;
4idt_pd.pd_base = (uint64_t)idt;
5
6// Load the IDT
7lidt(&idt_pd);

2. Set a stack for interrupt procedure

配置好 interrupt handler,接下來要考慮的問題是:interrupt 是在哪個 stack 上執行的?

在一開始有提到,為了確保 interrupt handler 的執行環境,我們會需要一個 privileged stack 來運行 handler,並且使用此 stack 來保存之前運行的狀態。而這個 stack address 資訊是存放在 TSS (Task State Segment) 的 stack segment,因此,我們看以下的 code:

 1void
 2trap_init_percpu(void)
 3{
 4    // Setup a TSS so that we get the right stack
 5    // when we trap to the kernel.
 6    ts.ts_esp0 = KSTACKTOP;
 7
 8    // Initialize the TSS slot of the gdt.
 9    SETTSS((struct SystemSegdesc64 *)((gdt_pd>>16)+40),STS_T64A, (uint64_t) (&ts),sizeof(struct Taskstate), 0);
10    // Load the TSS selector (like other segment selectors, the
11    // bottom three bits are special; we leave them 0)
12    ltr(GD_TSS0);
13}

可以看到 TSS 的配置,其中 stack pointer 被設置在 KSTACKTOP,這也意味著在 JOS 系統中,interrupt handler 都是在 kernel stack 上執行。

Example

1. Page fault in JOS lab 3

在有了一些概念之後,我們用更具體的例子來說明 JOS 中 interrupt 運作流程,就是開發者最熟悉的 page fault。從 user space 中讀取某個沒有與 physical memory 建立 mapping 的 virtual address,由於硬體找不到此 page,進而觸發 page fault interrupt。

上述為 JOS 觸發 page fault interrupt 過程,在例子當中,由於是 access 到沒有 mapping 的 virtual address,因此當前運行的 environment(process) 會被 free 掉。如果是一般的作業系統,在當前 process free 掉之後,就會切換到其他 process 繼續執行,不過目前 Lab 3 還沒有實作這個階段。

此外,JOS 中為了教學所需,JOS 的 interrupt handler 是使用 generic interrupt handler,在 handler 當中才去辨別不同的 interrupt identifer,並且分配給不同的 function 去執行。

在 page fault handler 中也可以看到一個 control register CR2,這是專門給 page-fault 使用,讓系統可以知道是哪一個 virtual address 不正確。

CR2 — Contains the page-fault linear address (the linear address that caused a page fault).

 1void
 2page_fault_handler(struct Trapframe *tf)
 3{
 4    uint64_t fault_va;
 5
 6    // Read processor's CR2 register to find the faulting address
 7    fault_va = rcr2();
 8
 9    // Handle kernel-mode page faults.
10    if((tf->tf_cs & 3) != 3)
11        panic("invalid page fault in kernel mode");
12
13    // We've already handled kernel-mode exceptions, so if we get here,
14    // the page fault happened in user mode.
15    // Destroy the environment that caused the fault.
16    cprintf("[%08x] user fault va %08x ip %08x\n",
17        curenv->env_id, fault_va, tf->tf_rip);
18    print_trapframe(tf);
19    env_destroy(curenv);
20}

Others

General Protection Fault

前面有提到,配置 interrupt handler 的時候需要設定 Descriptor Privilege Level(DPL),限制 user mode 只能使用某些特定 interrupt。那麼如果 user 執行其他 Privilege Level 的 interrupt,會發生什麼事呢?

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

以此程式為例,在 user mode 直接使用 INT instruction 執行 page fault interrupt,當然這種行為是不被允許的,因為 page fault interrupt 只能在 kernel mode 被觸發。因此, x86 會使用另一個 interrupt handler (General Protection Fault, identifer 13) 去處理。

而 General Protection Fault interrupt 觸發時機為(x86 Exceptions):

  1. Segment error (privilege, type, limit, read/write rights).
  2. Executing a privileged instruction while CPL != 0.
  3. Writing a 1 in a reserved register field or writing invalid value combinations (e.g. CR0 with PE=0 and PG=1).
  4. Referencing or accessing a null-descriptor.

例子中,我們是觸發到第二條規範,因此導致 General Protection Fault。

結論

此篇的重點比較不是在 Lab 3 的實作過程,而是透過 JOS 來了解 interrupt 的機制中可能會使用到哪些 register 和整體運作流程,其中包含 interrupt 的 context、TSS、和 IDT 等,都是蠻重要的知識,當然 linux kernel 的 interrupt 處理過程更為複雜,不過藉由 JOS,可以更具體地知道執行 interrupt 需要哪些要素,是我認為很重要的基礎概念。

Source code

YuShuanHsieh/CSE-506-jos branch: lab3

References

  1. Basic x86 interrupts
  2. Exceptions
  3. Intel® 64 and IA-32 ArchitecturesSoftware Developer’s Manual
  4. Chapter 9 Exceptions and Interrupts