PA2.1

寫在前面的話

如果您對該系列感興趣的話,推薦您先看一下南京大學的計算機組成原理實驗(也就是PA)的講義,然後再來看這篇文章可能有更多地收穫。如果您是要完成該作業的學生,我推薦你先看講義,或者好好聽老師的講課,然後自己獨立完成這個作業,但是如果你沒有聽懂,或者你無論如何也無法理解講義上面的字,又或者說對講義上面的某點知識某個問題不瞭解而又覺得太簡單不好意思問老師,那麼您可能會從這篇文章裏面獲得一些你需要的信息。本篇文章將會包括筆者自己做PA的所有經過,希望你並不將該文章當成抄襲的根源,而是成爲你思考的源泉。
現在已經到了PA2的階段,這才真正開始了組成原理的道路,在PA0是搭建環境,PA1複習複習C語言,寫寫功能函數,而現在纔開始真正去做計算機結構內部的東西,難度也上了一個檔次。

PA系列傳送門

PA0:https://blog.csdn.net/qq_41983842/article/details/88921427
PA1.1:https://blog.csdn.net/qq_41983842/article/details/88934779
PA1.2:https://blog.csdn.net/qq_41983842/article/details/89714479
PA1.3:https://blog.csdn.net/qq_41983842/article/details/89714689
PA2.1:https://blog.csdn.net/qq_41983842/article/details/95232055
PA2.2&2.3:https://blog.csdn.net/qq_41983842/article/details/101164495
PA3.1:https://blog.csdn.net/qq_41983842/article/details/103094859
PA3.2:https://blog.csdn.net/qq_41983842/article/details/103843093
PA4:https://blog.csdn.net/qq_41983842/article/details/104667951

思考題

  1. 設某指令執行前 eip 值爲 x1,該指令執行後 eip 值爲 x2,那麼 x2 - x1 的這個差值都包括了一條指令的哪些組成成分?

    opcode、操作數的地址、存儲地址、指令地址,指令類型不同包括不同的成分。

  2. opcode_table 數組中存放了所有指令的信息,請問表中每個表項是什麼類型?NEMU 又是如何通過這個表項得知操作數長度、應該使用哪個譯碼函數、哪個執行函數等信息的?

    opcode_entry類型,NEMU通過set_width知道操作數長度,通過make_DHelper函數知道要用哪個譯碼函數,通過make_EHelper函數知道要執行函數信息。

  3. 操作數結構體/共同體中都包括哪些成員,分別存儲什麼信息?他們是如何實現協同工作的?

    typedef struct {
      uint32_t type; //操作數類型
      int width; //操作數寬度
      union {
        uint32_t reg; //寄存器操作數
        rtlreg_t addr; //操作數的地址
        uint32_t imm; //立即操作數
        int32_t simm; //無符號立即操作數
      };
      rtlreg_t val;//RTL寄存器
      char str[OP_STR_SIZE];//操作數指令
    } Operand;
    
  4. 復現宏定義

    • make_EHelper(mov) //mov 指令的執行函數

      #define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)

      ->#define concat(x, y) concat_temp(x, y)

      ->#define concat_temp(x, y) x ## y

      把x和y這兩個參數粘在一起變成xy,所以在宏定義中將exec_mov粘在一起,執行mov操作

    • make_EHelper(push) //push 指令的執行函數

      #define make_EHelper(name) void concat(exec_, name) (vaddr_t *eip)

      ->#define concat(x, y) concat_temp(x, y)

      ->#define concat_temp(x, y) x ## y

      把x和y這兩個參數粘在一起變成xy,所以在宏定義中將exec_push粘在一起,執行push操作

    • make_DHelper(I2r) //I2r 類型操作數的譯碼函數

      #define make_DHelper(name) void concat(decode_, name) (vaddr_t *eip)

      與上一個一樣,把decode_I2r連接起來,執行immediate to register譯碼操作

    • IDEX(I2a, cmp) //cmp 指令的 opcode_table 表項

      #define IDEXW(id, ex, w) {concat(decode_, id), concat(exec_, ex), w}

      ->id變量表示譯碼函數參數名稱,傳給譯碼函數,ex變量表示執行函數參數名稱,傳給執行函數,w表示width,處理的數據的寬度。

    • EX(nop) //nop 指令的 opcode_table 表項

      #define EX(ex) EXW(ex, 0)

      ->ex變量表示執行函數參數名稱,傳給執行函數,EX(nop)=EXW(nop, 0)

    • make_rtl_arith_logic(and) //and 運算的 RTL 指令

      #define make_rtl_arith_logic(name) \
        static inline void concat(rtl_, name) (rtlreg_t* dest, const rtlreg_t* src1, const rtlreg_t* src2) { \
          *dest = concat(c_, name) (*src1, *src2); \
        } \
        static inline void concat3(rtl_, name, i) (rtlreg_t* dest, const rtlreg_t* src1, int imm) { \
          *dest = concat(c_, name) (*src1, imm); \
        }
      

      這個我在實現RTL指令的時候詳細說明了。

  5. 立即數背後的故事

    由於大端小端兩種方式數據的字節保存的地址位置相反,需要注意大端小端兩種保存數據模式的不同,先判斷機子是大端還是小端存儲方式,如果是大端,就傳到一個將小端存儲方式轉換成大端存儲方式的函數裏面就行了。

  6. 神奇的 eflags

    當運算的結果超過字長所能表示的範圍時,產生溢出,CF當然不能代替OF,對於運算的數來說,只要符合進位的情況,CF就置1,怎麼可能所有的進位情況都會超過字長所表示的範圍呢?在運算的過程中,當兩個符號相同的數相加,結果的符號與之相反,則OF=1,否則OF=0. 或者當兩個符號不同的數相減,結果的符號與減數相同,則OF=1,否則OF=0.

