計算機組成原理Ⅱ課程設計 PA3.1


計算機組成原理Ⅱ課程設計 PA3.1

目錄

思考題

  1. 什么是操作系統?

    我認為操作系統是運行在硬件之上,管理硬件,提供一系列操作硬件的API,供程序使用。因為我自己用Windows比較多,對比Linux,最直觀感受是Windows提供的圖形界面方便了用戶使用,使計算機進入了平常百姓家,而不只僅局限於技術人員。

  2. 我們不⼀樣,嗎?

    nanos-lite在調用AM的API的基礎上,實現了更多的功能和接口,例如文件系統、終端異常處理、加載器等。

    我認為他們是同等地位。

  3. 操作系統的實質

    程序

  4. 程序真的結束了嗎?

    main函數執行之前,主要就是初始化系統相關資源:

    1. 設置棧指針

    2. 初始化static靜態和global全局變量,即data段的內容

    3. 將未初始化部分的賦初值:數值型short,int,long等為0,bool為FALSE,指針為NULL,等等,即.bss段的內容

    4. 運行全局構造器,估計是C++中構造函數之類的吧

    5. 將main函數的參數,argc,argv等傳遞給main函數,然后才真正運行main函數main

    main函數執行之后:

    1. 獲取main的返回值
    2. 調用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

  5. 觸發系統調用

    編寫了一個程序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
    

    結果:成功輸出

  6. 有什么不同?

    與函數調用過程十分類似。函數調用時,獲取調用函數的起始地址,並跳轉過去。在觸發函數調用前,會保存相關寄存器到棧中,函數調用完畢后再恢復。

    可以。系統調用會根據系統調用號在 IDT 中索引,取得該調用號對應的系統調用服務程序的地址,並跳轉過去。在觸發系統調用前,會保護用戶相關狀態寄存器(EFLAGS, EIP等)到棧中,系統調用完畢后再恢復。這個過程與函數調用的過程基本一致,因此可以認為系統調用的服務程序理解為一個比較特殊的“函數”。

    “服務程序”的工作都是硬件自動完成的,不需要程序員編寫指令來完成相應的內容。在函數運行的過程中遇到異常,才會觸發。由於“服務程序”和“用戶程序”使用的是不同的堆棧,因此在觸發“系統程序”時,涉及到堆棧的切換。

  7. 段錯誤

    編譯的過程是吧高級語言轉化為低級語言的過程,期間會檢查語法,但是具體程序中指令會跳轉到什么地方,編譯階段不負責檢查。只有在程序執行中,訪問到不訪問的地方時,才會觸發段錯誤。

    段錯誤就是指訪問的內存超出了系統所給這個程序的內存空間。基本是是錯誤地使用指針引起的:

    • 訪問系統數據區
    • 內存越界,訪問到不屬於程序的內存區域(數組越界,變量類型不一致)
    • 多線程程序使用了不安全的函數
    • 多線程讀寫數據未加鎖保護
    • 堆棧溢出

    http://www.360doc.com/content/18/0331/16/21305584_741796830.shtml

  8. 對比異常與函數調用

    異常 保存寄存器、錯誤碼#irq、 EFLAGS、CS、 EIP,形成了 trap frame(陷阱幀)的數據結構。

    函數調用 調用者保存寄存器和被調用者保存寄存器(不一定保存)

    在異常處理的時候已經切換了棧幀,所以要保存更多的信息。

  9. 詭異的代碼

    以指向trapframe 內容的指針(esp)作為參數,調用trap函數。把eip作為入口參數傳進去,然后在執行irq_handle這個函數之前,通過pusha,在棧幀中形成了_RegSer這個結構體,把eip作為一個結構體的起始地址,通過成員irq來分發事件。

  10. 注意區分事件號和系統調用號

    事件號:標明我們一個未實現的系統調用事件的編號。

    系統調用號:在識別出系統調用事件后,從寄存器中取出系統調用號和輸入參數,根據系統調用號查找系統調用分派表,執行相應的處理函數,並記錄返回值。

  11. 打印不出來?

    printf打印是行緩沖,讀取到的字符串會先放到緩沖區里,直到一行結束或者整個程序結束,才輸出到屏幕,因為我們打印的字符串一行沒有結束,所以就先執行后面的*p=NULL報錯了。

    因此,我們要讓他輸出字符串內容,只需要在字符串后面加上\n就表明一行結束,可以輸出了。

    #include <stdio.h>
    int main(){
        int *p = NULL;
        printf("I am here!\n");//這里加上換行
        *p = 10;
        return 0;
    }
    
  12. 理解文件管理函數

    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

  13. 不再神秘的秘技

    游戲bug,開發時沒有注意變量類型等問題,造成在某些特定情況下,會出現溢出現象。

  14. 必答題

    存檔讀取: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接口顯示到屏幕上面。

  15. git loggit 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

  1. 首先用fs_open()根據文件名獲取文件位置
  2. 再用fs_filesz()獲取文件大小
  3. 用fs_read()讀取指定位置指定長度的內容
  4. 最后用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() 會壓入錯誤碼和異常號 #irqasm_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 的值,如果 fd12(分別代表 stdoutstderr),則將 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()實現步驟,講義明確列出來了:

  1. program break 一開始的位置位於 _end
  2. 被調用時,根據記錄的 program break 位置和參數 increment,計算出新 program break
  3. 通過 SYS_brk 系統調用來讓操作系統設置新 program break
  4. SYS_brk 系統調用成功,該系統調用會返回 0,此時更新之前記錄的 program break 的位置,並將舊 program break 的位置作為 _sbrk() 的返回值返回
  5. 若該系統調用失敗,_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

遇到的問題及解決辦法

  1. 遇到問題:實現堆區管理時,一直是按字符輸出,當時搞了好久。

    解決方案:全局make clean,然后重新編譯所有文件,過了。后面也遇到了相似問題,只要記得重新make,就解決了。

  2. 遇到問題:_syscall_()對這個函數不太懂,當時思考了很久,也讀了很多代碼,不知道系統調用的幾個函數該怎么填參數。

    解決方案:模仿框架已經寫好的_exit(),按順序把參數填到對應位置上,居然就過了。

實驗心得

本次實驗感覺難度不小,涉及到了很多計算機底層的知識,閱讀代碼花了我很長很長時間,雖然pa3.1做完了,代碼能跑了,但有些地方的調用關系以及思路還是不太理解,希望在后面的學習中能夠逐漸理解整個計算機底層是怎么運行的。

其他備注

助教真帥


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM