コンピュータアーキテクチャI講義ノート (4) 1 命令:コンピュータの言葉

2015.11.4
コンピュータアーキテクチャI 講義ノート (4)
命令:コンピュータの言葉
1
今週もみなさんには人間コンパイラになっていただく.そして,機械語命令(アセンブリ命令)
がどのように使われるか,命令セットアーキテクチャという視点でみていただきたい.
1.1
論理演算
この範疇の命令には,論理和,論理積,論理和否定,排他的論理和などの命令がある.具体的
には,
or $r1, $r2, $r3
and $r1, $r2, $r3
#論理和.ビットごとに演算する.R 形式の命令である.
#論理積.ビットごとに演算する.R 形式の命令である.
nor $r1, $r2, $r3
xor $r1, $r2, $r3
#論理和否定.ビットごとに演算する.R 形式の命令である.
#排他的論理和.ビットごとに演算する.R 形式の命令である.
などである.否定 (not) は,nor 命令を使って,次のように記述する.
nor $r1, $r2, $zero
#否定.ビットごとの演算.
ここで,$zero は定数 0 を保持しているレジスタである.
上記は,いずれも 32 ビットデータに対する演算であり,ビットごとに演算が行われることに注意.
[演習 1]C のプログラム部分
g = h | i;
f = g & j;
e = f ^ h; /* ^は排他的論理和演算 */
をコンパイルせよ.ただし,e,f,g,h,i,j はそれぞれ,$s1,$s2,$s3,$s4,$s5, $s6 に割り当てられている
ものとする.
そのほかに,シフト演算がある.シフト演算は,文字通り 32 ビットのデータを右方向あるいは
左方向にずらす命令である.たとえば,a31 a30 ...a0 を 2 ビット左にシフトすれば a29 a28 ...a0 00 と
なる.この例では,シフト前の上位の 2 ビットはなくなり,下位の 2 ビットには 0 が入る.はみ出
たビットを下位に入れることも考えられる.その場合はローテイト (rotate) という.シフト命令に
は以下のようなものがある.
sll $r1, $r2, const
#左論理シフト.const ビット左にシフトする.R 形式命令
srl $r1, $r2, const
sra $r1, $r2, const
rol $r1, $r2, const
#右論理シフト.const ビット右にシフトする.R 形式命令
#右算術シフト,const ビット右に算術シフトする.R 形式命令
#左ローテイト.疑似命令
ror $r1, $r2, const
#右ローテイト.疑似命令
1
命令の名前は sll(shift left logical), srl(shift right logical), sra(shift right arithmetic), rol(rotate
left) ror(rotate right) から来ている.右算術シフトは,除算をするときに必要な命令で,最上位
ビットの値を保持しつつ右にシフトする命令である.
(論理シフトでは,保持せず 0 が入る.
)命令
の仕様の詳細については,教科書の付録の該当部分を見てほしい.
論理シフト演算の特徴は,整数データを左に 1 ビットシフトすることと,2 倍することは同じ結
果を与え,右に 1 ビットシフトすることと 2 で割ることが同じ結果を与えることである.したがっ
て,アドレス計算をするとき 4 倍することがよくあるが,その場合は sll 命令を使って,左に 2 ビッ
トシフトすることを行う.
[演習 2]C のプログラム部分
g = h + A[i];
をコンパイルせよ.ただし,g,h,i はそれぞれ,$s1,$s2,$s4 に割り当てられており,A のベースア
ドレスは,$s3 に格納されているものとする.
(sll 命令を使うこと.
)
[演習 3]C のプログラム部分
unsigned int g, h;
g = h * 16;
g = h / 64;
をコンパイルせよ.ただし,g,h はそれぞれ,$s1,$s2 に割り当てられているものとする.
(賢いコ
ンパイラは 16 が 2 のべき乗であることを見て,sll 命令や srl 命令を使うことであろう.
)
条件判定用の命令
1.2
条件分岐命令を2つ,無条件分岐命令(ジャンプ命令)を1つあげておく.
beq $reg1, $reg2, Label
bne $reg1, $reg2, Label
j Label
#if $reg1=$reg2 then goto Label else next
#if $reg1!=$reg2 then goto Label else next
#goto Label
beg/bne 命令は,条件が成立/不成立のとき分岐する命令であり,I 形式命令である.j 命令は,
無条件に分岐(ジャンプ)する命令であり,J 形式命令である.J 形式命令のフィールド構成は以
下のようになる.
6
26
op
address
C 言語の構文,if ... then ... else ... の形のプログラムを見てみよう.
if(i==j) f=g+h; else f=g-h;
変数 f,g,h,i,j は$s0 から$s4 に割り当てられているものとする.
この構文のコンパイルの考え方は,1) まず,条件の判定を行い,等しくなければ分岐する,2)then
クローズのコンパイルを行い,if 文の外へジャンプする,3)else クローズのコンパイルをする,の
手順で行う.
2
[演習 4] このプログラムをコンパイルしてみよう.
この例で,分岐命令,ジャンプ命令の使い方がお分かりいただけると思う.また,構造をもつ構
文のコンパイルの仕方もわかることと思う.そうはいっても,その他の構文をすぐにコンパイルす
ることはハードルが高いから,いくつかを見ていくこととしよう.
まず,while 文を見てみよう.
while(save[i]==k) i=i+j;
変数 i,j,k は$s3,$s4,$s5 に割り当てられているものとする.また,配列 save のベースは$s6 にあ
るものとする.
この構文のコンパイルは,1)while の条件をコンパイルし,結果が真でなかったら while 文の外
に分岐する,2)while 文の本体をコンパイルする,3)while 文の先頭にジャンプする,の順で行う.
[演習 5] このプログラムをコンパイルしてみよう.
次に,C 言語の switch case 構文の例題を考える.これをコンパイルするために,新しいアセン
ブリ命令を2つ導入する.最初の命令は,
slt $reg1, $reg2, $reg3
#if $reg2 < $reg3 then $reg1=1 else $reg1=0
である.
この命令は,$reg2 と$reg3 の内容を比較し,$reg2<$reg3 なら$reg1 に定数 1 をセットし,そう
でなければ$reg1 に定数 0 をセットする命令である.
なんでこんなものを用意するかというと MIPS では branch on less than 命令 (blt) が用意されて
ないからである.なぜないかというと,blt 命令の実行は,他の命令の実行より時間がかかる.プ
ロセッサはクロックに同期して動くので,blt を入れるとクロックを遅くしなければならない.そ
れよりは,blt を命令セットから除いてクロックを速くしたほうが得ではないか.しかし,blt に相
当することができないといけない.これは,2 命令を使って実現する.blt の使用頻度は小さいの
で,そうしても性能は下がらない.こういう考えから,blt は MIPS 命令セットには入っていない.
その代り,slt 命令がある.slt 命令は他の命令と同等の時間で実行できる.そして,slt と beq(あ
るいは bnq) の 2 命令で blt を実現する.
slt のバリエーションとして次の 2 つがある.
slti $reg1, $reg2, const
#if $reg2 < const then $reg1=1 else $reg1=0
sltu $reg1, $reg2, $reg3
#if $reg2 < $reg3 then $reg1=1 else $reg1=0
slti は,レジスタの内容と定数の大小比較をする命令である.sltu はレジスタ同士の比較であるが,
符号なし数の大小比較をする命令である.
新しく導入する 2 つ目の命令は,
jr $reg
#$reg が指す番地に分岐する.
である.
jr 命令は,レジスタが持つ値の番地にジャンプする命令である.jr 命令は R 形式命令である.そ
れでは,これらを使った例題を見てみよう1 .
1 これは第3版の教科書にある例題.第4版では除かれている.
3
switch(k) {
case 0: f=i+j; break;
case 1: f=g+h; break;
case 2: f=g-h; break;
case 3: f=i-j; break;
}
変数 f,g,h,i,j,k は$s0 から$s5 に割り当てられているものとする.また,常に定数 0 が入っている
レジスタを仮定し,それを$zero で表す.さらに,$t2 には 4 がセットされているものとする.も
う一つ,4 語からなる表をメモリに用意し,その先頭アドレスが$t4 に入っているものとする.こ
の表には,各 case 文のコンパイルされた命令列の先頭アドレスが入っているものとする.
この構文のコンパイルは,アセンブリ言語に慣れてないと,手こずる.考え方は,1)k の値が,
0 から 3 の範囲にあるか調べる,2) その範囲になければ,switch-case 文の外にジャンプする,3)k
に対応する(case 文の)アドレス分岐表のエントリアドレスを求める,4) 分岐表を引いて分岐ア
ドレスをレジスタに入れる,5)jr 命令でジャンプ,6) 各 case 文に対して,case 本体をコンパイル
し,そのうしろに switch-case の外にジャンプする命令を付加する,という手順になる.
[演習 6] このプログラムをコンパイルしてみよう.
1.3
手続きのサポート (1)
つぎに手続き(Cでいうなら関数呼び出し)のコンパイルをやってみる.だんだん難しくなって
きますが,ここまで順を追って演習を行ってきた皆さんなら大丈夫です.
まず,Cで関数呼び出しの例とその実行の様子を見てみよう.
main(){
....
g=1; h=2; i=3; j=4;
k=func1(g,h,i,j);
....
}
int func1(int g, int h, int i, int j) {
int f;
f=(g+h)-(i-j);
return f;
}
このプログラムは,変数 (g,h,i,j) に値を代入した後で,関数 func1 を呼ぶ.関数 func1 の中では
f の値を求め,それを呼ばれた関数(この場合メイン関数)に返す.メイン関数ではその値を変数
k に代入し,引き続く計算をする.
これをアセンブリ言語に翻訳するには,関数呼び出しのところをどのように翻訳するかがポイン
トになる.具体的には,関数を実行するために必要なパラメータ(引数)を置く場所をメモリ上に
確保して,そこにパラメータをコピーする.関数を実行する際には,このコピーを使って計算す
4
る.というように翻訳する.こうすることによって,呼び出し側と呼ばれた側でパラメータが相互
に影響しないようにするのである.
(この意味は,先に進むとわかるであろう.
)
さて,そうすると,関数呼び出しのところは,
• パラメータ(引数)をわたす.
• 制御を移す.
(関数を呼び出す)
• (メモリ資源の確保.
)関数本体の実行.
(戻り値のセット.
)
• 制御を戻す.
(リターン命令の実行)
というようになる. (教科書 p101 に箇条書きで書いてあるところも参照してください.
)
パラメータの引渡しは可能な限りレジスタを使う.そのために,以下のように割り当てる.
$a0 - $a3 引数レジスタ パラメータを引き渡すために使う.
$v0 - $v1 値レジスタ 結果の値を入れる.
$ra 戻りアドレスレジスタ 制御をもとにもどすために使用する.
注)引数レジスタについて:メモリを使って引数を渡すのは時間がかかるので,できるだけ(ア
クセス時間の短い)レジスタを使って引数を渡そうという考え方です.引数の数が 4 つ以下の関数
は結構あるし,4 つを超える場合でも,レジスタを使う効果はある.
関数呼び出し用の命令が用意されている.(jal 命令.すなわち jump and link 命令) Jal 命令
は,J 形式命令である.
jal
func_address
#戻りアドレスを$ra に入れて,func_address に分岐する.
この命令は,戻り番地をレジスタ$ra に格納して,関数のエントリアドレスにジャンプする命令
である.戻り番地は実行している jal 命令の番地+4 である.つまり,jal 命令の次の命令の番地で
ある.戻り番地は命令が自動的に$ra にセットしてくれる.
関数から戻るときは,戻りアドレスが$ra に入っているので,jr $ra でOKである.
[演習 7] メイン関数の
g=1; h=2; i=3; j=4;
k=func1(g,h,i,j);
の部分をコンパイルしてみよう.関数のエントリアドレスは func1 としてください.g,h,i,j,k はそ
れぞれ$s0,$s1,$s2,$s3,$s4 に割り当てられているものとする.また,引数はレジスタで渡すものと
する.さらに,func1 の計算結果は,レジスタ$v0 に入っているものとする.
さて,引数や結果の値の個数がレジスタの数を超えた場合,あぶれたものの引渡しをどうやる
か?また呼び出された関数の中で使われる変数(ローカル変数)をどこに置くか?これが問題だ.
そこで登場するのが,スタックである.そのために,まず,スタックの概念を理解しておく必要が
ある.
スタックの例: 生協の食堂にあるトレイのスタック
コンピュータ上での実現方法: メモリ上に実現する.スタック領域を確保し,スタックのトッ
プをスタックポインタで管理する.データをスタックに入れたり,スタックから取り出す操作(そ
れぞれ,プッシュ,ポップという)を用意する.スタックポインタはレジスタに割り当てられる.
$sp という名前のレジスタがある.
5
[演習 8] スタックの操作を C で書いてみよう.
スタックは stack という名前の配列で用意されているものとする.また,スタックポインタは
stkp という名前の変数で与えられているものとする.stkp はスタックのトップにあるデータを指
している.また,stack, stkp は,グローバル変数とする.
#define STACKSIZE 4096
int stack[STACKSIZE];
int stkp=-1;
このとき,pop 関数と push 関数を作成せよ.
void push(int data);
/*data を stack に積む.*/
int pop();
/*stack トップのデータを返す.*/
(ヒント:スタックが空か否かのチェックや,スタックオーバーフローのチェックが必要である.
)
以上を念頭に,上記の関数 func1 のコンパイルをする.以下のような,レジスタ割当およびコン
パイルの流れとなる.
1)レジスタの割り当て: 引数 g,h,i,j は$a0 から$a3 に割り当てられているものとする.
2)メモリの確保: 退避のため3語分を確保 (スタック上に確保)
3)手続き内で使うレジスタの退避: $s0,$t0,$t1 を退避 (ここでは,$t0, $t1 を退避してい
るが,$ti は一時レジスタであり,退避しない約束である.p.104 参照)
4)手続き本体の実行: 本文コンパイル.
5)戻り値のセット: 結果レジスタ ($v0) にセット
6)レジスタの回復: 退避したレジスタを戻す.
(これで,呼び出し前のレジスタの状態が復元
された.
)
7)メモリの開放: スタックポインタをもとにもどす.
8)制御の移行: jr 命令
[演習 9] 上記のプログラム(func1 のプログラム)をコンパイルしてみよう.
[演習 10(難)] 分岐命令 (beq 命令)を実行できる回路ブロック構成を考えてみよう.
2
SW 命令の実行
すでに R 形式命令および LW 命令の実行ができる構成を示している.次に示すのは,SW 命令
が実行できる計算機構成である.図 1 がそれである.
これらを統合すると,上記の命令をすべて実行できる構成が出来上がる.図 2 のようになる.
6
SW命令の流れ
レジスタファイル
命令メモリ
データメモリ
ALU
Ladr
Radr
選択
PC
命令解読
制御信号
図 1: SW 命令を実行できる典型的な構成
R形式,LW, SW命令が実行できる構成
レジスタファイル
命令メモリ
データメモリ
ALU
Ladr
Radr
PC
選択
選択
Wadr
命令解読
制御信号
図 2: R 形式命令,LW,SW 命令を実行できる典型的な構成
7