MIT6.S081-lab4

MIT6.S081-lab4

注:本篇lab的前置知識在《MIT6.S081-lab3前置》

1. RISC-V assembly

第一個問題

Which registers contain arguments to functions? For example, which register holds 13 in main’s call to printf?

我們先來看看main干了什么:

void main(void) {1c:	1141                	addi	sp,sp,-161e:	e406                	sd	ra,8(sp) 20:	e022                	sd	s0,0(sp)22:	0800                	addi	s0,sp,16printf("%d %d\n", f(8)+1, 13);							# 編譯器直接算出來了,無需調用f和g函數24:	4635                	li	a2,13              	# printf參數存入寄存器a226:	45b1                	li	a1,12             28:	00001517          	auipc	a0,0x1        	 	# 存入格式格式字符串的大致地址,printf的第一個參數2c:	84850513          	addi	a0,a0,-1976        	# a0 = a0 - 1976,即精確地得到格式字符串地址 "%d %d\n"30:	68c000ef          	jal	6bc <printf>exit(0);34:	4501                	li	a0,0 36:	26e000ef          	jal	2a4 <exit>

綜上,a0,a1,a2存放了對應的調用函數所要用的參數。

第二個問題

is the call to function f in the assembly code for main? Where is the call to g? (Hint: the compiler may inline functions.)

對f的調用我發現已經被編譯器所優化了,這里直接將一個立即數存入了a1中:

  26:	45b1                	li	a1,12

第三個問題

At what address is the function printf located?

根據匯編代碼,我們可以知道,printf位于6bc處,事實上,我們可以在call.asm里面搜索printf,我們可以找到,函數的入口確實是6bc

....
void
printf(const char *fmt, ...)
{6bc:	711d                	addi	sp,sp,-966be:	ec06                	sd	ra,24(sp)6c0:	e822                	sd	s0,16(sp)6c2:	1000                	addi	s0,sp,32.....

第四個問題

What value is in the register ra just after the jalr to printf in main?

在main里面,我們很容易發現,根本沒有用到ra寄存器,但是其實,ra存儲的一般是我們的函數返回的地址,所以在我們調用jal的時候, 會自動將下一條指令的地址存入ra寄存器中,即0x34

第五個問題

Run the following code. What is the output?

	unsigned int i = 0x00646c72;printf("H%x Wo%s", 57616, (char *) &i);

輸出:He110 World,大端模式則需要將i修改為0x72 6c 64 00,我們可以發現就是反轉了一下,而另一個數字無需修改,因為這個打印的是16進制表示數字,與大端小端字節序無關。

第六個問題

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);

未定義行為,這取決于對應寄存器的值。

2. Backtrace

常爆panic的同學應該會對這個backtrace非常熟悉,他會打印我們函數調用鏈路的函數返回的地方。這就是我們實驗需要實現的東西了,根據lab1,我們可以知道,函數調用的時候,都會把函數返回的地方的地址存儲起來,那么我們的目的,就是找到這個存儲地址的地方,并且將他打印出來。

難點就在于怎么去找到這個地址,光靠自己去推理,肯定是很困難的,這時候就需要看給我們的hint。

首先,我們向kernel/defs.h中添加static inline uint64 r_fp()這個函數,用來在我們的當前需要編寫的backtrace中獲取當前的幀指針,以此為基礎,來獲取之前的函數返回地址。

隨后繼續往下看:

  • These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
  • Your backtrace() will need a way to recognize that it has seen the last stack frame, and should stop. A useful fact is that the memory allocated for each kernel stack consists of a single page-aligned page, so that all the stack frames for a given stack are on the same page. You can use PGROUNDDOWN(fp) (see kernel/riscv.h) to identify the page that a frame pointer refers to.

我們可以通過這個hint知道,我們保存的地址的偏移量是-8,而想要得到上一個幀地址,就需要-16,然后繼續以此為-8為偏移量去得到我們的保存的return地址,并且在遇到頁的邊緣的時候,我們就會停止回溯。

于是,我們的backtrace代碼就可以寫出來了:

void backtrace(void) {printf("backtrace:\n");uint64 ra, fp = r_fp();// 獲取前一個幀指針的位置,位于當前幀指針 fp - 16 的位置// 按照調用約定,fp-8 是返回地址,fp-16 是上一個函數的幀指針uint64 pre_fp = *((uint64*)(fp - 16));// 當上一個幀指針和當前幀指針還在同一個物理頁中(即沒有越過頁邊界)時,繼續回溯while (PGROUNDDOWN(fp) == PGROUNDDOWN(pre_fp)) {ra = *(uint64 *)(fp - 8);printf("%p\n", (void*)ra);// 更新當前幀指針為上一個幀指針fp = pre_fp;// 繼續獲取上一個幀的幀指針pre_fp = *((uint64*)(fp - 16));}// 打印最后一個返回地址(最后一個棧幀)ra = *(uint64 *)(fp - 8);printf("%p\n", (void*)ra);
}

除此之外,記得在kernel/defs.h定義我們的backtrace函數,并且將這個函數添加到sys_sleep中。

這樣,backtrace就算完成了。

3. Alarm

實驗要求是注冊一個時間間隔和函數到當前的cpu,到點的時候就會調用這個函數,并且期間要求恢復我們的當前進程的上下文(寄存器)不受影響,簡單來講,就是一個非常tiny的trap。

首先我們閱讀hint,這個實驗不讀hint真的是沒法做。

  • You’ll need to modify the Makefile to cause alarmtest.c to be compiled as an xv6 user program.

  • The right declarations to put in user/user.h are:

        int sigalarm(int ticks, void (*handler)());int sigreturn(void);
    
  • Update user/usys.pl (which generates user/usys.S), kernel/syscall.h, and kernel/syscall.c to allow alarmtest to invoke the sigalarm and sigreturn system calls.

  • For now, your sys_sigreturn should just return zero.

  • Your sys_sigalarm() should store the alarm interval and the pointer to the handler function in new fields in the proc structure (in kernel/proc.h).

  • You’ll need to keep track of how many ticks have passed since the last call (or are left until the next call) to a process’s alarm handler; you’ll need a new field in struct proc for this too. You can initialize proc fields in allocproc() in proc.c.

  • Every tick, the hardware clock forces an interrupt, which is handled in usertrap() in kernel/trap.c.

  • You only want to manipulate a process’s alarm ticks if there’s a timer interrupt; you want something like

        if(which_dev == 2) ...
    
  • Only invoke the alarm function if the process has a timer outstanding. Note that the address of the user’s alarm function might be 0 (e.g., in user/alarmtest.asm, periodic is at address 0).

  • You’ll need to modify usertrap() so that when a process’s alarm interval expires, the user process executes the handler function. When a trap on the RISC-V returns to user space, what determines the instruction address at which user-space code resumes execution?

  • It will be easier to look at traps with gdb if you tell qemu to use only one CPU, which you can do by running

        make CPUS=1 qemu-gdb
    
  • You’ve succeeded if alarmtest prints “alarm!”.

  • Your solution will require you to save and restore registers—what registers do you need to save and restore to resume the interrupted code correctly? (Hint: it will be many).
  • Have usertrap save enough state in struct proc when the timer goes off that sigreturn can correctly return to the interrupted user code.
  • Prevent re-entrant calls to the handler----if a handler hasn’t returned yet, the kernel shouldn’t call it again. test2 tests this.
  • Make sure to restore a0. sigreturn is a system call, and its return value is stored in a0.

這些hint可謂是信息量很大了,簡單梳理一下,我們先將需要的系統調用框架先搭好:

makefile

UPROGS=\$U/_cat\$U/_echo\$U/_forktest\$U/_grep\$U/_init\$U/_kill\$U/_ln\$U/_ls\$U/_mkdir\$U/_rm\$U/_sh\$U/_stressfs\$U/_usertests\$U/_grind\$U/_wc\$U/_zombie\// 添加這一行$U/_alarmtest\

user/usys.pl

entry("sigalarm");
entry("sigreturn");

user/user.h

// lab
int sigalarm(int ticks, void (*handler)());
int sigreturn(void);

kernel/syscall.h

#define SYS_sigalarm 22
#define SYS_sigreturn 23

kernel/syscall.c

[SYS_sigalarm] sys_sigalarm,
[SYS_sigreturn] sys_sigreturn
// 這部分加在數組里面,做過之前的lab懂得都懂

目前我們大體的框架是弄好了,隨后著手去看我們的hint,我們可以知道,如果發生了定時器中斷,我們的which_dev就是2,hint告訴了我們這一點,于是,我們可以在這一部分代碼塊寫下我們的中斷邏輯,但是這部分應該如何去寫呢?我們需要去執行我們之前注冊的函數,并且需要保存當前的trapframe,保證之后還能夠回到這里,并且還需要去判斷計時器的時間,并且做一些加減操作,所以,我們在此之前,還需要對我們的proc結構體進行一些修改:

kernel/proc.h

//為proc結構體添加以下字段uint64 interval;              // 間隔void (*handler)();            // 定時處理的函數uint64 ticks;                 // 上一次調用函數距離的時間struct trapframe *alarm_trapframe;  // 用于恢復 trapframeint alarm_goingoff;           // 是否正在alarm,防止嵌套的中斷,導致trapframe丟失

我們既然多了這么多字段,那么必須要在allocproc里面,也為這些字段進行初始化

static struct proc*
allocproc(void)
{//...found://...if((p->alarm_trapframe = (struct trapframe *)kalloc()) == 0) {freeproc(p);release(&p->lock);return 0;}p->ticks = 0;p->handler = 0;p->interval = 0;p->alarm_goingoff = 0;//...return p;
}

同時,在釋放proc的時候,也需要執行對應的操作:

static void
freeproc(struct proc *p)
{//...// free alarm trapframeif(p->alarm_trapframe)kfree((void*)p->alarm_trapframe);p->alarm_trapframe = 0;//...p->ticks = 0;p->handler = 0;p->interval = 0;p->alarm_goingoff = 0;p->state = UNUSED;
}

隨后,我們需要去編寫我們的具體的系統調用的邏輯,sigalarm和sigreturn

uint64
sys_sigalarm(void) {int n;uint64 handler;// 獲取參數argint(0, &n);argaddr(1, &handler);// 調用下一層return sigalarm(n, (void(*)())(handler));
}uint64
sys_sigreturn(void) {return sigreturn();
}

我們的sigreturn和sigalarm定義在trap.c

int sigalarm(int ticks, void(*handler)()) {// 初始化alarmstruct proc *p = myproc();p->interval = ticks;p->handler = handler;p->ticks = 0;return 0; 
}int sigreturn() {struct proc *p = myproc();// 恢復之前的trapframe,并清除alarm標志位*(p->trapframe) = *(p->alarm_trapframe);p->alarm_goingoff = 0;// 這里返回a0的原因是,當我們執行return的時候,返回值會被保存在a0中// 導致a0被覆蓋,所以此時直接返回a0即可,我們在最后會進行分析return p->trapframe->a0;
}

當然,這兩個函數還需要在kernel/defs.h中聲明,否則會報錯!

最后,回到我們的usertrap函數,我們會在這里完成最后的工作

void
usertrap(void)
{//...// give up the CPU if this is a timer interrupt.if(which_dev == 2) {if(p->interval != 0) { // 如果設定了時鐘事件if(p->ticks++ == p->interval) {if(!p->alarm_goingoff) { // 確保沒有時鐘正在運行p->ticks = 0;*(p->alarm_trapframe) = *(p->trapframe);p->trapframe->epc = (uint64)p->handler;p->alarm_goingoff = 1;}}}yield();}usertrapret();
}

我們在which_dev滿足等于2的條件的時候,會增加我們的時鐘計時,當達到我們的間隔時間,就會保存我們的trapframe,并且修改我們的epc,epc是什么?就是我們返回用戶態的時候,會執行的代碼的指針,我們將需要執行的函數的地址賦給epc,也就是說,我們接下來就會去執行它,當然,如果需要我們的之前執行的函數能夠恢復,也就意味著,我們需要在注冊的函數里面主動去調用sigreturn,然后才能恢復到我們原來的用戶態的中斷的地方,這樣,就完成了這個系統調用的閉環。

回到剛剛的問題,為什么要返回a0?

我們可以查看匯編代碼來解決這個問題

kernel/kernel.asm

  return p->trapframe->a0;80001c44:	6d3c                	ld	a5,88(a0)      # 加載 p->trapframe 的地址到 a5,偏移 88 字節是 trapframe*
}80001c46:	5ba8                	lw	a0,112(a5)     # 加載 trapframe->a0 的值到 a0,偏移 112 字節是 a0 寄存器的位置80001c48:	60a2                	ld	ra,8(sp)       # 恢復調用者的返回地址(ra)80001c4a:	6402                	ld	s0,0(sp)       # 恢復調用者的幀指針(s0)80001c4c:	0141                	addi	sp,sp,16       # 恢復棧指針(釋放本函數棧幀)80001c4e:	8082                	ret              # 返回到調用者,返回值已保存在 a0 中

