計算機組成原理Ⅱ課程設計 PA3.1
目錄
思考題
-
什么是操作系統?
我認為操作系統是運行在硬件之上,管理硬件,提供一系列操作硬件的API,供程序使用。因為我自己用Windows比較多,對比Linux,最直觀感受是Windows提供的圖形界面方便了用戶使用,使計算機進入了平常百姓家,而不只僅局限於技術人員。
-
我們不⼀樣,嗎?
nanos-lite
在調用AM
的API的基礎上,實現了更多的功能和接口,例如文件系統、終端異常處理、加載器等。我認為他們是同等地位。
-
操作系統的實質
程序
無
-
程序真的結束了嗎?
main函數執行之前,主要就是初始化系統相關資源:
-
設置棧指針
-
初始化static靜態和global全局變量,即data段的內容
-
將未初始化部分的賦初值:數值型short,int,long等為0,bool為FALSE,指針為NULL,等等,即.bss段的內容
-
運行全局構造器,估計是C++中構造函數之類的吧
-
將main函數的參數,argc,argv等傳遞給main函數,然后才真正運行main函數main
main函數執行之后:
- 獲取main的返回值
- 調用exit退出程序
int main(int argc, char *argv[], char *envp[]); __attribute__((section(".text.unlikely"))) void _start(int argc, char *argv[], char *envp[]) { int ret = main(argc, argv, envp); exit(ret); asser(t0); }
https://blog.csdn.net/wang_jing_2008/article/details/7946450
-
-
觸發系統調用
編寫了一個程序
hello.c
const char str[] = "Hello world!\n"; int main() { asm volatile ("movl $4, %eax;" // system call ID, 4 = SYS_write "movl $1, %ebx;" // file descriptor, 1 = stdout "movl $str, %ecx;" // buffer address "movl $13, %edx;" // length "int $0x80"); return 0; }
編譯運行
gcc hello.c -o hello -m32
結果:成功輸出
-
有什么不同?
與函數調用過程十分類似。函數調用時,獲取調用函數的起始地址,並跳轉過去。在觸發函數調用前,會保存相關寄存器到棧中,函數調用完畢后再恢復。
可以。系統調用會根據系統調用號在 IDT 中索引,取得該調用號對應的系統調用服務程序的地址,並跳轉過去。在觸發系統調用前,會保護用戶相關狀態寄存器(EFLAGS, EIP等)到棧中,系統調用完畢后再恢復。這個過程與函數調用的過程基本一致,因此可以認為系統調用的服務程序理解為一個比較特殊的“函數”。
“服務程序”的工作都是硬件自動完成的,不需要程序員編寫指令來完成相應的內容。在函數運行的過程中遇到異常,才會觸發。由於“服務程序”和“用戶程序”使用的是不同的堆棧,因此在觸發“系統程序”時,涉及到堆棧的切換。
-
段錯誤
編譯的過程是吧高級語言轉化為低級語言的過程,期間會檢查語法,但是具體程序中指令會跳轉到什么地方,編譯階段不負責檢查。只有在程序執行中,訪問到不訪問的地方時,才會觸發段錯誤。
段錯誤就是指訪問的內存超出了系統所給這個程序的內存空間。基本是是錯誤地使用指針引起的:
- 訪問系統數據區
- 內存越界,訪問到不屬於程序的內存區域(數組越界,變量類型不一致)
- 多線程程序使用了不安全的函數
- 多線程讀寫數據未加鎖保護
- 堆棧溢出
http://www.360doc.com/content/18/0331/16/21305584_741796830.shtml
-
對比異常與函數調用
異常
保存寄存器、錯誤碼#irq
、 EFLAGS、CS、 EIP,形成了 trap frame(陷阱幀)的數據結構。函數調用
調用者保存寄存器和被調用者保存寄存器(不一定保存)在異常處理的時候已經切換了棧幀,所以要保存更多的信息。
-
詭異的代碼
以指向trapframe 內容的指針(esp)作為參數,調用trap函數。把
eip
作為入口參數傳進去,然后在執行irq_handle
這個函數之前,通過pusha
,在棧幀中形成了_RegSer
這個結構體,把eip
作為一個結構體的起始地址,通過成員irq
來分發事件。 -
注意區分事件號和系統調用號
事件號:標明我們一個未實現的系統調用事件的編號。
系統調用號:在識別出系統調用事件后,從寄存器中取出系統調用號和輸入參數,根據系統調用號查找系統調用分派表,執行相應的處理函數,並記錄返回值。
-
打印不出來?
printf打印是行緩沖,讀取到的字符串會先放到緩沖區里,直到一行結束或者整個程序結束,才輸出到屏幕,因為我們打印的字符串一行沒有結束,所以就先執行后面的
*p=NULL
報錯了。因此,我們要讓他輸出字符串內容,只需要在字符串后面加上
\n
就表明一行結束,可以輸出了。#include <stdio.h> int main(){ int *p = NULL; printf("I am here!\n");//這里加上換行 *p = 10; return 0; }
-
理解文件管理函數
fs_open
:按照文件名在file_table
里面搜索,若匹配到對應的文件名,則把該文件的讀寫指針標為0並返回文件的位置(即第幾個index);若未匹配到,則報錯並返回-1。fs_read
:若文件標號小於2,報錯。若fd為FD_EVENTS
,則調用events_read()
讀取指定位置指定長度的內容並返回。根據fd計算文件開始的位置,然后計算該文件的剩余字節數remain_bytes
,並根據fd選擇讀取方式。最后更新讀寫指針。fs_write
:根據fd計算文件開始位置,並計算該文件的剩余字節數remain_bytes
,根據fd不同,選擇不同的寫方式,最后更新讀寫指針。fs_lseek
:根據fd計算文件開始位置,獲取文件的讀寫指針位置和文件大小,然后根據whence
選擇不同方式來對new_offset
更新。若更新后,new_offset
小於0或者大於文件大小,則把其置為0或文件大小並返回。fs_close
:返回0 -
不再神秘的秘技
游戲bug,開發時沒有注意變量類型等問題,造成在某些特定情況下,會出現溢出現象。
-
必答題
存檔讀取
:PAL_LoadGame()先打開指定文件然后調用fread()從文件里讀取存檔相關信息(其中包括調用nanos.c里的_read()以及syscall.c中的sys_raed()),隨后關閉文件並把讀取到的信息賦值(用fs_write()修改),接着使用AM提供的memcpy()拷貝數據,最后使用nemu的內存映射I/O修改內存。更新屏幕
:redraw()調用ndl.c里面的NDL_DrawRect()來繪制矩形,NDL_Render()把VGA顯存抽象成文件,它們都調用了nan0s-lite
中的接口,最后nemu把文件通過I/O接口顯示到屏幕上面。 -
git log
和git branch
截圖
實驗內容
實現 loader
1.實現簡單 loader,觸發未實現指令 int
根據講義,我們只需要用到 ramdisk_read 函數,其中第一個參數填入 DEFAULT_ENTRY
,偏移量為 0,長度為 ramdisk 的大小即可。
在loader.c
中,別忘了聲明外部函數
extern void ramdisk_read(void *buf, off_t offset, size_t len);
extern size_t get_ramdisk_size();
更新loader()
uintptr_t loader(_Protect *as, const char *filename) {
ramdisk_read(DEFAULT_ENTRY,0,get_ramdisk_size());
return (uintptr_t)DEFAULT_ENTRY;
}
2.實現引入文件系統后的 loader
- 首先用fs_open()根據文件名獲取文件位置
- 再用fs_filesz()獲取文件大小
- 用fs_read()讀取指定位置指定長度的內容
- 最后用fs_close()關閉文件
//讀取文件位置
int index=fs_open(filename,0,0);
//讀取長度
int length=fs_filesz(index);
//讀取內容
fs_read(index,DEFAULT_ENTRY,length);
//關閉文件
fs_close(index);
別忘了引入頭文件
#include "../include/fs.h"
添加寄存器和 LIDT 指令
1.根據 i386 ⼿冊正確添加 IDTR 和 CS 寄存器
根據手冊,可知IDTR
中base32位,limit16位。cs16位
struct {
uint32_t base; //32位base
uint16_t limit; //16位limit
}idtr;
uint16_t cs;
2.在 restart() 中正確設置寄存器初始值
根據講義可知cs寄存器需要初始化為8
static inline void restart() {
/* Set the initial instruction pointer. */
cpu.eip = ENTRY_START;
cpu.eflags.value=0x2;//eflags賦初始值
cpu.cs=0x8;
#ifdef DIFF_TEST
init_qemu_reg();
#endif
}
3.LIDT 指令細節可在 i386 ⼿冊中找到
查表可知,LIDT
在gpr7中
填表
make_group(gp7,
EMPTY, EMPTY, EMPTY, EX(lidt),
EMPTY, EMPTY, EMPTY, EMPTY)
若OperandSize
是16,則limit讀取16位,base讀取24位
若OperandSize
是32,則limit讀取16位,base讀取32位
make_EHelper(lidt) {
cpu.idtr.limit=vaddr_read(id_dest->addr,2);//limit16
if (decoding.is_operand_size_16) {
cpu.idtr.base=vaddr_read(id_dest->addr+2,3);//base24
} else
{
cpu.idtr.base=vaddr_read(id_dest->addr+2,4);//base32
}
print_asm_template1(lidt);
}
實現 INT 指令
1.實現寫在 raise_intr() 函數中
通過觀看視頻,可知該函數的具體實現步驟
void raise_intr(uint8_t NO, vaddr_t ret_addr) {
/* TODO: Trigger an interrupt/exception with ``NO''.
* That is, use ``NO'' to index the IDT.
*/
//獲取門描述符
vaddr_t gate_addr=cpu.idtr.base+8*NO;
//P位校驗
if (cpu.idtr.limit<0){
assert(0);
}
//將eflags、cs、返回地址壓棧
rtl_push(&cpu.eflags.value);
rtl_push(&cpu.cs);
rtl_push(&ret_addr);
//組合中斷處理程序入口點
uint32_t high,low;
low=vaddr_read(gate_addr,4)&0xffff;
high=vaddr_read(gate_addr+4,4)&0xffff0000;
//設置eip跳轉
decoding.jmp_eip=high|low;
decoding.is_jmp=true;
}
2. 使⽤ INT 的 helper 函數調⽤ raise_intr()
執行 int
指令后保存的 EIP
指向的是 int
指令的下一條指令,所以第二個參數是decoding.seq_eip
make_EHelper(int) {
raise_intr(id_dest->val,decoding.seq_eip);
print_asm("int %s", id_dest->str);
#ifdef DIFF_TEST
diff_test_skip_nemu();
#endif
}
3.指令細節可在 i386 ⼿冊中找到
填表
/* 0xcc */ EX(int3), IDEXW(I,int,1), EMPTY, EMPTY,
成功運行
實現其他相關指令和結構體
1.組織 _RegSet 結構體,需要說明理由
根據講義可知,現場保存的順序為:①硬件保存 EFLAGS, CS, EIP ②vecsys()
會壓入錯誤碼和異常號 #irq
③ asm_trap()
會把用戶進程的通用寄存器保存到堆棧上
則恢復的時候倒序恢復
那么,我們就可知 _RegSet
的組織方式了
struct _RegSet {
uintptr_t edi,esi,ebp,esp,ebx,edx,ecx,eax;
int irq;
uintptr_t error_code,eip,cs,eflags;
};
運行截圖在下一問展示
2.pusha
填表
/* 0x60 */ EX(pusha), EMPTY, EMPTY, EMPTY,
3.popa
填表
/* 0x60 */ EX(pusha), EX(popa), EMPTY, EMPTY,
按手冊順序pop即可
make_EHelper(popa) {
rtl_pop(&cpu.edi);
rtl_pop(&cpu.esi);
rtl_pop(&cpu.ebp);
rtl_pop(&t0);
rtl_pop(&cpu.ebx);
rtl_pop(&cpu.edx);
rtl_pop(&cpu.ecx);
rtl_pop(&cpu.eax);
print_asm("popa");
}
3.iret
填表
/* 0xcc */ EX(int3), IDEXW(I,int,1), EMPTY, EX(iret),
根據手冊,按順序eip cs eflags
出棧即可
make_EHelper(iret) {
ret_pop(&decoding.jmp_eip);
decoding.is_jmp=1;
rtl_pop(&cpu.cs);
rtl_pop(&cpu.eflags.value);
print_asm("iret");
}
完善事件分發和 do_syscall
1.完善 do_event,⽬前階段僅需要識別出系統調⽤事件即可
按照講義,識別系統調用事件 _EVENT_SYSCALL
,然后調用 do_syscall()
即可
別忘了聲明函數do_syscall()
extern _RegSet* do_syscall(_RegSet *r);
static _RegSet* do_event(_Event e, _RegSet* r) {
switch (e.event) {
case _EVENT_SYSCALL:
do_syscall(r);
break;
default: panic("Unhandled event ID = %d", e.event);
}
return NULL;
}
2.添加整個階段中的所有系統調⽤(none, exit, brk, open, write, read, lseek, close)
實現SYSCALL_ARGx(r)
宏,根據講義提示,很容易實現
#define SYSCALL_ARG1(r) r->eax
#define SYSCALL_ARG2(r) r->ebx
#define SYSCALL_ARG3(r) r->ecx
#define SYSCALL_ARG4(r) r->edx
完善do_syscall()
,在其中添加如下代碼
a[0] = SYSCALL_ARG1(r);
a[1] = SYSCALL_ARG2(r);
a[2] = SYSCALL_ARG3(r);
a[3] = SYSCALL_ARG4(r);
-
none
編寫
sys_none()
,該函數什么也不做,返回1,不要忘記設置系統調用的返回值static inline uintptr_t sys_none(_RegSet *r) { //設置系統調用的返回值 SYSCALL_ARG1(r)=1; return 1; }
成功
-
exit
講義中說:你需要實現
SYS_exit
系統調用,它會接收一個退出狀態的參數,用這個參數調用_halt()
即可。這里這個退出狀態的參數我不明白怎么找的,反正就是4個參數,挨個試,到最后發現
SYSCALL_ARG2(r)
成功了static inline uintptr_t sys_exit(_RegSet *r) { _halt(SYSCALL_ARG2(r)); return 1; }
-
write
檢查
fd
的值,如果fd
是1
或2
(分別代表stdout
和stderr
),則將buf
為首地址的len
字節輸出到串口(使用_putc()
即可)。fs_write()
符合上述要求。填寫參數的時候,注意buf類型static inline uintptr_t sys_write(uintptr_t fd, uintptr_t buf, uintptr_t len) { return fs_write(fd,(void *)buf,len); }
最后還要設置正確的返回值,否則系統調用的調用者會認為
write
沒有成功執行。返回什么,通過man 2 write
可知,要返回寫的字符的bytes,正好是fs_write()
的返回值由於
sys_write()
沒有參數r
,因此我在do_syscall()
中填寫返回case SYS_write: SYSCALL_ARG1(r)=sys_write(a[1],a[2],a[3]); break;
在
navy-apps/libs/libos/src/nanos.c
的_write()
中調用系統調用接口函數通過閱讀
_syscall_()
參數,可知要傳系統調用類型、參數一參數二、參數三int _write(int fd, void *buf, size_t count){ _syscall_(SYS_write,fd,(uintptr_t)buf,count); }
運行成功截圖
這里目前只需要實現這三個就可以跑通,然后我就接着往下做了。
最后整體做完,我按照要求來這補了一下系統調用。關鍵是別忘了把返回值存入SYSCALL_ARG1(r)
_RegSet* do_syscall(_RegSet *r) {
uintptr_t a[4];
a[0] = SYSCALL_ARG1(r);
a[1] = SYSCALL_ARG2(r);
a[2] = SYSCALL_ARG3(r);
a[3] = SYSCALL_ARG4(r);
switch (a[0]) {
case SYS_none:
sys_none(r);
break;
case SYS_exit:
sys_exit(r);
break;
case SYS_write:
SYSCALL_ARG1(r)=(int)sys_write(a[1],a[2],a[3]);
break;
case SYS_brk:
SYSCALL_ARG1(r)=(int)sys_brk(r);
break;
case SYS_open:
SYSCALL_ARG1(r)=(int)sys_open(a[1],a[2],a[3]);
break;
case SYS_read:
SYSCALL_ARG1(r)=(int)sys_read(a[1],a[2],a[3]);
break;
case SYS_close:
SYSCALL_ARG1(r)=(int)sys_close(a[1]);
break;
case SYS_lseek:
SYSCALL_ARG1(r)=(int)sys_lseek(a[1],a[2],a[3]);
break;
default: panic("Unhandled syscall ID = %d", a[0]);
}
實現堆區管理
在 Nanos-lite 中實現 SYS_brk
系統調用。由於目前 Nanos-lite 還是一個單任務操作系統,空閑的內存都可以讓用戶程序自由使用,因此我們只需要讓 SYS_brk
系統調用總是返回 0
即可,表示堆區大小的調整總是成功。
case SYS_brk:
SYSCALL_ARG1(r)=0;
break;
具體_sbrk()
實現步驟,講義明確列出來了:
- program break 一開始的位置位於
_end
- 被調用時,根據記錄的 program break 位置和參數
increment
,計算出新 program break - 通過
SYS_brk
系統調用來讓操作系統設置新 program break - 若
SYS_brk
系統調用成功,該系統調用會返回0
,此時更新之前記錄的 program break 的位置,並將舊 program break 的位置作為_sbrk()
的返回值返回 - 若該系統調用失敗,
_sbrk()
會返回-1
extern char _end;//聲明外部變量
static intptr_t brk=(intptr_t)&_end;//記錄開始位置
void *_sbrk(intptr_t increment){
intptr_t pre = brk;
intptr_t now=pre+increment;//記錄增加后的位置
intptr_t res = _syscall_(SYS_brk,now,0,0);//系統調用
if (res==0){//若成功,則返回原位置
brk=now;
return (void*)pre;
}//否則返回-1
return (void *)-1;
}
重點注意,改完navy-apps/libs/libos/src/nanos.c
中的代碼,要記得重新編譯``navy-apps`!!!
實現系統調用
1.sys_open()
調用 fs_open
,根據給定路徑、標志和打開模式打開文件,注意pathname類型轉換
static inline uintptr_t sys_open(uintptr_t pathname, uintptr_t flags, uintptr_t mode) {
return fs_open((char *)pathname,flags,mode);
}
do_syscall中別忘了寫返回值
case SYS_open:
SYSCALL_ARG1(r)=(int)sys_open(a[1],a[2],a[3]);
break;
_open()
模仿已有的例子,別忘了類型轉換就可以
int _open(const char *path, int flags, mode_t mode) {
_syscall_(SYS_open,(uintptr_t)path,flags,mode);
}
2.sys_write()
調用 fs_write
,將給定緩沖區的指定長度個字節寫入指定文件號的文件中,注意buf類型轉換
static inline uintptr_t sys_write(uintptr_t fd, uintptr_t buf, uintptr_t len) {
return fs_write(fd,(void *)buf,len);
}
do_syscall中別忘了寫返回值
case SYS_write:
SYSCALL_ARG1(r)=(int)sys_write(a[1],a[2],a[3]);
break;
_write()
模仿已有的例子,別忘了類型轉換就可以
int _write(int fd, void *buf, size_t count){
_syscall_(SYS_write,fd,(uintptr_t)buf,count);
}
3.sys_read()
調用 fs_read
,從指定文件號的文件中讀取指定長度個字節到給定緩沖區中,注意buf類型轉換
static inline uintptr_t sys_write(uintptr_t fd, uintptr_t buf, uintptr_t len) {
return fs_write(fd,(void *)buf,len);
}
do_syscall中別忘了寫返回值
case SYS_read:
SYSCALL_ARG1(r)=(int)sys_read(a[1],a[2],a[3]);
break;
_read()
模仿已有的例子,別忘了類型轉換就可以
int _read(int fd, void *buf, size_t count) {
_syscall_(SYS_read,fd,(uintptr_t)buf,count);
}
4.sys_lseek()
已經實現
根據fd
確定傳入的文件位置,並以此確定文件大小以及讀寫指針位置。然后根據whence
來確定對指針進行操作,根據offset
對文件進行讀寫,並判斷是否刪除完畢或者寫入內容超過文件大小。最后返回文件最新的讀寫位置。
5.sys_close()
調用 fs_close
,關閉指定文件號的文件
static inline uintptr_t sys_close(uintptr_t fd) {
return fs_close(fd);
}
do_syscall中別忘了寫返回值
case SYS_close:
SYSCALL_ARG1(r)=(int)sys_close(a[1]);
break;
_close()
三四參數不用寫,傳入文件位置即可。
int _close(int fd) {
_syscall_(SYS_close,fd,0,0);
}
6.sys_brk()
前面已經實現
成功運⾏各測試⽤例
1.Hello world
2./bin/text
3./bin/bmptest
4./bin/events
5./bin/pal
遇到的問題及解決辦法
-
遇到問題:實現堆區管理時,一直是按字符輸出,當時搞了好久。
解決方案:全局make clean,然后重新編譯所有文件,過了。后面也遇到了相似問題,只要記得重新make,就解決了。
-
遇到問題:
_syscall_()
對這個函數不太懂,當時思考了很久,也讀了很多代碼,不知道系統調用的幾個函數該怎么填參數。解決方案:模仿框架已經寫好的
_exit()
,按順序把參數填到對應位置上,居然就過了。
實驗心得
本次實驗感覺難度不小,涉及到了很多計算機底層的知識,閱讀代碼花了我很長很長時間,雖然pa3.1做完了,代碼能跑了,但有些地方的調用關系以及思路還是不太理解,希望在后面的學習中能夠逐漸理解整個計算機底層是怎么運行的。
其他備注
助教真帥