實驗內容

實現所有 RTL 指令

一開始打開rtl.h文件發現裏面這麼多TODO()可給我嚇壞了,之後好好看了PPT和講義才找清楚他們都是幹什麼的,然後纔好開始寫,一些rtl指令很簡單,完全就是跟着註釋走,基本上註釋已經把答案寫出來了。

在開始寫所有的指令之前,有一段代碼引起了我的注意
在這裏插入圖片描述
這是幹啥的啊,看起來定義了好多的運算,但是定義的又不是那麼清楚直接,我想了半天才明白了,重點在他定義的make_rtl_arith_logic裏面,這個函數有一個參數名字叫name,下面緊接着一個concat函數,作用應該是將rtl_name連接起來,就構成了rtl_name這樣一個東西,然後呢,在這個函數裏面就把這個東西當成c_name,找到上面的宏定義來實現相應的功能,第35到44行,一共定義了這麼多運算,這可就省了好多事了,在接下來完善rtl指令的時候就可以直接調用這些定義好的運算了。於是就開始實現相關指令

rtl_mv函數,就是簡單賦值:
rtl_not函數:
在這裏插入圖片描述
rtl_eq0函數:
rtl_eqi函數:
rtl_neq0函數:
rtl_msb函數:請注意,這個函數的寫法是錯誤的!將在PA2.2中解決
在這裏插入圖片描述
然後這些最簡單的操作寫完了,就可以寫其他的比較複雜的操作了。

rtl_push函數:
rtl_pop函數:
在這裏插入圖片描述
rtl_sext函數,這個函數寫的時候是費了我好大的勁,想了很多種情況,然後問了助教之後,不管src是多少位,通通擴展到32位,這樣以來就簡單一些,可以通過與一個數相或或者左移右移的方法來實現符號擴展,這裏我選擇的是左移右移的方式。因爲寄存器都是無符號整型,所以左移右移沒辦法移入1,所以要設置一個帶符號整數,通過帶符號整數來左移右移,最後賦給dest
在這裏插入圖片描述
make_rtl_setget_eflags,在寫這裏的時候遇到了一些困難,這個函數沒有註釋提示,然後我只知道和寄存器有關,然後就無從下手,把這個任務拖了拖,結果後面一看講義,要我自己實現eflags結構體,然後就轉去先做任務3再回來做這個,其實就是簡單的賦值操作。
在這裏插入圖片描述

rtl_update_ZF函數,簡單的判斷之後就傳到rtl_set_ZF函數裏面設置給ZF相應的值。請注意,這個函數的寫法是錯誤的!將在PA2.2中發現並解決

rtl_update_SF函數,找到符號位,傳到rtl_set_SF函數裏面設置給SF相應的值。請注意,這個函數的寫法是錯誤的!將在PA2.2中發現並解決
在這裏插入圖片描述

實現 eflags 寄存器

查詢了位域的相關概念,把講義搬過來

 31                  23                  15               7             0
 +-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+
 |                                               |O| |I| |S|Z|       | |C|
 |                       X                       | |X| |X| | |   X   |1| |
 |                                               |F| |F| |F|F|       | |F|
 +-------------------+-------------------+-------+-+-+-+-+-+-+-------+-+-+

類似於這樣子(從百度百科上面抄了個例子)

struct bs {
unsigned a:4;
unsigned :0 ;/*空域*/
char b:4 ;/*從下一單元開始存放*/
unsigned c:4;
}data;
//a 爲unsigned,空域爲unsigned