我們可以看見,我們會將返回的代碼賦給a0,但是即便如此,我們的a5也會被覆蓋,所以最好的辦法還是自己用匯編來實現這些上下文的切換。

那么最后,我們的alarm實驗就完成了。

== Test backtrace test == 
$ make qemu-gdb
backtrace test: OK (2.6s) 
== Test running alarmtest == 
$ make qemu-gdb
(4.8s) 
== Test   alarmtest: test0 == alarmtest: test0: OK 
== Test   alarmtest: test1 == alarmtest: test1: OK 
== Test   alarmtest: test2 == alarmtest: test2: OK 
== Test   alarmtest: test3 == alarmtest: test3: OK 
== Test usertests == 
$ make qemu-gdb
usertests: OK (151.6s) 

即便之前讀過了系統調用陷入的一系列代碼,通過寫這個lab4的實驗,也是比較困難的,但也能學到一些東西的,雖然中途確實看了別人的代碼,但是總歸是寫出來的,重要的不是看了別人的多少的代碼,我倒是覺得這并不可恥,在一些無聊的地方卡住好幾個小時沒有一點進展,而因為秉持著學術誠信最后卻因為一些bug而放棄,這反倒是我最不想看到的,最重要的是從這個實驗中學到了多少,所以,在這里,我將自己學到的分享出去,希望能夠幫助更多的人。

