組成原理第二章——計算機指令

計算機指令

算術運算指令

add a, b, c# a = b + c

設計原則一——對指令進行規整化設置

  • 簡化實現
  • 獲得更高的性能,更低的成本

代碼示例

  • C語言代碼
f = (g + h) - (i + j)
  • MIPS
add t0, g, h # temp : t0 = g + h
add t1, i, j # temp : t1 = i + j
sub f, t0, t1 #final: f  = t0 - t1

參與算術邏輯運算的變量必須是寄存器變量,對於MIPS(x 32)指令集來說,有32個寄存器,每個寄存器長32bit32bit,它們:

  • 用於存儲最常用到的變量
  • 每個寄存器的編號爲0~31
  • 每個寄存器中32位數據稱作一個

約定符號:

  • $t0,$t1,…,$t9 用於表示臨時變量
  • $s0,$s1,…,$s 7表示需要保存的變量

設計原則二——更小就會更快

  • 傾向於將算術運算放到寄存器中運行,以加快速度
  • 內存的存儲單元通常是百萬級的

前面的代碼,根據約定符號進行矯正

add $t0, $s1, $s2
add $t1, $s3, $s4
sub $s0, $t0, $t1

可以看到,在MIPS32MIPS32指令集中,只有323232*32個存儲空間,這並不足以支持所有運算,所以很多的數據是存儲在內存中的,我們將這些被儲存在內存中的數據稱爲_Memory Operands。內存的操作數是用來保存複雜的操作數,這是由寄存器的空間過小導致的。

算數邏輯運算不能直接對內存中的數據進行運算,這就需要我們先從內存中讀取(load)數據,然後再進行處理,最終再將操作的結果寫入(write)內存。在對內存的操作中,都是通過字節尋址的(每八位分配一個地址)。值得注意的是,每個字節由8bit8bit組成,而寄存器中每一位長度是32bit32bit,這就表明,我們在知道首地址的情況下,想要尋找第ii個元素時,需要將偏移量4*4

MIPS中數據的對齊方式是:大端對齊。什麼是大端對齊呢?

  • 存儲一個數據,需要四個字節,這也就對應着內存上的四個存儲單元。
  • 這四個存儲單元有他們相應的標號
  • 高內存地址放整數的低位,低內存地址放整數的高位,這種方式叫正着放,術語叫大端對齊
  • 與大端對齊相反的是小端對齊,說白了就是反着存

Memory Operand 小例子

C code

g = h + A[8]

其中,$s1 is g,$s2 is h,$s3 is the base address of A

MIPS code

lw  $t0, 32($s3) # 讀取A[8],32來源於偏移量*4 = 8*4 = 32
add $s1, $s2, $t0

Memory Operand 小例子2

C code

A[12] = h + A[8]

其中,$s2 is h,$s3 is the base address of A

MIPS code

lw  $t0, 32($s3)
add $t0, $s2, $t0
sw  $t0, 48($s3)#寫入

可以看出,寄存器和內存的交互主要通過lwlw ,swsw兩條語句,進行數據的讀取、寫入,對於編譯器而言,選擇哪些變量放到寄存器中,哪些放到內存中,是非常關鍵的問題,同時也是非常困難的問題。

立即數

所謂立即數,即所使用的變量是一個常數,他是包含在指令中的。如:

addi $s3, $s3, 4

這就相當於C語言中的

s3 += 4;

立即數操作中沒有減法,因爲減掉一個數就相當於加上這個數的相反數,即:

addi $s3, $s3, -4

就可以完成C語言中如下功能

s3 -= 4;

**支持的數字範圍:**立即數操作的常亮僅支持有符號的16位整數,即:[32768,32767]\left[ -32768, 32767\right]這個區間。如果需要一個32位整數,那麼這個整數就只能被先放到內存中,然後再被寄存器讀取。

這裏就引出了第三個設計原則:加快高概率事件(Make the Common Case Fast)

  • 小的常數是常用的

  • 使用常用的數,不需要從內存中讀取

    數字的類型

  • 無符號整數(unsigned int)

    • 用於表示地址
    • 或是非負數
  • 有符號整數

    • 正負數
  • 浮點數

    • 單精度浮點數(float)
    • 雙精度浮點數(double)

有符號數的表示

  • 補碼
  • 源碼

補碼和源碼之間有+0和-0的區別。

MIPS中特殊的取值——0