那這樣以來豈不是就很好實現,對照講義可以看出來,這些符號位肯定都是無符號整型,這幾個符號位各佔一位,其他的都是空域。由於下面有初值問題,所以還要定義一個變量存放初值。由於struct裏面是位域,所以只能在struct外面再搞一個struct,然後設置一個變量來存放值。

struct {
    struct {
      uint32_t CF:1;//CF佔一位
      unsigned : 5;//之後是5位空域
      uint32_t ZF:1;//ZF佔一位
      uint32_t SF:1;//SF佔一位
      unsigned : 1;//1位空域
      uint32_t IF:1;//IF佔一位
      unsigned : 1;//1位空域
      uint32_t OF:1;//OF佔一位
      unsigned : 20;//20位空域
    };
    uint32_t value;//賦初值要用
  }eflags;

然後要設置eflags的初值,在手冊第10章,一下子就找到了
在這裏插入圖片描述

要設置2爲初值,在monitor.c中的restart函數裏面添加
在這裏插入圖片描述

從任務6回來,發現自己的sub指令實現的有問題,找了半天的錯誤,發現邏輯上面沒啥錯誤,然後想到sub指令跟寄存器結構體有關,不會是我結構體實現的有問題把…就反過來檢查自己的結構體,思考了一下好像是有點問題的,寄存器的位域是結構體,但是eflags寄存器的初值應該是大家共用的而不是單單隻有一個纔對,並且最開始union申請空間直接要申請32位的初值空間,這樣纔可以在這個空間裏面定義位域。所以把外面哪個struct改成了union
在這裏插入圖片描述

實現 6 條基本指令

先把call指令實現:

/* 0xe8 */ IDEXW(J,call,0), EMPTY, EMPTY, EMPTY,

然後找到decode.c文件中的make_DHelper(J)函數,根據i386手冊裏面第275頁,找到call指令的相關僞代碼並在make_EHelper(call)函數裏面寫出來相關操作:
在這裏插入圖片描述
實現完call指令以後興致勃勃運行dummy單步執行一下,結果發現了這個情況
在這裏插入圖片描述
找到Assertion '0' failed這個問題所在,發現是make_DoHelper(SI)這個函數沒有實現,然後頓時就懵了,不是說好填完表然後寫好執行函數就行了嘛,怎麼還要寫這個譯碼函數??然後我在講義裏面找到了這樣一句話:

make_Dophelper(name):名爲 decode_op_name 的操作數譯碼函數的原型說明,那這個函數應該是和帶符號立即數的進一步譯碼有關係,雖然還是不太懂其中的原理,但是根據註釋還有前面的make_DoHelper(I)可以很快寫出來相應的功能,照貓畫虎嘛。這裏實現的也有問題呀!!! 將在PA2.2中發現並解決
在這裏插入圖片描述
之後再嘗試運行dummy,成功實現。
在這裏插入圖片描述
現在來實現push指令:

接着上一步繼續si告訴我0x55沒有實現,就查表找到0x55位置,然後查閱i386手冊

在這裏插入圖片描述

從50到57都是跟push相關的,只要實現32位的就行了,找到make_DHelper(r),那麼開始填表,雖然要運行我們這個程序的話填0x55就行了,但是爲了以後着想,從0x500x57都要填。

  /* 0x50 */	IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),
  /* 0x54 */	IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4), IDEXW(r,push,4),

找到data-mov.c文件中的make_EHelper(push)函數,開始實現。

make_EHelper(push) {
  rtl_push(&t0);//把寄存器壓棧
  if (id_dest->type == OP_TYPE_REG) { rtl_sr(id_dest->reg, id_dest->width, &t0); }//如果目的操作數是寄存器操作數,寫入寄存器
  else if (id_dest->type == OP_TYPE_MEM) { rtl_sm(&id_dest->addr, id_dest->width, &t0); }//如果是在內存裏面,寫入內存
  else { assert(0); } //不可能出現其他情況了。

  print_asm_template1(push);
}

從第6步回來,怎麼想感覺自己都寫的沒錯啊…問題出在哪兒了呢?於是用printf一行一行打印來確定問題所在之處,最後確定到rtl_sm這個函數,寫入寄存器的操作出了問題,我不是push ebp麼?爲啥寫入寄存器的值出現問題了啊!返回來從頭查看程序代碼,發先我的第一行rtl_push(&t0);//把寄存器壓棧這是啥?我爲什麼要把一個啥也不是的temp壓棧?我說爲什麼cpu.ebp一直是0,然後馬上更改,給t0賦上了寄存器的值,然後再入棧,最後再寫入操作數,就OK了。這裏的push仍然有一步實現有問題!!!將在PA2.2中發現並解決
在這裏插入圖片描述
si測試實現push功能
在這裏插入圖片描述

