MIT6.S081-Lab4 Traps
開始日期:22.4.7
作業系統:Ubuntu20.0.4
Link:Lab Traps
目錄Lab Traps
寫在前面
vscode+wsl2+unbuntu20.04
-
由於使用虛擬機器,可能是分配的記憶體不夠,遇到了記憶體洩露,故更換為vscode+wsl2+unbuntu20.04
-
參考連結:
-
由於筆者mircosoft store無法使用,要自己下載windows terminal 和 ubuntu20.04
-
成功了
只使用gdb-multiarch
進入qemu-gdb
-
此方案是在學習群一位群友提出的,事實上,按之前的方式,輸入
gdb-multiarch kernel/kernel
之後就有「提示」了For help, type "help". Type "apropos word" to search for commands related to "word". warning: File "/home/duile/xv6-labs-2021/.gdbinit" auto-loading has been declined by your `auto-load safe-path' set to "$debugdir:$datadir/auto-load". To enable execution of this file add add-auto-load-safe-path /home/duile/xv6-labs-2021/.gdbinit line to your configuration file "/home/duile/.gdbinit". To completely disable this security protection add set auto-load safe-path / line to your configuration file "/home/duile/.gdbinit".
-
使用第一個
.gitinit
檔案使得/
目錄安全,該檔案位於/home/user/.gitinit
,
該檔案要「自己手動建立」,檔案內容只有一句,如下set auto-load safe-path /
-
/
路徑安全之後,就可以安全地執行第二個位於/home/user/xv6-labs-2021/.gdbinit
的檔案,檔案內容在git clone
的時候已經有了。set confirm off set architecture riscv:rv64 target remote 127.0.0.1:26000 symbol-file kernel/kernel set disassemble-next-line auto set riscv use-compressed-breakpoints yes
- 可以注意到,語句
target remote 127.0.0.1:26000
,筆者之前就使用過了,這裡的操作就是讓Ubuntu系統自動執行語句。
- 可以注意到,語句
-
那麼即可改為如下命令,即可進入qemu-gdb
-
one windows $ make CUPS=1 qemu-gdb another windows $ gdb-multiarch
效果如下:
參考連結
實驗內容
RISC-V assembly
-
以下是筆者的txt檔案,僅供參考
1. Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf? main() : a0 = pc value or ret value a1 = 12, a2 = 13 f() : a0 = ret value g() : a0 = ret value 2. Where is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.) 2.1 no call f()(and no call g()) make inline optimization that a1 = 12 2.2 the compiler inline func (look at line:32) 26 int f(int x) { 27 e: 1141 addi sp,sp,-16 28 10: e422 sd s0,8(sp) 29 12: 0800 addi s0,sp,16 30 return g(x); 31 } 32 14: 250d addiw a0,a0,3 33 16: 6422 ld s0,8(sp) 34 18: 0141 addi sp,sp,16 35 1a: 8082 ret 3. At what address is the function printf located? line:630 4. What value is in the register 'ra' just after the 'jalr' to 'printf' in 'main'? ra = pc+4 = 0x34 + 0x4 = 0x38 5. Run the following code. unsigned int i = 0x00646c72; printf("H%x Wo%s", 57616, &i); What is the output? Here's an ASCII table that maps bytes to characters. The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set i to in order to yield the same output? Would you need to change 57616 to a different value? 5.1 I run this process in win10, because ubuntu has warning(win10 also use little endian and I make some changes) #include<stdio.h> int main(){ unsigned int i = 0x00646c72; printf("H%x Wo%s \n", 57616, &i); printf("H%x Wo%x \n", 57616, i); } output: He110 World He110 Wo646c72 5.2 in ASCII: 0x72->r, 0x6c->l, 0x64->d if RISC-V is big endian, we must change i (i = 0x00646c72 => i = 0x00726c64) because we would the value of fisrt addr is 0x72 in big endian No change 57616 because we only use it hexadecimal value 6.In the following code, what is going to be printed after 'y='? (note: the answer is not a specific value.) Why does this happen? printf("x=%d y=%d", 3); I also run this process in win10 and I also make some changes #include<stdio.h> int main(){ printf("x=%d y=%d \n"); printf("x=%d y=%d \n", 3); } output: x=11364944 y=11356192 x=3 y=434198848 before run this process, x belong to a register that value is 11364944 y belong to another register that value is 434198848 before run this process, x belong to a register that value be changed to 3 y belong to another register that value also is 434198848
-
筆者重點講一下第4題,首先程式碼:
30: 00000097 auipc ra,0x0 34: 610080e7 jalr 1552(ra) # 640 <printf>
-
然後是參考檔案:
P36
AUIPC (add upper immediate to pc) uses the same opcode as RV32I. AUIPC is used to buildpc-relative addresses and uses the U-type format. AUIPC forms a 32-bit offset from the U-immediate,filling in the lowest 12 bits with zeros, sign-extends the result to 64 bits, adds it to the address of the AUIPC instruction, then places the result in register rd.
P37
Note that the set of address offsets that can be formed by pairing LUI with LD, AUIPC with JALR
-
由此得知,auipc的作用是「把高位立即數加到pc上」即執行
ra <- PC += (imm << 12)
,
看第一行程式碼30: 00000097 => 30: 0000000000000000 00001 0010111
,那麼有pc = 0x30
,imm = 0x0
,rd = 00001 -> ra
,opcode = 001011 -> auipc
(這些操作或者暫存器的「對應程式碼」,也可以在檔案查到,我這裡就不列出來了)
從而ra <- 0x30 += (0x0 << 12)
得出ra = 0x30
P21
The indirect jump instruction JALR (jump and link register) uses the I-type encoding. The target address is obtained by adding the sign-extended 12-bit I-immediate to the register rs1, then settingthe least-significant bit of the result to zero. The address of the instruction following the jump(pc+4) is written to register rd. Register x0 can be used as the destination if the result is not required.
-
由此得知,jalr的作用是「跳轉到偏移地址並將下一條pc指令儲存到暫存器當中」
即執行jump to address: base + offest
和ra <- pc + 0x4
看第二行程式碼,34: 00000097 => 30: 011000010000 00001 000 00001 1100111
,那麼有pc = 0x34
,offest = 1552 = 0x610
,rs1 = rd = 00001 -> ra
,opcode = 001011 -> jalr
在第一行程式碼,我們已經算出ra = 0x30
從而jump to address: ra + offest = 0x30 + 0x610 = 0x640
,ra <- pc + 0x4 = 0x34 + 0x4 = 0x38
-
綜上,在跳轉到printf函式後,
ra = 0x38
Backtrace
-
題意:在一個程式棧的多個幀中實現返回地址的「回溯」,在這個過程中,把這些返回地址打印出來
-
參考hints即可寫出,是回溯時,這個迴圈的結束條件是什麼?
-
hint提示到:一個stack的大小是一個頁面即
4kb
,從而得知,這個頁面上下限也是固定的地址。那麼無論棧指標fp
地址被改變成多少,靠fp
計算出的PGROUNDUP(fp)
和PGROUNDDOWN(fp)
都是不變的,筆者將其打印出來是:void backtrace(void) { uint64 fp = r_fp(); printf("%p, %p\n", PGROUNDUP(fp), PGROUNDDOWN(fp)); } output: 0x3fffff9000 0x3fffffa000
從而,得出迴圈條件:
while (PGROUNDUP(fp) == 0x3fffffa000 && PGROUNDDOWN(fp) == 0x3fffff9000)
,結合PGSIZE
也可以寫成while ((PGROUNDUP(fp) - PGROUNDDOWN(fp)) == PGSIZE)
-
取出
fp
的值時,要先轉為指標,再解除指標拿到裡面的值,「因為fp
本身是一個地址,不能直接用」,參考程式碼如下:void backtrace(void) { uint64 fp = r_fp(); printf("backtrace:\n"); uint64 ret_addr; while ((PGROUNDUP(fp) - PGROUNDDOWN(fp)) == PGSIZE){ ret_addr = *((uint64*)(fp-8)); printf("%p\n", ret_addr); fp = *((uint64*)(fp-16)); } }
-
-
hints的其它內容別忘了
Alarm
-
題意:CPU的每經歷一個tick就會觸發一次timer interrupt,我們需要安裝一個
sigalarm(n, fn)
(原題意是呼叫,筆者認為理解為安裝更好),使得xv6在n
個ticks
之後就能在CPU觸發的timer interrupt中呼叫一次fn
,這個fn
是handler
。void test0() { int i; printf("test0 start\n"); count = 0; /* install sigalrm */ sigalarm(2, periodic); /* wait timer interrupt after 2 ticks */ for(i = 0; i < 1000*500000; i++){ if((i % 1000000) == 0) write(2, ".", 1); if(count > 0) break; } /* unstall sigalrm */ sigalarm(0, 0); if(count > 0){ printf("test0 passed\n"); } else { printf("\ntest0 failed: the kernel never called the alarm handler\n"); } }
-
按照hints一步步來
test0: invoke handlers
-
實現呼叫一次
handler
,也就是呼叫一次periodic()
,因為可以看到test0
只輸出了一個alarm!
test0 start ........alarm! test0 passed
-
在實現呼叫一次
handler
之前,我們先回顧一下一次syscall
是怎麼發生的:- 硬體執行一些行為,做準備 [CPU]
- 彙編指令準備[vector]
- 在C程式碼中處理trap[handler]
- 返回原來的mode(kernel/mode)
-
而在這段過程中,需要思考它的
program count: PC
是怎麼變化,我們假設是一次syscall,如下When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?
-
ecall:
sret
是硬體指令,會將SEPC
設定為PC
,即SEPC <- PC <- ecall
,SEPC
此時為ecall
-
userver: no operation about any PC
-
usertrap:
p->trapfram->epc = r_sepc();
,將SEPC
傳給epc
,epc
此時為ecall
-
usertrap:
p->trapframe->epc += 4;
,+4
指向下一條指令,epc
此時為ret
.global sigalarm sigalarm: li a7, SYS_sigalarm ecall ret
-
usertrapret:
w_sepc(p->trapframe->epc);
,將epc
傳給SEPC
,SEPC
此時為ret
-
userret:
sret
是硬體指令,可以將PC
設定為SEPC
,PC
此時為ret
# return to user mode and user pc. # usertrapret() set up sstatus and sepc. sret
-
在使用者態執行
PC
即ret
-
-
回到
test0
,我們需要滿足,n
個ticks
之後,在CPU觸發的timer interrupt中呼叫一次handler
,參照上面的PC
變化,以及提示說要修改usertrap
,不難想出:在usertrap
中新增p->trapfram->epc = handler;
,使得最終處於使用者態會去執行handler
函式。這條語句當然需要寫在if(which_dev == 2)
之下,因為這是timer interrupt -
完成前五個hints
-
proc.h
新增new field,passticks
是為了配合ticks
,滿足n
個ticks
之後,才呼叫一次handler
// these are private to the process, so p->lock need not be held. int ticks; //ticks of alarm void (*handler)(); //handler function int passticks; //ticks from the last handler to the current handler
-
記得在allocproc,freeproc中分配,釋放相關內容。
allocproc() ... found: p->passticks = 0; p->ticks = 0; p->handler = 0; ... freeproc() ... p->passticks = 0; p->ticks = 0; p->handler = 0; ...
-
編寫
sys_sigalarm
,獲得入參ticks
和(void*)handler
,handler
通過argaddr()
傳進來時是型別是uint64
,要轉型為(void*)
uint64 sys_sigalarm(void) { int ticks; uint64 handler; if(argint(0, &ticks) < 0) return -1; if(argaddr(1, &handler) < 0) return -1; struct proc *p = myproc(); p->ticks = ticks; p->handler = (void*)handler; return 0; }
-
修改
usertrap()
,滿足n
個ticks
之後,才呼叫一次handler
- 一定要注意,我們這是在timer interrupt中
// give up the CPU if this is a timer interrupt. if(which_dev == 2){ p->passticks = p->ticks; while(p->ticks){ p->ticks--; if(p->ticks == 0) p->trapframe->epc = (uint64)p->handler; } p->ticks = p->passticks; yield(); }
test1/test2(): resume interrupted code
-
先考慮
test1
,CPU觸發timer interrupt之後,呼叫了一次handler
即periodic()
void periodic() { count = count + 1; printf("alarm!\n"); sigreturn(); }
-
它會接著執行一次syscall即
sigreturn()
,還是一樣:- 硬體執行一些行為,做準備 [CPU]
- 彙編指令準備[vector]
- 在C程式碼中處理trap[handler]
- 返回user mode
-
等到它迴歸user mode時,它的
trapfram
是屬於periodic()
的,但我們希望返回user mode時,它的trapfram
是屬於timer interrupt的。
那我們就需要在呼叫periodic()
之前先儲存好timer_trapfram
,在sys_sigreturn()
返回user mode之前用timer_trapfram
替換periodic()
的trapfram
即可 -
同時,為了滿足test2,Prevent re-entrant calls to the handler----if a handler hasn't returned yet, the kernel shouldn't call it again. 我們引入一個標誌:
handler_execute
-
proc.h
新增new field// these are private to the process, so p->lock need not be held. int ticks; //ticks of alarm void (*handler)(); //handler function int passticks; //ticks from the last handler to the current handler struct trapframe *timer_trapframe; // saves registers to resume in sigret int handler_execute; // handler executing => 1, handler no executing => 0
-
記得在allocproc,freeproc中分配,釋放相關內容。
allocproc() ... found: p->passticks = 0; p->ticks = 0; p->handler = 0; p->handler_execute = 0; // Allocate a timer_trapframe page. if((p->timer_trapframe = (struct trapframe *)kalloc()) == 0){ freeproc(p); release(&p->lock); return 0; } ... freeproc() ... p->passticks = 0; p->ticks = 0; p->handler = 0; if(p->timer_trapframe) kfree((void*)p->timer_trapframe); p->timer_trapframe = 0; p->handler_execute = 0; ...
-
修改
usertrap()
,在呼叫periodic()
之前先儲存好timer_trapfram
,同時滿足periodic()
的執行沒有結束之前,不能呼叫它- 注意不要使用
p->timer_trapframe = p->trapframe
,它只能獲得地址,而不是內容
// give up the CPU if this is a timer interrupt. if(which_dev == 2){ p->passticks = p->ticks; while(p->ticks){ p->ticks--; if(p->ticks == 0 && p->handler_execute == 0){ // we can't do it beacese we only get address of trapframe but not get content // p->timer_trapframe = p->trapframe; memmove(p->timer_trapframe, p->trapframe , sizeof(struct trapframe)); p->handler_execute = 1; p->trapframe->epc = (uint64)p->handler; } } p->ticks = p->passticks; yield(); }
- 注意不要使用
-
修改
usertrap()
,在sys_sigreturn()
返回user mode之前用timer_trapfram
替換periodic()
的trapfram
,同時,此時已經執行periodic()
可以視作執行結束,要將handler_execute
置為0
int sys_sigreturn(void) { struct proc *p = myproc(); memmove(p->trapframe, p->timer_trapframe, sizeof(struct trapframe)); p->handler_execute = 0; return 0; }
-
成果圖
總結
- 結束日期:22.4.10
- 本次實驗在過程中,沒有很好地把已學知識聯絡起來,尤其是在
PC
變化那一塊,我是知道的,但沒有完全聯絡起來,當然我雖然知道,但確實沒有很清晰 -
trapfram
的地址和內容是有區別,傳遞時要注意,到底是一個頁面內容,還是這個頁面的地址 - proc的新欄位,在allocproc,freeproc中都需要處理
- 最近在聽【A-SOUL/向晚】頂碗先生