MIPS中0號寄存器永遠爲0,只能讀取,不能寫入。我們常用$zero來代表上述的零號計算器,利用這個寄存器,我們可以進行下面的操作:

add  $t2, $s1, $zero
addi $t2, $zero, 100

事實上,MIPS中是沒有初始化的方法的,在上面的代碼中,我們用$s1的值初始化了$t2,然後將100賦值給$t2,這種語法的設計減少了很多冗餘的語句。

有符號數和無符號數的表示

無符號二進制數計算公式:
x=Σ(xi2i) x = \Sigma \left( x_i 2^i\right)
這個老掉牙了,沒啥好說的,一個長度爲n的無符號二進制數,能表示[0,2n1]\left[0,2^n-1\right]這個區間內的整數。

有符號數的表示

  • 源碼的表示:最高爲作爲符號位
    • 0代表符號爲正
    • 1代表符號爲負
  • 源碼錶示存在的問題:同時存在正0和負0

補碼的表示:這一部分我之前看的時候基本上都是硬背的,現在聽到這種講法才恍然大悟(菜是原罪)

假設補碼錶示的二進制數有n位,標號爲0,n10 ,n-1,那麼補碼到十進制數的計算公式爲:
x=xn12n1+Σi=0n2(xi2i) x = -x_{n-1}*2^{n-1} + \Sigma_{i=0}^{n-2} \left(x_{i}*2^{i}\right)
它對應的取值範圍是:[2n1,+2n11]\left[ -2^{n-1},+2^{n-1}-1\right]

在整數運算中,大多數情況下,使用的是補碼的形式。下面有一些特殊的補碼數字(幫助理解用的,不用想哪裏特殊):

十進制 二進制
0 0000 0000 … 0000
-1 1111 1111 … 1111
最小的數 1000 0000 … 0000
最大的數 0111 1111 … 1111

一般情況下,MIPS指令集下的運算都是對有符號數進行運算,除非你顯式的告訴計算機要進行無符號數運算,需使用addu操作。

常見操作:

  • 將數字取反:將數字的每一位取反,再對最後一位加1
    • 優點:可以用已有的加法電路,做簡單的拓展即可作爲減法電路使用。
  • 符號位擴展
    • 應用場景:如:使用addiaddi指令時,得到的是一個16位數字,要將該數字拓展爲32位數字,纔可以與其他數字進行算術運算。
    • 無符號數的拓展方法
      • 直接補零
    • 有符號數的拓展方法
      • 正數:在數字前方補0
      • 負數:在數字前方補1

指令的表示

還記得我們第一章的時候講過,彙編語言多數時間執行着將高級語言翻譯成機器語言(machine code) 工作。下面是一些寄存器名稱和用途的的對應表:
在這裏插入圖片描述
其中,1號寄存器被稱爲$at,它是爲彙編程序預留的;26-27號寄存器被稱爲$k0,$k1,他們是保留給操作系統的。

指令集分爲六大類:

  • 算數邏輯運算
  • 內存訪問
  • 分支和跳轉指令
  • 浮點運算指令 --使用協處理器完成
  • 內存管理指令
  • 特殊指令

0-31號寄存器是我們(程序員)能夠訪問的到的寄存器,還有我們訪問不到的寄存器:

  • PC:用於存放當前程序正在執行的指令
  • HI & LO:在乘除法運算時做臨時儲存

mips32中指令集架構的格式

在這裏插入圖片描述

R format

主要用途:用於表示算數邏輯運算,分爲:

他們分別代表:

  • opoperation codeoperation\ code用於表示當前的操作類型
  • rs,rt:表示用於運算的兩個源操作數,對應寄存器的編號
  • rd:目標操作數,對應寄存器的編號
  • shamt:針對移位運算,記錄移位的次數
  • funct:是opCode的拓展

例子:

add $t0, $s1, $s2

對應的指令爲:

special $s1 $s2 $t0 0 add
0 17 18 8 0 32
000000 10001 10010 01000 00000 100000

I format

在這裏插入圖片描述
I-format可以用於之前的addiaddi操作、load/storeload/store或是條件跳轉等指令。
其中

  • rs/rt :源操作或目標操作寄存器
  • constant or addressconstant\ or\ address
  • 在進行運算時:補碼形式的二進制數
  • 在進行尋址時:表示內存地址

設計規則四——Good design demands good compromises