但是由上圖可知在0x83這個地址處他又不知道幹啥了,於是查看反彙編發現是sub指令,接下來就實現sub指令

i386中找到0x83位置,發現要找grp1,在grp1中找到sub,在第6個位置
在這裏插入圖片描述
在這裏插入圖片描述

回到我們的表裏,找到make_group(gp1,找到第六個EMPTY,改一下,無譯碼函數,直接執行,v表示字長不定,所以直接用EX
在這裏插入圖片描述
[外鏈圖片轉存失敗(img-FPsxdwy0-1562674179640)(圖片/033.png)]

arith.c裏面找到執行函數,根據手冊上面寫的僞代碼開始實現,一開始完全照着手冊上面來實現,寫個if寫個else,本來覺得沒啥問題,結果一跑就出錯了,我想想這邏輯也沒問題啊,怎麼就跑不對了呢?然後就往下翻,還是在這個文件裏面,我看到了有個sbb執行的執行函數,哎呦,這個跟sub好像哎,於是我就繼續查手冊,找到了這個指令的相關內容:

在這裏插入圖片描述

這跟sub不就差一個CF的問題麼,有了sbb那麼sub也好實現了,對照着他就可以開始寫了:
在這裏插入圖片描述

寫完sub指令發現之前自己的push第3、4、5行完全可以直接調用operand_write函數…我竟然沒注意到這個函數!

成功實現

在這裏插入圖片描述

繼續si直到報錯

在這裏插入圖片描述

發現0x31這個地址處的指令沒有實現,查看反彙編以後是xor指令,現在就去實現它:

查手冊,從0x30開始到0x35都是要實現異或的,0x300x31E->G,反過來對應譯碼函數要找G2E,0x320x33正好相反要找E2G,0x34要找I2a,0x35要找I2r

在這裏插入圖片描述

返回到表中開始填寫相關位置
在這裏插入圖片描述

logic.c文件中開始寫執行函數

還是查手冊獲得xor指令怎麼寫,CF和OF位都是0,目標操作數等於srcdest的異或
在這裏插入圖片描述
在這裏插入圖片描述
成功實現

在這裏插入圖片描述

再運行一步又出現問題了!真是問題不斷,這次查閱了反彙編,發現是pop指令沒有實現

在這裏插入圖片描述

i386手冊裏面,pop就在push的旁邊,0x580x5F

在這裏插入圖片描述
回到我們的opcode表,其實跟push一模一樣,譯碼函數也是r
在這裏插入圖片描述
就連執行函數也和push的邏輯一模一樣,就反過來,一個壓棧,一個出棧就行了。

在這裏插入圖片描述

成功實現

在這裏插入圖片描述

繼續si,這回是0xc3地址處的指令沒有實現,還是老慣例,查看反彙編,發現是最後一個ret指令,現在開始實現吧:
老樣子查i386手冊
在這裏插入圖片描述

找到opcode0xc3的位置,因爲是空白,所以沒有譯碼函數,直接執行。

/* 0xc0 */ IDEXW(gp2_Ib2E, gp2, 1), IDEX(gp2_Ib2E, gp2), EMPTY, EX(ret),

找到之前寫call指令的那個文件,開始寫make_EHelper(ret)函數

回想理論課中ret指令做了些什麼?將eip退棧就行了,並且參照call指令,還要設置跳轉標誌。
在這裏插入圖片描述
在這裏插入圖片描述
成功實現
在這裏插入圖片描述

對已實現指令增加標誌寄存器行爲

好像在實現第2個任務的時候,對照着i386手冊就直接把這些指令的符號位設置實現了,當時沒看到第四個任務…

callpushretpop指令不改變任何標誌位

subxor指令改變除了IF的其他標誌位

這些都在任務2實現了

運行第一個客戶程序

由於上面幾個任務的完成,指令的實現,現在可以成功運行這個程序

在這裏插入圖片描述
不要忘記在all-instr.h裏面聲明所有的指令執行函數!
在這裏插入圖片描述

實現 differential testing

common.h中把#define DIFF_TEST的註釋取消,講義用了很長的篇幅來講解這個東西,但是實現的話卻只要一個if語句
在這裏插入圖片描述
成功發現自己編寫失敗的指令
在這裏插入圖片描述

哭着回到第二步檢查自己的sub指令的問題…

eflags寄存器回來,再次測試

在這裏插入圖片描述

這回sub沒問題了,push又出現了問題,然後回到第二步,之後就可以成功運行了。

利用 differential testing 檢查已實現指令

在經歷了任務6以後,成功實現對了所有的指令
在這裏插入圖片描述
PA2.1的內容到此結束,感謝您的耐心閱讀,文中加黑加粗的寫錯的地方將會在後來的PA完成過程中更改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章