6.S081 附加Lab1 用户执行系统调用的过程(Trap)
6.S081 附加Lab011 用戶執(zhí)行系統(tǒng)調(diào)用的過程(Trap)
文章目錄
- 6.S081 附加Lab011 用戶執(zhí)行系統(tǒng)調(diào)用的過程(Trap)
- 0. 一些背景說明
- 1. 進入內(nèi)核前的準(zhǔn)備write() -> ECALL
- 2. 進入內(nèi)核后的ECALL -> uservec()
- 3. 保存用戶寄存器內(nèi)容uservec()
- 4. 處理trap——usertrap()
- 5. usertrapret()
- 6. userrret()
0. 一些背景說明
附加lab從011開始編號,下一個是012(為了避開lab10,lab11…)
注意,這里主要接 6.S081-5用戶空間和內(nèi)核空間的切換–Trap機制
代碼流程:write() -> ECALL -> uservec()(Trampoline.s) -> usertrap() -> syscall() -> sys_write() -> syscall() -> usertrapret() -> usertrapret() ->ret
Trap的時候,我們需要做什么?(當(dāng)前處于user mode,現(xiàn)在需要執(zhí)行系統(tǒng)調(diào)用)
- 保存32個用戶寄存器
- 保存當(dāng)前PC
- mode切換成supervisor
- SATP從user page table切換成kernel_pagetable
- Stack Frame和Stack Pointer都需要改變,因為需要一個stack來調(diào)用kernel的函數(shù)
- 跳入kernel
本實驗將主要關(guān)心的是,執(zhí)行系統(tǒng)調(diào)用時計算機的狀態(tài) – 可以用寄存器狀態(tài)來判斷,主要關(guān)心的寄存器有:
-
PC 程序計數(shù)器(Program Counter Register)
-
mode標(biāo)志位(是supervisor mode 還是 user mode)
-
SATP(Supervisor Address Translation and Protection)寄存器,它包含了指向page table的物理內(nèi)存地址
-
STVEC(Supervisor Trap Vector Base Address Register)寄存器,它指向了內(nèi)核中處理trap的指令的起始地址。
-
SEPC(Supervisor Exception Program Counter)寄存器,在trap的過程中保存程序計數(shù)器的值。
-
SSRATCH(Supervisor Scratch Register)寄存器
后面的實驗內(nèi)容和流程,主要是實驗內(nèi)容參考-中文notes,實驗的流程和工具參考6.S081 Lab00 xv6啟動過程
總結(jié)一下:系統(tǒng)調(diào)用被刻意設(shè)計的看起來像是函數(shù)調(diào)用,但是背后的user/kernel轉(zhuǎn)換比函數(shù)調(diào)用要復(fù)雜的多。之所以這么復(fù)雜,很大一部分原因是要保持user/kernel之間的隔離性,內(nèi)核不能信任來自用戶空間的任何內(nèi)容。
1. 進入內(nèi)核前的準(zhǔn)備write() -> ECALL
打開sh.c,代碼如下,注意write(2, "$ ", 2);,它是我們接下來要追蹤的系統(tǒng)調(diào)用。-- shell 將 "$ " 通過 write系統(tǒng)調(diào)用輸出到文件描述符2。 – 注意,我這里其實原本是fprintf(2, "$ ");,需要修改成write ,然后make clean,然后再調(diào)試
int getcmd(char *buf, int nbuf) {// fprintf(2, "$ ");write(2, "$ ", 2);memset(buf, 0, nbuf);gets(buf, nbuf);if(buf[0] == 0) // EOFreturn -1;return 0; }int main(void) {static char buf[100];int fd;// Ensure that three file descriptors are open.while((fd = open("console", O_RDWR)) >= 0){if(fd >= 3){close(fd);break;}}// Read and run input commands.... }進入調(diào)試
- make qemu-gdb,并使用cpu個數(shù) = 1(方便調(diào)試)
- 新建窗口,打開gdb并用gdb遠(yuǎn)程鏈接 qemu的gdb
- 實際上被調(diào)用的write函數(shù)的實現(xiàn)如下(usys.s中可以看到) – 還有很多系統(tǒng)調(diào)用都是這樣實現(xiàn)的,比如pipe, close, read, kill …
? ecall會讓我們跳轉(zhuǎn)到內(nèi)核,kernel執(zhí)行完之后會返回,然后執(zhí)行ret,最終返回到shell。
- 找到shell 的write對應(yīng)的ecall地址(再shell.asm中找到如下代碼👇),可以看到ecall對應(yīng)地址是0xd66
- 在0xd66處設(shè)置斷點,然后運行 (注意這里的display/i $pc在每次斷點的時候,都顯示下一條指令的反匯編)
- 可以看到,我們成功在shell的write對應(yīng)的ecall處停下來了,然后我們刪除當(dāng)前斷點,打印pc,打印全部32個用戶寄存器(info reg)。這里的a0, a1, a2是shell傳給write系統(tǒng)調(diào)用的參數(shù)(write(2, "$ ", 2)) ——所以a0是2(文件描述符),a1是字符串指針,a2是2(寫入字符數(shù))。
- 打印a1的內(nèi)容,確實是"$ "
-
此外,寄存器可以看出,sp和pc還都比較接近0,說明現(xiàn)在運行在用戶態(tài)。(-- 說明是虛擬地址),因為物理地址,至少都是從0x80000(OS啟動處)開始的了。 --xv6中 kernel的虛擬地址和物理地址是一樣的。
-
查看SATP寄存器(頁表地址)
-
查看頁表:qemu界面,按ctrl + a然后按c可以進入qemu console, 輸入info mem可以查看頁表的,但是我這里看不到。
尋找原因:在如下所示的路徑文件下,存在的代碼如下,說明我必須是I386機型,才能使用info mem,這里我進行了注釋的修改,最終make會報錯,因此后面關(guān)于page table的輸出,我都只能暫時省去了。
// /home/wc/OS_experiment/qemu-4.1.0/riscv64-softmmu/hmp-commands-info.h// changed by levi #if defined(TARGET_I386) { .name = "mem", .args_type = "", .params = "", .help = "show the active virtual memory mappings", .cmd = hmp_info_mem, }, #endif // { // .name = "mem", // .args_type = "", // .params = "", // .help = "show the active virtual memory mappings", // .cmd = hmp_info_mem, // },- page table的正確輸出如圖👇——后面幾頁地址很大,是因為位于彈簧床程序中。(這是trampoline page。trampoline page包含了內(nèi)核的trap處理代碼)
2. 進入內(nèi)核后的ECALL -> uservec()
- 繼續(xù)執(zhí)行,ecall
- 打印pc,發(fā)現(xiàn)已經(jīng)是很大的地址了 – 說明進入了內(nèi)核 根據(jù)現(xiàn)在的程序計數(shù)器,代碼正在trampoline page的最開始,這是用戶內(nèi)存中一個非常大的地址,所以現(xiàn)在我們的指令正運行在內(nèi)存的trampoline page中。
- 查看當(dāng)前的指令(查看的是pc - 4的內(nèi)容):這些指令是內(nèi)核在supervisor mode中將要執(zhí)行的最開始的幾條指令,也是在trap機制中最開始要執(zhí)行的幾條指令。
- 繼續(xù)查看寄存器的值,發(fā)現(xiàn)并沒有改變——因此在此之前,我們還不能往這些寄存器賦值,否則不能正確恢復(fù)
- 進入qemu,查看page table,發(fā)現(xiàn)還沒改變👇。ecall并不會切換page table,這是ecall指令的一個非常重要的特點。 —— trap處理代碼必須存在于每一個user page table中,因為ecall并不會切換page table,我們需要在user page table中的某個地方來執(zhí)行最初的內(nèi)核代碼。而這個trampoline page,是由內(nèi)核小心的映射到每一個user page table中,以使得當(dāng)我們?nèi)匀辉谑褂胾ser page table時,內(nèi)核在一個地方能夠執(zhí)行trap機制的最開始的一些指令。——這里的控制是通過STVEC寄存器實現(xiàn)的,在從內(nèi)核空間進入到用戶空間之前,內(nèi)核會設(shè)置好STVEC寄存器指向內(nèi)核希望trap代碼運行的位置。
- 即使trampoline page是在用戶地址空間的user page table完成的映射,用戶代碼不能寫它,因為這些page對應(yīng)的PTE并沒有設(shè)置PTE_u標(biāo)志位。這也是為什么trap機制是安全的。
ecall指令都做了什么??
根據(jù)0. 一些背景說明中的要求,我們只完成了橘色部分,紅色部分還需要別的函數(shù)/指令完成👇
Trap的時候,我們需要做什么?(當(dāng)前處于user mode,現(xiàn)在需要執(zhí)行系統(tǒng)調(diào)用)
- 保存32個用戶寄存器
- 保存當(dāng)前PC
- mode切換成supervisor
- SATP從user page table切換成kernel_pagetable
- Stack Frame和Stack Pointer都需要改變,因為需要一個stack來調(diào)用kernel的函數(shù)
- 跳入kernel
為什么ecall 不切換pagetable?——切換page table的代價比較高,不用在不必要的場景切換page table。
所以ecall的下一條指令的位置是STVEC指向的地址,也就是trampoline page的起始地址。(注,實際上ecall是CPU的指令,自然在gdb中看不到具體內(nèi)容)
3. 保存用戶寄存器內(nèi)容uservec()
接上面,由于我們的page table中存放了trampoline page(-- 這樣每個進程都有自己的trapframe page,并且此處的虛擬地址總是0x3ffffffe000)
trampframe存放的內(nèi)容如下(proc.h 中的trapframe的結(jié)構(gòu)體),剛開始有5個kernel實現(xiàn)存放在trapframe中的數(shù)據(jù),后面是32個用戶寄存器的內(nèi)容。
// 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. // the sscratch register points here. // 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;/* 104 */ uint64 s1;/* 112 */ uint64 a0;/* 120 */ uint64 a1;/* 128 */ uint64 a2;/* 136 */ uint64 a3;/* 144 */ uint64 a4;/* 152 */ uint64 a5;/* 160 */ uint64 a6;/* 168 */ uint64 a7;/* 176 */ uint64 s2;/* 184 */ uint64 s3;/* 192 */ uint64 s4;/* 200 */ uint64 s5;/* 208 */ uint64 s6;/* 216 */ uint64 s7;/* 224 */ uint64 s8;/* 232 */ uint64 s9;/* 240 */ uint64 s10;/* 248 */ uint64 s11;/* 256 */ uint64 t3;/* 264 */ uint64 t4;/* 272 */ uint64 t5;/* 280 */ uint64 t6; };查看trampoline.S的代碼,第一行就是csrrw a0, sscratch, a0這個指令交換了a0和sscratch兩個寄存器的內(nèi)容。(為了清晰我這里給出了trampoline.S的完整代碼——后續(xù)也會用到)
#this is trampoline.S# code to switch between user and kernel space.## this code is mapped 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 to be aligned# to a page boundary.#.section trampsec .globl trampoline trampoline: .align 4 .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.## sscratch points to where the process's p->tf is# mapped into user space, at TRAPFRAME.## swap a0 and sscratch# so that a0 is TRAPFRAMEcsrrw a0, sscratch, a0# save the user registers in TRAPFRAMEsd 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)sd a3, 136(a0)sd a4, 144(a0)sd a5, 152(a0)sd a6, 160(a0)sd a7, 168(a0)sd s2, 176(a0)sd s3, 184(a0)sd s4, 192(a0)sd s5, 200(a0)sd s6, 208(a0)sd s7, 216(a0)sd s8, 224(a0)sd s9, 232(a0)sd s10, 240(a0)sd s11, 248(a0)sd t3, 256(a0)sd t4, 264(a0)sd t5, 272(a0)sd t6, 280(a0)# save the user a0 in p->tf->a0csrr t0, sscratchsd t0, 112(a0)# restore kernel stack pointer from p->tf->kernel_spld sp, 8(a0)# make tp hold the current hartid, from p->tf->kernel_hartidld tp, 32(a0)# load the address of usertrap(), p->tf->kernel_trapld t0, 16(a0)# restore kernel page table from p->tf->kernel_satpld t1, 0(a0)csrw satp, t1sfence.vma zero, zero# a0 is no longer valid, since the kernel page# table does not specially map p->tf.# jump to usertrap(), which does not returnjr t0.globl userret userret:# userret(TRAPFRAME, pagetable)# switch from kernel to user.# usertrapret() calls here.# a0: TRAPFRAME, in user page table.# a1: user page table, for satp.# switch to the user page table.csrw satp, a1sfence.vma zero, zero# put the saved user a0 in sscratch, so we# can swap it with our a0 (TRAPFRAME) in the last step.ld t0, 112(a0)csrw sscratch, t0# restore all but a0 from TRAPFRAMEld 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)# restore user a0, and save TRAPFRAME in sscratchcsrrw a0, sscratch, a0# return to user mode and user pc.# usertrapret() set up sstatus and sepc.sret打印sscratch寄存器的內(nèi)容,現(xiàn)在是2,其實這里就是a0之前的值,正如之前所說,write(2, "$ ", 2);函數(shù)調(diào)用的時候a0作為第一個傳入?yún)?shù)的保存者,保存的是文件描述符2。也就是說:在進入到user space之前,內(nèi)核會將trapframe page的地址保存在這個寄存器中,也就是0x3fffffe000這個地址。更重要的是,RISC-V有一個指令允許交換任意兩個寄存器的值。而SSCRATCH寄存器的作用就是保存另一個寄存器的值,并將自己的值加載給另一個寄存器。,所以,現(xiàn)在的a0就是trapframe的首地址(0x3ffffffe000)
(gdb) p/x $sscratch $5 = 0x2 (gdb) p/x $a0 $6 = 0x3fffffe000所以trampoline.S的后續(xù)指令(第二三四五六七八…)都有意義了,也就是a0其實是trapframe的首地址,由第二條指令sd ra, 40(a0)可知,ra被保存在了trapframe + 40的位置…。注意代碼的最后(倒數(shù)第二條指令,又將a0和sscratch寄存器的內(nèi)容互換了回去,然后執(zhí)行sret就返回用戶空間了)
- 然后注意trampoline中的第一條load指令,因為a0是trapframe,所以a0 + 8是kernel_sp(kernel的Stack Pointer)所以這條指令的作用是初始化Stack Pointer指向這個進程的kernel stack的最頂端。
- 指向完這👆條指令之后,我們打印一下當(dāng)前的Stack Pointer寄存器,這是這個進程的kernel stack。因為XV6在每個kernel stack下面放置一個guard page,所以kernel stack的地址都比較大。下一條指令是tp – 用來保存hartid(CPU編號) —— 保存當(dāng)前運行在多核CPU的哪一個核上(因為我設(shè)置了單核,所以編號一定是0)。
- 接下來是t0(usertrap的函數(shù)地址),t1(kernel_pagetable的地址)——實際上嚴(yán)格來說,t1的內(nèi)容并不是kernel page table的地址,這是你需要向SATP寄存器寫入的數(shù)據(jù)。它包含了kernel page table的地址,但是移位了(注,詳見4.3),并且包含了各種標(biāo)志位。
下一條指令是交換SATP和t1寄存器。這條指令執(zhí)行完成之后,當(dāng)前程序會從user page table切換到kernel page table。現(xiàn)在我們在QEMU中打印page table,可以看出與之前的page table完全不一樣👇。——成功切換了page table——切換到了kernel page table——有了kernel_pagetable我們就可以讀取kernel的data
這里還有個問題,為什么代碼沒有崩潰?畢竟我們在內(nèi)存中的某個位置執(zhí)行代碼,程序計數(shù)器保存的是虛擬地址,如果我們切換了page table,為什么同一個虛擬地址不會通過新的page table尋址走到一些無關(guān)的page中?看起來我們現(xiàn)在沒有崩潰并且還在執(zhí)行這些指令。有人來猜一下原因嗎?
學(xué)生回答:因為我們還在trampoline代碼中,而trampoline代碼在用戶空間和內(nèi)核空間都映射到了同一個地址。
之所以叫trampoline page,是因為你某種程度在它上面“彈跳”了一下,然后從用戶空間走到了內(nèi)核空間。
這就是本科的時候,柏軍老師說的彈簧床可以防止程序“跑飛”(這里的彈簧床和本實驗的彈簧床有所不同)——柏軍老師指的是操作系統(tǒng)啟動的時候,用彈簧床程序引導(dǎo)bootloader去main()的首地址去執(zhí)行(而不是直接用bootloader執(zhí)行main),這里的“彈簧床”意思就是,當(dāng)main()里面出現(xiàn)bug了,就再去彈簧床處執(zhí)行(再次調(diào)用main()),這樣程序就不會因為一次bug而panic。
最后一條指令是jr t0。執(zhí)行了這條指令,我們就要從trampoline跳到內(nèi)核的C代碼中。這條指令的作用是跳轉(zhuǎn)到t0指向的函數(shù)中。(前面已經(jīng)說過,是usertrap函數(shù)),當(dāng)然也可以打印一下:
(gdb) stepi 0x0000003ffffff08e in ?? () 1: x/i $pc => 0x3ffffff08e: jr t0 (gdb) x/3i $t00x8000276a <usertrap>: addi sp,sp,-320x8000276c <usertrap+2>: sd ra,24(sp)0x8000276e <usertrap+4>: sd s0,16(sp)接下來我們就要以kernel stack,kernel page table跳轉(zhuǎn)到usertrap函數(shù)。
4. 處理trap——usertrap()
usertrap某種程度上存儲并恢復(fù)硬件狀態(tài),但是它也需要檢查觸發(fā)trap的原因,以確定相應(yīng)的處理方式。代碼如下。
// // handle an interrupt, exception, or system call from user space. // called from trampoline.S // void usertrap(void) {int which_dev = 0;if((r_sstatus() & SSTATUS_SPP) != 0)panic("usertrap: not from user mode");// send interrupts and exceptions to kerneltrap(),// since we're now in the kernel.w_stvec((uint64)kernelvec);struct proc *p = myproc();// save user program counter.p->tf->epc = r_sepc();if(r_scause() == 8){// system callif(p->killed)exit(-1);// sepc points to the ecall instruction,// but we want to return to the next instruction.p->tf->epc += 4;// an interrupt will change sstatus &c registers,// so don't enable until done with those registers.intr_on();syscall();} else if((which_dev = devintr()) != 0){// ok} else if(r_scause() == 13 || r_scause() == 15){// printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);// printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());uvmalloc(p->pagetable, PGROUNDDOWN(r_stval()), PGROUNDDOWN(r_stval()) + 4096);}else {printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());printf("page down:%d\n",PGROUNDDOWN(r_stval()));// printf("r :%d\n",r_scause());// int sz = 0;// while( !(r_stval()>=sz && r_stval()<sz+4096) )// {// sz = sz + 4096;// }// printf("sz:%d\n",sz);p->killed = 1;}if(p->killed)exit(-1);// give up the CPU if this is a timer interrupt.if(which_dev == 2)yield();usertrapret(); }它做的第一件事情是更改STVEC寄存器。取決于trap是來自于用戶空間還是內(nèi)核空間,實際上XV6處理trap的方法是不一樣的。目前為止,我們只討論過當(dāng)trap是由用戶空間發(fā)起時會發(fā)生什么。如果trap從內(nèi)核空間發(fā)起,將會是一個非常不同的處理流程,因為從內(nèi)核發(fā)起的話,程序已經(jīng)在使用kernel page table。所以當(dāng)trap發(fā)生時,程序執(zhí)行仍然在內(nèi)核的話,很多處理都不必存在。
在內(nèi)核中執(zhí)行任何操作之前,usertrap中先將STVEC指向了kernelvec變量,這是內(nèi)核空間trap處理代碼的位置,而不是用戶空間trap處理代碼的位置。
然后來一步步分析代碼,注意看注釋部分
// 找出當(dāng)前正在運行的進程 -- 通過hartid (之前切換pagetable前已經(jīng)保存到t0) struct proc *p = myproc(); // 把當(dāng)前進程的pc保存到當(dāng)前進程的trapframe(防止進程切換找不到了) // save user program counter. p->tf->epc = r_sepc();- 把當(dāng)前進程的pc保存到當(dāng)前進程的trapframe(防止進程切換找不到了);接下來檢查進程是否被killed(這里shell沒有被killed,所以可以繼續(xù)執(zhí)行;記錄ret的地址 = 當(dāng)前pc + 4——這樣我們會在ecall的下一條指令恢復(fù),而不是重新執(zhí)行ecall指令;打開中斷(因為XV6會在處理系統(tǒng)調(diào)用的時候使能中斷,這樣中斷可以更快的服務(wù),有些系統(tǒng)調(diào)用需要許多時間處理。中斷總是會被RISC-V的trap硬件關(guān)閉,所以在這個時間點,我們需要顯式的打開中斷。);然后調(diào)用syscall。
- syscall函數(shù)——根據(jù)預(yù)定義的函數(shù)編號,找到函數(shù)地址sys_write
- 運行sys_write(參數(shù)保存在a0,a1和a2),現(xiàn)在需要返回了
- syscall之后再次返回usertrap函數(shù),usertrap調(diào)用了一個函數(shù)usertrapret。
5. usertrapret()
處理返回用戶空間之前,內(nèi)核要做的工作。
- 首先關(guān)閉中斷。我們關(guān)閉中斷因為當(dāng)我們將STVEC更新到指向用戶空間的trap處理代碼時,我們?nèi)匀辉趦?nèi)核中執(zhí)行代碼。如果這時發(fā)生了一個中斷,那么程序執(zhí)行會走向用戶空間的trap處理代碼,即便我們現(xiàn)在仍然在內(nèi)核中,出于各種各樣具體細(xì)節(jié)的原因,這會導(dǎo)致內(nèi)核出錯。所以我們這里關(guān)閉中斷。
- 在下一行我們設(shè)置了STVEC寄存器指向trampoline代碼,在那里最終會執(zhí)行sret指令返回到用戶空間。位于trampoline代碼最后的sret指令會重新打開中斷。這樣,即使我們剛剛關(guān)閉了中斷,當(dāng)我們在執(zhí)行用戶代碼時中斷是打開的。
- 接下來的幾行填入了trapframe的內(nèi)容
- kernel_pagetable
- 當(dāng)前用戶進程的kernel stack
- usertrap地址——trampoline可以跳轉(zhuǎn)到這里
- tp寄存器中的hartid
- 要設(shè)置SSTATUS寄存器,這是一個控制寄存器。這個寄存器的SPP bit位控制了sret指令的行為,該bit為0表示下次執(zhí)行sret的時候,我們想要返回user mode而不是supervisor mode。這個寄存器的SPIE bit位控制了,在執(zhí)行完sret之后,是否打開中斷。因為我們在返回到用戶空間之后,我們的確希望打開中斷,所以這里將SPIE bit位設(shè)置為1。修改完這些bit位之后,我們會把新的值寫回到SSTATUS寄存器。我們在trampoline代碼的最后執(zhí)行了sret指令。這條指令會將程序計數(shù)器設(shè)置成SEPC寄存器的值,所以現(xiàn)在我們將SEPC寄存器的值設(shè)置成之前保存的用戶程序計數(shù)器的值。在不久之前,我們在usertrap函數(shù)中將用戶程序計數(shù)器保存在trapframe中的epc字段。
- 根據(jù)current user process 的頁表,設(shè)置SATP寄存器,準(zhǔn)備切換到user 頁表。
- 計算好我們要jmp到的地址(trampoline中的userret函數(shù) —— 這個函數(shù)包含了所有能將我們帶回到用戶空間的指令。)
- 最后,就是執(zhí)行userret函數(shù)(這個函數(shù)在trampoline中)
6. userrret()
這個函數(shù)包含了所有能將我們帶回到用戶空間的指令。
.globl userret userret:# userret(TRAPFRAME, pagetable)# switch from kernel to user.# usertrapret() calls here.# a0: TRAPFRAME, in user page table.# a1: user page table, for satp.# switch to the user page table.csrw satp, a1sfence.vma zero, zero# put the saved user a0 in sscratch, so we# can swap it with our a0 (TRAPFRAME) in the last step.ld t0, 112(a0)csrw sscratch, t0# restore all but a0 from TRAPFRAMEld 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)# restore user a0, and save TRAPFRAME in sscratchcsrrw a0, sscratch, a0# return to user mode and user pc.# usertrapret() set up sstatus and sepc.sret-
首先是切換pagetable(從kernel pa tb到user pg tb)
-
然后是將之前保存在trapframe中的registers還原。user page table也映射了trampoline page,所以程序還能繼續(xù)執(zhí)行而不是崩潰。 (在這里a0是trapframe的地址)
-
解釋 sfence.vma zero, zero是clear page table 的TLB
-
重新打印所有寄存器(和調(diào)用前一致) (除了a0 —— a0 被write的返回值覆蓋了)
-
打印pc可以看出來,現(xiàn)在已經(jīng)回到用戶空間了 —— 以為pc很小,明顯是虛擬地址
最后總結(jié)一下,系統(tǒng)調(diào)用被刻意設(shè)計的看起來像是函數(shù)調(diào)用,但是背后的user/kernel轉(zhuǎn)換比函數(shù)調(diào)用要復(fù)雜的多。之所以這么復(fù)雜,很大一部分原因是要保持user/kernel之間的隔離性,內(nèi)核不能信任來自用戶空間的任何內(nèi)容。
Trampoline page之所以叫trampoline page,是因為你某種程度在它上面“彈跳”了一下,然后從用戶空間走到了內(nèi)核空間。從內(nèi)核空間彈了一下,又走出來。——trampoline代碼在用戶空間和內(nèi)核空間都映射到了同一個地址。這樣不會讓程序在user和kernel pagetable切換的時候崩潰。
總結(jié)
以上是生活随笔為你收集整理的6.S081 附加Lab1 用户执行系统调用的过程(Trap)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Cesium 鼠标操作习惯设置
- 下一篇: 初识docker容器(优势真的巨大,比虚