參考文獻:

miigon’blog

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/902181.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/902181.shtml
英文地址,請注明出處:http://en.pswp.cn/news/902181.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

一文總結通信電路中LC諧振回路中各公式以及對深入解讀品質因數Q

目錄 前言 一、基本公式總結 1.并聯諧振回路 2.串聯諧振回路 二、淺談品質因數 1.衡量諧振回路能量存儲與能量損耗之比的無量綱參數&#xff0c;用于描述諧振電路的頻率選擇性 2.當受到振蕩驅動力時&#xff0c;諧振腔的中心頻率與其帶寬的比值 3.為什么諧振時電容上的…

Linux:文件系統

一.認識硬件–磁盤 1. 物理結構 1.2 存儲結構 ?如何定位?個扇區呢&#xff1f; 可以先定位磁頭&#xff08;header&#xff09;——》確定磁頭要訪問哪?個柱?(磁道)&#xff08;cylinder&#xff09;——》 定位?個扇區(sector)。 柱?&#xff08;cylinder&#xff09…

數字孿生廢氣處理工藝流程

圖撲數字孿生廢氣處理工藝流程系統。通過精準 3D 建模&#xff0c;對廢氣收集、預處理、凈化、排放等全流程進行 1:1 數字化復刻&#xff0c;實時呈現設備運行參數、污染物濃度變化等關鍵數據。 借助圖撲可視化界面&#xff0c;管理者可直觀掌握廢氣處理各環節狀態&#xff0c…

