這一個實驗主要是對RISC-V的匯編、棧幀結構以及陷阱進行簡單的了解,難度並不大。
代碼放在github上。
RISC-V assembly (easy)
Q1: Which registers contain arguments to functions? For example, which register holds 13 in main's call to printf?
RISC-V的函數調用過程參數優先使用寄存器傳遞,即a0~a7共8個寄存器。返回值可以放在a0和a1寄存器。printf的參數13保存在a2寄存器。
Q2: 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.
從代碼可以看出,這兩個都被內聯優化處理了。main中的f調用直接使用了結果12,而f中的函數g調用直接內聯在f中了。
Q3: At what address is the function printf located?
在0x630的位置
Q4: What value is in the register ra just after the jalr to printf in main?
值應該為0x38,即函數的返回地址。
跳轉並鏈接指令(jal)具有雙重功能。若將下一條指令PC + 4的地址保存到目標寄存器中,通常是返回地址寄存器ra,便可以用它來實現過程調用。如果使用零寄存器(x0)替換ra作為目標寄存器,則可以實現無條件跳轉,因為x0不能更改。像分支一樣,jal將其20位分支地址乘以2,進行符號擴展后再添加到PC上,便得到了跳轉地址。
跳轉和鏈接指令的寄存器版本(jalr)同樣是多用途的。它可以調用地址是動態計算出來的函數,或者也可以實現調用返回(只需ra作為源寄存器,零寄存器(x0)作為目的寄存器)。Switch和case語句的地址跳轉,也可以使用jalr指令,目的寄存器設為x0。
Q5: 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?
結果為:He110 World; 不要修改為0x726c6400; 57616不需要進行改變,編譯器會進行轉換。
Q6: 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);
應該打印出寄存器a2的值,因為printf會從a2寄存器中讀取第三個參數作為y的值。
Backtrace (moderate)
實現backtrace,遞歸打印函數調用棧。使用r_fp獲取當前棧幀地址,由於棧是由高地址向低地址增長的,因此使用PGROUNDUP獲得棧底地址,之后循環打印棧幀的函數的返回地址。
void
backtrace(void)
{
printf("backtrace:\n");
uint64 fp = r_fp();
uint64 base = PGROUNDUP(fp);
while(fp < base) {
printf("%p\n", *((uint64*)(fp - 8)));
fp = *((uint64*)(fp - 16));
}
}
Alarm (hard)
這一個要求添加系統調用sigalarm
來實現當用戶程序運行了n個ticks后,觸發一次回調函數。由之前的學習可以知道,時鍾中斷的處理是在usertrap
函數中的if(which_dev == 2)
里面的。
為了實現這個功能,首先在proc
結構體中添加相應字段:
struct proc {
...
// these are used for sys_alarm
int duration; // ticks after last alarm
int alarm; // alarm every n ticks
uint64 handler; // handler for alarm
struct trapframe *alarm_trapframe; // register saved for alarm
};
之后實現sys_alarm
函數,將相關信息填入proc
中:
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->alarm = ticks;
p->handler = handler;
p->duration = 0;
p->alarm_trapframe = 0;
return 0;
}
而最關鍵的部分是在usertrap
中,當發生時鍾中斷時,將p->duration
增加,如果p->duration == p->alarm
,那么就要觸發一次回調函數,而觸發的方法就是將p->trapframe->epc
設置為回調函數地址,當陷阱處理程序結束后就會跳轉到回調函數。
而為了保證回調函數不會破壞原程序的寄存器,需要對trapframe
進行保存;我這里選擇的方法是通過kalloc
申請一個新的trapframe
結構體,然后將trapframe
復制一份。
為了保證回調函數執行期間不會重復調用,就可以判斷p->alarm_trapframe
是否為0,不為0說明上一次的回調函數還沒有調用sigreturn
,即函數未結束。
if(which_dev == 2){
if(p->alarm != 0){
p->duration++;
if(p->duration == p->alarm){
p->duration = 0;
if(p->alarm_trapframe == 0){
p->alarm_trapframe = kalloc();
memmove(p->alarm_trapframe, p->trapframe, 512);
p->trapframe->epc = p->handler;
}else{
yield();
}
}else{
yield();
}
}else{
yield();
}
}
最后就是sigreturn
函數,這個函數要做的工作就是將之前保存的alarm_trapframe
還原到trapframe
中,並將alarm_trapframe
釋放掉。別忘了在freeproc
函數中也要對p->alarm_trapframe
進行判斷,防止程序異常結束時該頁面沒有被釋放。
uint64
sys_sigreturn(void)
{
struct proc* p = myproc();
if(p->alarm_trapframe != 0){
memmove(p->trapframe, p->alarm_trapframe, 512);
kfree(p->alarm_trapframe);
p->alarm_trapframe = 0;
}
return 0;
}