系列文章 : [6.1810] 跟著 MIT 6.1810 學習基礎作業系統觀念
有三種事件會讓 CPU 擱置原本的 instruction,強制將控制權轉交給負責該事件的特別的 kernel code。
這三種事件在這裡都統稱為 trap。
通常發生 trap 時正在被執行的程式碼,在之後會希望可以恢復執行,並且不需要意識到發生了什麼事。
我們通常會希望 trap 是透明的(transparent) ( 不會被 trap 的 process 所察覺。例如一個正在計算某個東西的程式,發生了 timer interrupt 的時候,這個程式不需要為這個 timer interrupt 特別處理,大部分的時候也不會知道,不會看到,不會觀察到 timer interrupt 的發生,就好比 timer interrupt 對於被 trap 的 process 來說,是透明的 )
一個 trap 會強制將控制權交給 kernel ( from U-mode to S-mode ),xv6 kernel 會保存 register 的值或是其他狀態,讓我們可以返回 trap 前正在執行的程式碼。
kernel 會執行適當的 handler ( system call implementation 或是 device driver ),kernel 會把 trap 前的狀態恢復,並從 trap 回到原本的執行流,從原本發生 trap 的地方恢復執行。
xv6-riscv 會在 kernel ( S-mode ) 去處理所有的 trap ( 包含 system call, exception, interrupt ),並不會在 U-mode 去處理 trap。
xv6 的 trap handling 有四個步驟
雖然 kernel 可以用單一個 code path 去處理三種 trap,但把兩個情況分開來處理會更加方便
處理 trap 的 kernel code 通常被稱為 handler。
handler 的第一道指令通常會以組合語言撰寫,並且有時被稱為 vector
每個 RISC-V CPU 都有一組 CSR, kernel 透過寫入這些暫存器來告知 CPU 如何處理 trap,並透過讀取這些 CSR 來了解發生了什麼樣的 trap。
這邊整理一下重要的 registers
trap 發生的時候,RISC-V 會把 pc 值存進 sepc ( 因為 pc 已經被 stvec 覆蓋了 ),之後sret ( return from trap ) 會把 sepc 存回 pc。sepc,來控制 sret 要跳去哪個地方。device interrupt 能不能 trap CPU。假如 SIE == 0,RISC-V 會推遲 device interrupt ( 但不會推遲 system call, exception ) 直到 kernel 設定 SIE = 1。sret 會回到哪個 mode。上面的這些 CSR 都只能在 supervisor mode ( S-mode ) 的時候去使用,CPU 會避免讓 U-mode 去讀寫這些 CSR。
在多核系統上的每一個 CPU 都有自己的 CSRs。
當一個 trap 發生的時候,RISC-V 的硬體或作下列的事情
且 sstatus.SIE == 0,則不會繼續往下走CPU 不會切換到 root kernel page table,也並不會切換到 kernel 的 stack,並且不會儲存除了 pc 以外的 registers,作業系統核心的程式碼需要自己處理這些工作。CPU 會做那麼少事情的原因之一是會了提供彈性,因為某些作業系統為了提高 trap 的效能,可能會省略切換 page table 這件事情,
CPU 在 trap 發生時什麼都不做的話,可能導致資訊安全的危險。例如說 CPU 在 trap 之後沒有自動地把 pc 切換成 stvec 的話,那就表示進入 S-mode 時,還是繼續執行 user 撰寫的程式碼,這時候攻擊者可以很輕鬆地把 satp 換掉,指向一個由攻擊者構成的 root page table,取得所有 physical memory 的控制權。
trampoline 是跳板 ( 或彈跳床 ) 的意思,在這裡的意思應該為 user process ( U-mode ) 跟 kernel ( S-mode ) 間的跳板。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trampoline.S#L21 }
#
# low-level code to handle traps from user space into
# the kernel, and returns from kernel to user.
#
# the kernel maps the page holding this code
# at the same virtual address (TRAMPOLINE)
# in user and kernel space so that it continues
# to work when it switches page tables.
# kernel.ld causes this code to start at
# a page boundary.
#
trampoline.S 裡面的程式碼會被映射到 virtual address : TRAMPOLINE 的位置,不論是 kernel ( S-mode ) 或是 user process ( U-mode ) 都是這個樣子。
這時候有幾個問題可以思考
以下是原因
trap 發生,從 U-mode 跳到 S-mode 的時候, satp 還沒有切換,所以雖然我們已經在 S-mode,但還是使用 user process 的 root page table 而不是 root kernel page table。
usertrap 這個 frunction 前,才會把 sapt 覆蓋過去,換成 root kernel page table。user process 的 root pable table 以及 kernel prcocess 的 root kernel page table 映射在相同的地方。.globl uservec
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
在 U-mode 的時候,stvec 會被設為 uservec。當我們發生 trap ( ecall, exception, interrupt ),就會提權到 S-mode,且 pc 就會跳到這邊來進行執行,並且仍舊使用 user process 的 page table。
# save user a0 in sscratch so
# a0 can be used to get at TRAPFRAME.
csrw sscratch, a0
把 user-process 的 a0 暫時放在 sscratch 裡面,因為 a0 需要被用來指向 TRAPFRAME。
# each process has a separate p->trapframe memory area,
# but it's mapped to the same virtual address
# (TRAPFRAME) in every process's user page table.
li a0, TRAPFRAME
讓 a0 指向 TRAPFRAME,幫助我們把 user process 的 CPU 狀態存進 p->trapfram。
下面稍微看一下 TRAPFRAME 是怎麼被映射的 ( va ←→pa )。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/proc.c#L198 }
// map the trapframe page just below the trampoline page, for
// trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
在這裡,會把 p->trapframe ( physical address ) 映射到 TRAPFRAM ( virtual address )。
在 trap 發生時,可以將 user process 發生 trap 當下的 CPU 資訊 ( e.g. registers ) 存放在 trapframe 裡面。要從 trap 返回 user process 的時候,可以把 trapframe 裡面的資訊放回 CPU 上,使得 trap 對 user process 來說是透明的 ( transparent ),user process 大部分時候不會知道有 trap 的發生。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/memlayout.h#L59 }
TRAPFRAM 這一段 virtual address 是放在 TRAMPOLINE 下面。跟 TRAMPOLINE 很像,雖然在 user process 的 root page table 裡面有這一段映射,但其實 user process ( U-mode ) 沒辦法使用,只有在 trap 到 S-mode 的時候,才有辦法使用。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/proc.h#L31C1-L81C1 }
// per-process data for the trap handling code in trampoline.S.
// sits in a page by itself just under the trampoline page in the
// user page table. not specially mapped in the kernel page table.
// uservec in trampoline.S saves user registers in the trapframe,
// then initializes registers from the trapframe's
// kernel_sp, kernel_hartid, kernel_satp, and jumps to kernel_trap.
// usertrapret() and userret in trampoline.S set up
// the trapframe's kernel_*, restore user registers from the
// trapframe, switch to the user page table, and enter user space.
// the trapframe includes callee-saved user registers like s0-s11 because the
// return-to-user path via usertrapret() doesn't return through
// the entire kernel call stack.
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
/* 16 */ uint64 kernel_trap; // usertrap()
/* 24 */ uint64 epc; // saved user program counter
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 40 */ uint64 ra;
/* 48 */ uint64 sp;
/* 56 */ uint64 gp;
/* 64 */ uint64 tp;
/* 72 */ uint64 t0;
/* 80 */ uint64 t1;
/* 88 */ uint64 t2;
/* 96 */ uint64 s0;
…
每一個 process 都有自己的 page 來存放 trapframe,並且會映射在 user process 的 root page table,並不會特別映射在 kernel root page table。
雖然每一個 process 都有自己的一塊 trapframe,但他們通通都映射到相同的 virtual address TRAPFRAME 上。這讓每一個 process 在找自己的 trapframe 的時候,都可以用相同的 virtual address,相當方便。
在 trampoline.S/uservec 裡面,會將 user registers 都存進 trapframe 裡面,並且仰賴 trapframe 裡面的資訊去初始化一些 registers
usertrap 這個 function 的 address,會透過這個資訊,將 pc 值跳到 usertrap 這個 function 進行後續的處理。當我們要從 trap 返回 user process 的時候
kernel_* 設定好要注意,在 trapframe 這邊除了 caller-saved register 以外,也需要儲存 callee-saved registers。
# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
…
這裡 a0 會指向 p->trapframe,開始把 user process 的 registers 放進 trapframe。
# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)
因為 user process 的 register : t0 已經被放進 trapframe 了,所以我們可以開始使用這個 register! 我們把 user process register : a0 從 sscratch 放進 t0,然後再放進 trapframe 裡面。
# initialize kernel stack pointer, from p->trapframe->kernel_sp
ld sp, 8(a0)
從 user stack 切換到 kernel stack
# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0)
讓 tp 保持有當前的 hartid,這是因為 xv6-riscv 的 cpuid 實作是仰賴 tp ( https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/proc.c#L65 )。
而為什麼 p->trapframe->kernel_hartid 會指向當前的 id 呢 ? 這是因為在 prepare_return { https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/5474d4bf72fd95a6e5c735c2d7f208f58990ceab/kernel/trap.c#L118 } 的時候,會把當前的 hart-id 存進 p->trapframe->kernel_hartid。
就算這個 process 從 CPU0 換到 CPU2 去執行,因為在 context switch 後,回到 user-space 前 才會執行 prepare_return,所以 p->trapframe->kernel_hartid 永遠會指向最新的 hartid。
# load the address of usertrap(), from p->trapframe->kernel_trap
ld t0, 16(a0)
把 usertrap() 的 function pointer 放進 t0。
# fetch the kernel page table address, from p->trapframe->kernel_satp.
ld t1, 0(a0)
拿到 kernel 的 root page table。
# wait for any previous memory operations to complete, so that
# they use the user page table.
sfence.vma zero, zero
# install the kernel page table.
csrw satp, t1
# flush now-stale user entries from the TLB.
sfence.vma zero, zero
設定 satp,從 user process 的 page table,正式切換到 kernel root page table。
在設定 satp 的前後,記得都要加上 memory barrier。
# call usertrap()
jalr t0
剛剛有把 usertrap() 的 function pointer 放進 t0。
所以這邊會把 pc 值跳到 usertrap 開始執行。
//
// handle an interrupt, exception, or system call from user space.
// called from, and returns to, trampoline.S
// return value is user satp for trampoline.S to switch to.
//
uint64
usertrap(void)
在 usertrap 這個 function 裡面,會去處理來自 U-mode 的 interrupt, exception, 以及 system call。
trampoline.S 的 uservec 會跳到這裡執行,而這邊處理完後,會跳回到 trampoline.S 的 userret。
這個 function 的 return value 是 user process 的 root page table,會回傳給 trampoline.S/userret。
{
int which_dev = 0;
which_dev 用於紀錄觸發中斷的設備類型。
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
{ riscv-privileged : 12.1.1. Supervisor Status (sstatus) Register }
sstatus.SPP ( Supervisor Previous Privilege ) : 當中斷發生時,CPU ( 硬體 ) 會將當下的特權級 ( U-mode or S-mode ) 存在 sstatus.SPP 裡面
這邊假如檢查到,中斷不是來自於 U-mode,卻進入了 usertrap,這顯然不正常。會發 panic,把系統整個凍結住。
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
w_stvec((uint64)kernelvec); //DOC: kernelvec
因為我們現在是在 kernel ( S-mode ),所以當中斷發生時,我們該跳去 kernelvec,而不是 uservec,這邊把 stvec 設為 kernelvec。
struct proc *p = myproc();
取得目前正在執行的 process 的 struct proc
// save user program counter.
p->trapframe->epc = r_sepc();
sepc 存進該 process 的 trapframe->epc。
if(r_scause() == 8){
// system call
if(killed(p))
kexit(-1);
假如這個 process 已經標記為被砍掉的 process,就用 kexit 把這個 process 砍掉。
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
假如我們是因為執行 ecall 而發生 trap 的話,sepc 實際上會指向 ecall 這個 instruction 的位址。所以當我們想從 S-mode 回到 U-mode 的時候,實際上我們會想要回到 sepc + 4 的位置 ( ecall 的下一道指令 )。
假想一下,我們沒有 +4 的話,當我們從 trap 回到 user process,就又會再執行一次 ecall,然後又進入 trap … 會陷入無窮的 trap !!
// an interrupt will change sepc, scause, and sstatus,
// so enable only now that we're done with those registers.
intr_on();
到此為止,已經把該存的狀態都存的差不多了,所以又可以將 interrupt 開啟。
syscall();
開始處理 system call。
} else if((which_dev = devintr()) != 0){
// ok
devintr 會去查看 scause,看看這是否是硬體觸發的中斷。
接下來
} else if((r_scause() == 15 || r_scause() == 13) &&
vmfault(p->pagetable, r_stval(), (r_scause() == 13)? 1 : 0) != 0) {
// page fault on lazily-allocated page
vmfault : 假如 va 是落在合法的位址,就配置一塊記憶體。
scause :
} else {
printf("usertrap(): unexpected scause 0x%lx pid=%d\n", r_scause(), p->pid);
printf(" sepc=0x%lx stval=0x%lx\n", r_sepc(), r_stval());
setkilled(p);
}
假如沒辦法處理這個 trap ( e.g. 除以 0,未知的硬體中斷... ),就把這個 process 給 kill 掉。setkilled 實際上只會進行 p->killed = 1; ,將 killed flag 舉起來,真的想殺掉這個 process,需要仰賴 kexit。
if(killed(p))
kexit(-1);
檢查這個 process 是否已經被標記為 killed ,若是的話則呼叫 kexit 結束它。
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
假如遇到 timer interrupt,就把 CPU 讓給其他人。
呼叫 yield function 會讓這個 process 把 CPU 讓出來,讓其他 process 有機會使用這個 CPU。
prepare_return();
在這個 function 裡,我們會將一些資訊存回 trapframe,並且設定 CPU 的 CSR。
準備回到 user space。
// the user page table to switch to, for trampoline.S
uint64 satp = MAKE_SATP(p->pagetable);
// return to trampoline.S; satp value in a0.
return satp;
}
MAKE_SATP 可以將 page table 的 physical address 變成可以放進 satp 的值。
在 usertrap 會 return satp 的值給 trampoline.S/userret。
在 userret 裡面,可以透過 a0 register 取得這個值。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L100 }
//
// set up trapframe and control registers for a return to user space
//
void
prepare_return(void)
這個 function 會把一些資訊存回 p->trapframe,以及設定當前 CPU 的一些 CSRs,為返回 user space 來作準備。
trapframe
CSRs
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(). because a trap from kernel
// code to usertrap would be a disaster, turn off interrupts.
intr_off();
因為要返回 user space 了,所以準備要將 stvec 從 kerneltrap 換成 usertrap。
假如這時候發生 trap,以 kernel ( S-mode ) 的身分跳進 usertrap 可就慘了! 所以這邊乾脆把中斷給關起來。
// send syscalls, interrupts, and exceptions to uservec in trampoline.S
uint64 trampoline_uservec = TRAMPOLINE + (uservec - trampoline);
w_stvec(trampoline_uservec);
計算出 uservec 的 virtual address 該在哪邊 ( trampoline_uservec ),並且把它放進 stvec。
Q: 為什麼會需要 uservec - trampoline ??
為什麼不直接 trampoline_uservec = TRAMPOLINE ??
A: 可能是怕 trampoline 跟 uservec 之間會插入其他程式碼。
// set up trapframe values that uservec will need when
// the process next traps into the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// set up the registers that trampoline.S's sret will use
// to get to user space.
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);
}
因為等等會呼叫 sret 跳回 user-mode
這邊將想要跳去的值寫入 sepc,這樣子呼叫 sret 的時候,就會跳到 p->trapframe->epc
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trampoline.S#L101 }
# usertrap() returns here, with user satp in a0.
# return from kernel to user.
# switch to the user page table.
sfence.vma zero, zero
csrw satp, a0
sfence.vma zero, zero
設定 sapt,將 page table 從 kernel root page table 換回到屬於這個 process 的 user process root page table。
在切換 sapt 的前後,需要記得加上 barrier。
li a0, TRAPFRAME
將 a0 指向 TRAPFRAME ( virtual address )。
現在我們已經使用 user process 的 root page table 了! 雖然我們仍舊身處在 kernel space ( S-mode )。
# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
ld gp, 56(a0)
ld tp, 64(a0)
ld t0, 72(a0)
ld t1, 80(a0)
ld t2, 88(a0)
ld s0, 96(a0)
ld s1, 104(a0)
ld a1, 120(a0)
ld a2, 128(a0)
ld a3, 136(a0)
ld a4, 144(a0)
ld a5, 152(a0)
ld a6, 160(a0)
ld a7, 168(a0)
ld s2, 176(a0)
ld s3, 184(a0)
ld s4, 192(a0)
ld s5, 200(a0)
ld s6, 208(a0)
ld s7, 216(a0)
ld s8, 224(a0)
ld s9, 232(a0)
ld s10, 240(a0)
ld s11, 248(a0)
ld t3, 256(a0)
ld t4, 264(a0)
ld t5, 272(a0)
ld t6, 280(a0)
從 trapframe 裡面,去把 user process 的 register 放回到 CPU 上。
# restore user a0
ld a0, 112(a0)
a0 不用再指向 p->trapframe,終於功成身退了,可以把它恢復成 user process 的 a0 register。
# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret
正式跳回 U-mode !
sret 硬體上會 :
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L20 }
void
trapinit(void)
{
initlock(&tickslock, "time");
}
初始化 tickslock,這個 lock 是用來保護 ticks 變數。
{ https://github.com/TommyWu-fdgkhdkgh/xv6-riscv/blob/xv6-riscv-rev5/kernel/trap.c#L27 }
// set up to take exceptions and traps while in the kernel.
void
trapinithart(void)
{
w_stvec((uint64)kernelvec);
}
每個 CPU 都會去呼叫 trapinithart 來初始化自己的 stvec。這邊將 kernelvec 這個 function 的位址寫入 stvec ( Supervisor Trap Vector ),代表只要在 kernel ( S-mode ) 發生中斷時, pc 就會跳到 kernelvec 去處理。
xv6-riscv 在處理 trap 的時候,會把 (來自 U-mode 的 trap) ,以及 ( 來自 S-mode 的 trap ) 分開處理。
這邊簡單介紹一下,來自 U-mode 的 trap 大致上會怎麼樣處理。
一個來自 user space 的 trap 可能是因為 user program
簡化的流程處理流程
不會 去設定 satp,不會 切換 page table。ret 回到 trampoline.S,然後執行 userret
userret 會用 sret 從 S-mode 跳回到 U-mode,並回到 user process。這邊要特別注意的是,當我們在 U-mode 發生 trap,進入 uservec 的時候,硬體並不會幫我們切換 page table,所以在剛進入 uservec 的時候,其實我們仍舊使用 user process 的 root page table !
因為硬體不會幫我們切換 page table,所以 xv6-riscv 的 trap handling 程式碼 ( 被 stvec 指向的地方 ),勢必要映射在 user process 的 root page table 裡面,儘管 user process 沒辦法觸碰這個區段。
又因為我們在 xv6-riscv 的 trap handling 需要去切換 page table ( 從 user-process root page table 切換到 kernel root page table ),所以在 kernel root page table 也必須要有 trap handling ( stvec 指向的位址 ) 這塊 address 的 va ←→pa 映射。
xv6-riscv 為了滿足這些需求,所以使用了一個 trampoline page ( 跳板頁 ),來存放 user space 與 kernel space 間的跳板。這個 page 包含了 uservec ( 有就是 user process 執行時,stvec 所指向的地方 )。這個 trampoline page 在每一個 process 的 page table,以及 kernel 的 root page table,都會有映射。而映射的位址會在 virtual address : 0x3ffffff000 (在程式碼內叫做 TRAMPOLINE)。
trampoline page 會位於 virtual address space 的最後一個 page
因為 trampline page 在 user process page table 跟 kernel page table 會映射到相同的位址,所以在 trap handler 裡面,就算我們設定 sapt,並從 user process page table 轉換成 kernel kernel table 的時候,我們還是可以順暢地繼續執行。
trapframe page 的性質跟 trmapoline page 有點相似,但也有不同的地方。
kernel space 不需要把 trapframe page 映射在 TRAPFRAME ( 0x3fffffe000 ),kernel space 在取用特定 process 的 trapframe 的時候,會直接使用 direct-mapped physical memory ( kernel space 會把大部份的 physical memory direct-mapped 在自己的 root page table 裡面。 ) ( direct-mapped : 雖然在 page table 上有 va ←→pa 的映射,但其實 va == pa )。所以可以看到,在 usertrap 這個 function 裡,會去用 myproc 去取得相對應的 process 的 trapframe。每個 user process 都會把 trapframe 映射在 virtual address ( TRAPFRAME, 0x3fffffe000 ),但是每個 user process 的 trapframe 實際上會是在不同的 physical address ( 這也是理所當然的,每個 process 都該有自己的一份 trapframe )。
trap 會有幾種狀況
syscall function 處理devintr function 處理要注意的是,假如是處理 system call 的時候,需要把 p->trapframe->epc + 4 ( interrupt, exception 不需要 ),因為當 U-mode 執行 ecall,並 trap 進 S-mode 的時候,硬體 在 sepc 所儲存的 pc 值是指向 ecall 這個 instruction,於是當我們處理完 system call,想從 S-mode 回到 U-mode 的時候,我們該回到的是 ecall 的下一道 instruction。
從 S-mode 回到 U-mode 大致上的步驟:
prepare_return
trampoline page,並執行 userret
sret,回到 U-mode !