Scratch——第18課 列表接龍問題

在四級的考級中&#xff0c;接龍的題目雖然在CIE中只出現過兩次&#xff0c;但是這類題目對字符串的知識點考察相對全面。 一、接龍游戲的判斷方法 接龍的內容對應的字符數 ? 已接龍內容的字符數 滿足條件>接龍內容的第一個字符數 ? 上一項接龍的最后一個字符 滿足條件…

webgl入門實例-向量在圖形學中的核心作用

在圖形學中&#xff0c;向量是描述幾何、光照、運動等核心概念的基礎工具。以下是向量在圖形學中的關鍵應用和深入解析&#xff1a; 1. 向量的核心作用 幾何表示&#xff1a;描述點、方向、法線、切線等。空間變換&#xff1a;平移、旋轉、縮放等操作依賴向量運算。光照計算&a…

Redis 是如何保證線程安全的?

Redis 是如何保證線程安全的&#xff1f; Redis 是一個高性能的鍵值數據庫&#xff0c;廣泛應用于緩存、消息隊列、實時分析等場景。由于其性能優勢&#xff0c;Redis 已經成為許多系統的核心組件之一。然而&#xff0c;很多開發者在使用 Redis 時&#xff0c;常常會問&#x…

Img2img-turbo 在2080Ti上的測試筆記