這種設計方法看似折中、不利於譯碼,但將長度固定在了32位,爲了統一,增加了一部分譯碼的複雜性,達到了整體的統一,同時,這種設計方法,降低了後期電路設計的複雜性。

J format

後面再講

馮諾依曼計算機設計的兩個關鍵特性

1.指令和數據都是二進制串,無法區分

2.程序時能夠被改寫的

在這裏插入圖片描述

存儲程序相關的基本概念:

  • 程序可以被保存爲二進制文件,這樣的特性使得一個程序可以從一個電腦搬到另一個電腦上使用,這一個叫做“二進制的兼容性”(deepin-wine)

  • 爲了保證對已經編譯好了的軟件的繼承,指令集架構應當圍繞着少數幾個大的指令集架構發展。

    邏輯運算指令

    一些對應的語言:
    在這裏插入圖片描述

    移位運算(Shift Operations)

special rs rt rd shamt funct
6 bits 5 bits 5 bits 5 bits 5 bits 6 bits

C語言中

$s1 = $s2 << 10;
$s1 = $s2 >> 10;

與MIPS中:

sll $1,$2,10
srl $1,$2,10

相同
其中: shamt-用於記錄移動多少位

  • 邏輯左移運算:

    • 左移,並將空出來的部分用零填充
    • 左移ii位,相當於乘上2i2^i
  • 邏輯右移運算:

    • 右移,並將空出來的部分用零填充

    • 右移ii位,相當於除以上2i2^i

      與、或運算(And/Or Operations)

      and $t0, $t1, $t2#t0 = t1 and t2
      or    $t0, $t1, $t2#t0 = t1 or     t2
      

      將$t1 & $t2的值賦給$t0,與運算可以運用在屏蔽寄存器中。

NOR運算

NOR就是not,or,這是一個三目運算符,使用方法如下:

nor $t0, $t1, $t2

這個運算表示not (t2 or t1)not\ \left( t2\ or\ t1\right)
MIPS中沒有單獨的取反操作,如果想要對$t1取反存入到$t0中,方法如下:

nor $t0, $t1, $zero

在上述代碼中,進行的是not (t1 or zero)not\ \left( t1\ or\ zero\right)的操作,其中,任何一個數和零進行或運算,還是他本身,再對它本身取反就可以得到not t1not\ t1的結果。

條件指令(分支與循環)

常見的控制指令:

beq re, rt, L1
bne rs, rt, L1
j          L1

他們對應的C語言語法是:

#beq re, rt, L1
if(re == rt){
    goto L1; 
}
#bne rs, rt, L1
if(re != rt){
    goto L1; 
}
#j          L1,無條件跳轉
goto L1;

跳轉的目標指令,與當前的beq/bne之間,不能超過正負2152^{15},同時,在寫C和C++的時候除特殊情況外應當避免/減少goto語句的使用,因爲使用goto語句不當可能會讓程序表意不明,更加混亂。

一個小例子(選擇結構)

老規矩,先看C/C++

if(i==j){
    f = g + h;
}else {
    f = g - h;
}

對應的MIPS

           #s3 is i, s4 is j, s0 is f, s1 is g, s2 is h.
           bne $s3, $s4, Else#判斷i,j是否相等,若不等就直接跳過下面兩行
           add $s0, $s1, $s2#若相等,就繼續執行到這一行,完成f=g+h
           j Exit#執行完f = g - h之後跳過下一條語句直接退出
 Else: sub $s0, $s1, $s2#執行f = g - h
 Exit:

從上面的代碼中可以看出,MIPS的標記本身並沒有改變代碼的執行順序,僅僅是對某行代碼做了標記而已,如果沒有j,bne,beqj,bne,beq三條語句的話,就算程序中有標記,MIPS還是會順序執行的。

第二個例子(循環結構)

Ccode

 while(save[i] == k) i += 1;

MIPS Code

 #i is $s3, k is $s5, 地址被保存在$s6中
 Loop: sll $t1, $s3, 2 #偏移量 = i * 4
              add $t1, $t1, $s6# 當前地址= 起始地址 + 偏移量
              lw $t0, 0($t1)#讀取數據到t0
              bne $t0, $s5, Exit
              addi $s3, $s3, 1# s3 = s3 + 1
              j  Loop
Exit:

沒啥好講的了,自行體會。

更多條件指令

MIPS Code

slt rd, rs, rt
slti rt, rs, constant

