RISC-V assembly
Which registers contain arguments to functions? For example, which register holds 13 in main's call to
printf
?
第一道題目是RISC-V匯編的相關內容。課程網頁上reference里給了一些參考資料,我搜了一下第一本The RISC-V Reader,沒想到還有中文版,建議看一下第三章:The RISC-V Reader: An Open Architecture Atlas (riscvbook.com)
下圖是RISC-V各寄存器的用途:

函數各參數顯然是在a0-a7中傳遞的,再看一下printf附近的匯編代碼,根據"li a2,13"顯然可知是a2。
Where is the call to functionf
in the assembly code for main? Where is the call tog
? (Hint: the compiler may inline functions.)
顯然沒有調用,編譯器直接優化了,把f(8)+1的值直接計算出來傳入printf中。
At what address is the function
printf
located?

這個auipc是把0x0左移12位,再加上PC值0x30存到ra里。jalr是先根據ra算出跳轉地址,再把現在的PC+4存入ra,作為稍后printf的返回地址。所以printf的位置是0x30+1552=0x640。(后面注釋也寫了)注意這個值是會變的,不同的環境可能不一樣。
What value is in the registerra
just after thejalr
toprintf
inmain
?
把運行到jalr處的PC+4存入ra,也就是0x38。
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.
打印結果是HE110 World。第一個(57616)2=(E110)16 這個顯然。i=110 0100 0110 1100 0111 0010,在內存里每個字節還要反過來存儲。因此直接查詢ascii表就行了。
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);
結果是x=3 y=1。y的值取決於printf第三個參數,a2寄存器的值。為了驗證,我們在0x38處下斷點:

運行到斷點處,執行一下info reg看看寄存器內容,$a2=1:

Backtrace
在kernel/printf.c里實現函數backtrace(),要求是打印棧里存儲的frame pointer。Hints里提示了risc-v棧幀的結構,很明顯是一個鏈表結構,因此我們需要做的事情就只是遍歷鏈表。
void backtrace(void)
{
struct proc* p=myproc();
printf("backtrace:\n");
uint64 fp=r_fp();
while(1)
{
fp=fp-16;//入棧的上一級frame pointer
uint64 ret=*((uint64*)(fp+8));
fp=*((uint64*)fp);//訪問這個地址
if(PGROUNDUP(fp) != p->kstack+PGSIZE)
{
break;
}
printf("%p\n",ret);//打印一下返回的PC值
}
}
被調用者的棧幀保存的frame pointer存儲在距離棧底16B的位置,占8B空間(記住棧的空間是從高到低擴展的),指向調用者的棧底。因此,首先把r_fp()得到的寄存器值減去16,得到frame pointer的地址;然后讀出存儲在距離棧底8B的return address 也就是fp+8。
這里用了一點技巧,先把fp+8轉換成uint64類型的指針,然后再使用*運算符把fp+8里面存儲的值讀出來。用相同的方法訪問fp得到上一級被調用者棧幀的開頭,如果fp所處的頁表最上端已經不是p->kstack指向的頁表最上端,說明fp已經脫離了內核分配給它的棧幀,循環終止。
多說一句:對於一個C結構體或者C++類
typedef struct e
{
int x;
int y;
int a;
int b;
int c;
int d;
}Entity;
想計算某一個成員相對結構體開頭的偏移量,怎么辦?以b為例:
printf("%d",&(((Entity*)0)->b));
先把0強制轉換成Entity*類型的指針,也就是欺騙編譯器,告訴它:有一個從地址0開始的Entity類,並使用&運算符計算這個類中b的地址,就可以得到結果12。
Alarm
這個實驗的要求還是比較復雜的,做之前一定要看一下視頻https://www.bilibili.com/video/BV19k4y1C7kA?p=5。
先梳理一下用戶進程進入內核態的過程;
1.調用ecall,ecall負責把進程從user mode提升到kernel mode,把PC當前值存進sepc里,把stvec寄存器里的地址復制到PC
2.stvec里是trampoline.S中uservec的地址,CPU執行下面的指令,切換用戶頁表到內核頁表,保存各寄存器到tramframe,從trapframe中恢復kernel stack地址等參數,跳轉到usertrap()
3.usertrap執行系統調用或者處理中斷,調用usertrapret()。如果執行系統調用的話,就將p->trapframe->epc加上4,這樣稍后返回用戶態的時候就會從調用trap的下一條指令開始執行。
4.usertrapret存儲kernel相關信息,跳轉到trampoline.S中的userret執行下面的匯編代碼
5.userret恢復寄存器和頁表,最終返回用戶態。
搞清楚系統調用是怎么進行的之后,我們來看程序要求。要求實現一個sigalarm函數,傳入一個函數指針handler,每過若干次時鍾中斷就執行一次handler。根據提示,我們用if(which_dev == 2)來檢測時鍾中斷。需要注意的是,程序傳入的是用戶地址空間中的函數指針,是不能在內核中執行的,需要返回到用戶態中執行。所以,當需要執行handler時,應該做的事情是把函數指針賦給p->trapframe->epc,這樣返回用戶態的時候CPU就會從handler的位置執行指令,而不是試圖在usertrap()等地方調用函數指針。有了這個基本思路就可以寫代碼了。先給struct proc添加幾項:
struct proc {
struct spinlock lock;
// p->lock must be held when using these:
enum procstate state; // Process state
void *chan; // If non-zero, sleeping on chan
int killed; // If non-zero, have been killed
int xstate; // Exit status to be returned to parent's wait
int pid; // Process ID
// wait_lock must be held when using this:
struct proc *parent; // Parent process
// these are private to the process, so p->lock need not be held.
uint64 kstack; // Virtual address of kernel stack
uint64 sz; // Size of process memory (bytes)
pagetable_t pagetable; // User page table
struct trapframe *trapframe; // data page for trampoline.S
struct trapframe *alarm_save; // 保存調用sigalarm之前的寄存器的原始數據
struct context context; // swtch() here to run process
struct file *ofile[NOFILE]; // Open files
struct inode *cwd; // Current directory
char name[16]; // Process name (debugging)
int alarm_int; //tick時間間隔
uint64 handler; //sigalarm()需要處理的handler
int tick_passed; //上一次執行handler之后過去的時間
int flag; //是否正在調用handler
};
在allocproc()里初始化:
found:
p->pid = allocpid();
p->state = USED;
p->tick_passed=0;
p->alarm_int=-1;
p->handler=0;
p->flag=0;
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
//分配alarm_save
if((p->alarm_save = (struct trapframe *)kalloc()) == 0){
freeproc(p);
release(&p->lock);
return 0;
}
freeproc():
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->alarm_save)//記得釋放alarm_save
kfree((void*)p->alarm_save);
p->alarm_save = 0;
修改后的usertrap:
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->trapframe->epc = r_sepc();
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->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
//intr_on();
if(which_dev == 2&&p->alarm_int!=0)
{
p->tick_passed+=1;
if(p->tick_passed==p->alarm_int)
{
if(p->flag==0)
{
memmove(p->alarm_save,p->trapframe,PGSIZE);
p->trapframe->epc=p->handler;
p->flag=1;//只有sigreturn可以將它置為0
}
p->tick_passed=0;
}
}
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
{
yield();
}
usertrapret();
}
兩個系統調用:
uint64 sys_sigalarm(void)
{
struct proc* p=myproc();
//獲取參數
int ticks;
uint64 handler_ptr;
argint(0,&ticks);
argaddr(1,&handler_ptr);
//傳入參數
p->alarm_int=ticks;
p->handler=handler_ptr;
return 0;
}
uint64 sys_sigreturn(void)
{
struct proc* p=myproc();
p->flag=0;//記得p->flag重置為0
memmove(p->trapframe,p->alarm_save,PGSIZE);
return 0;
}
代碼說明:
1.alarm_int是指定的時間間隔,在proc中初始置為-1,tick_passed初始置為0,所以不調用sigalarm就永遠不會執行下面memmove等操作。sys_sigalarm()唯一的作用是把時間間隔和函數指針存入struct proc之中。
2.如果alarm_int==0,則不執行任何操作。
3.如何保證執行完handler之后回到原來trap的位置? 在proc中設置新參數struct trapframe *alarm_save。初始化進程時給這個指針分配一頁內存。在修改epc到handler之前,把整個trapframe復制到alarm_save指向的頁。(當然這個頁只能在kernel mode中訪問)
4.系統調用sigreturn()的作用就是:handler完成后,再進入usertrap,復制這個alarm_save到trapframe中,這樣稍后再執行usertrapret()時,trapframe和剛執行sigalarm()時的trapframe就完全相同了。
5.如何保證執行handler時不再次執行sigalarm():在proc中聲明一個變量flag,初始化為0。只有flag==0時才允許執行修改epc和memmove的操作,執行完之后flag置1,只有sigreturn才有權限把它重新置為0。這樣,如果在handler執行時遇到時鍾中斷,當tick_passed等於alarm_int時就不會執行handler操作,但是仍然會執行重置或累加tick_passed的操作。