1. 介紹 [img2img-turbo]是[pytorch-CycleGAN-and-pix2pix]推薦的更新的圖像變換的代碼實現&#xff1b; 2. 配置信息 Conda環境名稱&#xff1a;img2img-turbo 3. 問題描述 當前在我們嘗試使用了官方推薦的訓練命令在2080Ti上進行訓練&#xff0c; 3.1 出現了 CUDA out …

代碼隨想錄算法訓練營第三十五天|416. 分割等和子集、698.劃分為k個相等的子集、473.火柴拼正方形

今日題目 416. 分割等和子集 題目鏈接&#xff1a;416. 分割等和子集 - 力扣&#xff08;LeetCode&#xff09; 思考&#xff1a;本題要將數組分為兩個子數組&#xff0c;且兩個子數組和相等&#xff0c;因此首先可以想到的條件就是數組可分為兩個&#xff0c;這要求數組元素數…

純CSS實現自動滾動到底部

<!DOCTYPE html> <html lang"zh-CN"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>自動滾動到底部</title><style>*…

【新人系列】Golang 入門(十五):類型斷言

? 個人博客&#xff1a;https://blog.csdn.net/Newin2020?typeblog &#x1f4dd; 專欄地址&#xff1a;https://blog.csdn.net/newin2020/category_12898955.html &#x1f4e3; 專欄定位&#xff1a;為 0 基礎剛入門 Golang 的小伙伴提供詳細的講解&#xff0c;也歡迎大佬們…

AI大模型發展現狀與MCP協議誕生的技術演進

1. 大模型能力邊界與用戶痛點&#xff08;2023年&#xff09; 代表模型&#xff1a;GPT-4&#xff08;OpenAI&#xff09;、Claude 3&#xff08;Anthropic&#xff09;、通義千問&#xff08;阿里云&#xff09;等展現出強大的生成能力&#xff0c;但存在明顯局限&#xff1a…