C Code

#slt rd, rs, rt
if(rs < rt ){
    rd = 1;
}else {
    rd = 0;
}
#slti rt, rs, constant
if(re < constant){
    rd = 1;
}else {
    rd = 0;
}

這個語句可以和beq一起用,來進行在大於、小於等情況下的選擇結構。

小練習

if(i>=j){
    i = i + 1;
}else {
    j = j + 1;
}

將上述代碼轉化成mips指令集中的code,i,j–>$s1,$s2
我的答案:

           slt    $t0, i, j # if i>=j 0
           beq $t0, $zero, Else #if t0==zero --> i>=j goto Else:j=j+1,else go on
           addi $s1, $s1, 1
           j         Exit
Else: addi $s2, $s2, 1
Exit: ...

有符號和無符號數字的比較

無符號數進行比較:
sltu,sltui,其實說白了就是後面加個u就是無符號數操作,再加個i就是常數操作。

函數的調用和函數的返回

基本塊,是一個指令序列,在這個序列中是沒有分支指令的,也沒有其他分支指令的跳轉指令
在這裏插入圖片描述

函數調用的基本步驟:

  • 將函數相關的參數放入寄存器中(load 指令或者是邏輯運算指令)
  • 將寄存器的控制權交給函數相關的進程。關鍵
  • 申請,並獲得存儲空間(堆棧)
  • 執行函數中的指令
  • 將返回的結果放回寄存器中關鍵
  • 將結果返回到調用的地址中
jal ProcedureLabel

將寄存器控制權交給相關的進程:
將ProcedureLabel的下一個地址放到$ra中,然後跳轉到目標地址。(這裏$ra用於記錄返回值返回到哪裏)

jr $ra

將$ra複製到程序計數器中,這條語句也可以用於其他的跳轉用途。

這段他講的好抽象,我有點沒聽懂。
下面有一個實例。

Leaf Procedure Example(不調用其它函數的函數)

C Code

int leaf_example(int g, int h, int i, int j){
    int f;
    f = (g + h) - (i + j);
    return f;
}

我就直接貼最後的代碼了:

leaf_example:
    addi $sp, $sp, -4#壓棧,往下壓四個字節
    sw $s0, 0($sp)#將s0放入壓好的棧中
    add $t0, $a0, $a1
    add $t1, $a2, $a3
    sub $s0, $t0, $t1
    add $v0, $s0, $zero#將結果放入用於返回的參數$v0中
    lw $s0, 0($sp)#恢復$s0
    addi $sp, $sp, 4#恢復堆棧位置
    jr $ra#返回

說實話,除了jr都看懂了,那個確實沒看懂。

None-Leaf Procedures(函數的嵌套調用)

調用者需要將一些信息存儲到堆棧中:

  • 被使用的寄存器($s0-$s7)
  • 他的返回地址
  • 在函數、過程中用到的臨時變量
    在返回之前,將堆棧中數據取出進行恢復。
if fact(int n){
    if(n < 1){
        return 1;
    }
       return n*fact(n-1);
}

對應的mips,參數n放到$a0中,結果放到$v0

fact:
    addi $sp, $sp, -8
    sw $ra, 4($sp)
    sw $a0, 0($sp)
    slti $t0, $a0, 1#判斷是否小於1
    beq $t0, $zero, L1#如果大於等於1,那麼跳到後面去
    addi $v0, $zero, 1#如果小於1,那麼將1付給v0
    addi $sp, $sp, 8#從棧空間中出棧兩個
    jr $ra#返回
L1:
    addi $a0, $a0, -1#減一
    jal fact# 從新跳轉到fact部分去執行
    lw $a0, 0($sp)#將堆棧中把兩個參數取出
    lw $ra, 4($sp)
    addi $sp, $sp, 8#恢復堆棧
    mul $v0, $a0, $v0#相乘
    jr $ra#返回

棧上的變量

在這裏插入圖片描述
堆棧用於存儲臨時變量。
圖中$sp指向堆棧可用地址,申請棧的時候,向下申請。$fp的存在使得便於恢復棧。

內存中一個程序分爲不同的段:

  • 代碼段:用於存放指令
  • 靜態數據:執行過程中不會變的數據
  • :C語言中new出來的東東,需要手動釋放
  • :臨時變量

思考:

  • 遞歸能否轉循環
    • 答案是可以的,就直接模擬它的棧
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章