深入理解Linux中的線程控制:多線程編程的實戰技巧

個人主頁&#xff1a;chian-ocean 文章專欄-Linux 前言&#xff1a; POSIX線程&#xff08;Pthreads&#xff09; 是一種在 POSIX 標準下定義的線程庫&#xff0c;它為多線程編程提供了統一的接口&#xff0c;主要用于 UNIX 和類 UNIX 系統&#xff08;如 Linux、MacOS 和 BS…

(mac)Grafana監控系統之監控Linux的Redis

Grafana安裝-CSDN博客 普羅米修斯Prometheus監控安裝&#xff08;mac&#xff09;-CSDN博客 1.Redis_exporter安裝 直接下載 wget https://github.com/oliver006/redis_exporter/releases/download/v1.0.3/redis_exporter-v1.0.3.linux-amd64.tar.gz 解壓 tar -xvf redis_…

鴻蒙應用元服務開發-Account Kit未成年人模式訂閱和處理用戶信息變更

一、概述 通過訂閱用戶信息變更&#xff0c;您可以接收有關用戶及其賬戶的重要更新。當用戶取消元服務的授權信息、注銷華為賬號時&#xff0c;華為賬號服務器會發送通知到元服務&#xff0c;元服務可以根據通知消息進行自身業務處理。 二、用戶信息變更事件介紹 三、訂閱用…

buildroot構建根文件系統報錯(已解決大部分問題)

title: buildroot構建根文件系統報錯(set FORCE_UNSAFE_CONFIGURE1) author: cbus categories: 小知識 tags:小知識 abbrlink: 53691 date: 2025-04-20 08:03:00 錯誤1 set FORCE_UNSAFE_CONFIGURE1 在使用buildroot構建根文件系統時&#xff0c;一切按照文檔的配置&#xff0…

7.QT-常用控件-QWidget|font|toolTip|focusPolicy|styleSheet(C++)

font API說明font()獲取當前widget的字體信息.返回QFont對象.setFont(const QFont& font)設置當前widget的字體信息. 屬性說明family字體家族.?如"楷體",“宋體”,"微軟雅?"等.pointSize字體??weight字體粗細.以數值?式表?粗細程度取值范圍為[…

通過面向目標的獎勵彌合人與機器人的靈活性差距

24年10月來自紐約大學的論文“Bridging the Human to Robot Dexterity Gap through Object-Oriented Rewards”。 直接通過人類視頻訓練機器人是機器人技術和計算機視覺領域的一個新興領域。盡管雙指機械手在雙指夾持器方面取得了顯著進展&#xff0c;但以這種方式讓多指機械手…

C++入門篇(下)

目錄 1、引用 1.1 引用概念 1.2 引用特性 1.3 常引用 1.4 使用場景 1.4.1 引用做參數 1.4.2 引用做返回值 1.5 引用和指針的區別 2、內聯函數 2.1 概念 2.2 特性 3、auto關鍵字 4、基于范圍的for循環 5、指針空值nullptr 5.1 C98 中的指針空值處理 5.2 C11 …

Multi-Query Attention (MQA) PyTorch 實現

和多頭注意力機制的唯一區別&#xff1a;K、V在不同的head之間實現了復用&#xff0c;而對于不同的頭&#xff0c;Q依然不同。 因此這里的代碼和標準多頭注意力的實現也是幾乎完全一樣&#xff1a; import torch import torch.nn as nn import torch.nn.functional as Fclass…

visual studio無法跳轉到函數定義、變量定義、跳轉函數位置不準問題解決

參考&#xff1a;https://blog.csdn.net/snakehacker/article/details/135438353 程序有時會出現大部分函數都不能準確的從頭文件中正確定位到函數定位,這是因為數據庫錯亂造成的,可以通過重構數據庫來解決,操作方法如下&#xff1a; 菜單欄&#xff1a;工具——選項 文本編輯…