コンピュータシステム概論 SPIM 演習第一回

コンピュータシステム概論
SPIM 演習第一回
(コンピュータシステム概論 2016 年度ホームページ URL
file:///home/course/csI/public_html/2016/welcome.html 学内)
1. SPIM - MIPS 2000/3000 シミュレータ
SPIM は MIPS 2000/3000 のシミュレ-タです。MIPS 用に書かれたアセンブリファイルをアセンブルすることなく読み込
んで実行できます。SPIM は仮想的な CPU の上でプログラムを実行するので、エラーを検出してくれますし、1 命令ずつ実行し
たり、レジスタの内容やメモリの中を覗くこともできます。もちろん実際のマシンに影響をあたえることはありません。また、
遅延分岐や遅延ロ-ドを考慮せずにプログラムを書くことができます。
1.1 xspim の使い方
SPIM にはコマンドライン版とX ウィンドウシステム版の2 つがありますが、
演習ではX ウィンドウシステム版を使います。
以下のコマンドで起動できます(マシンを自動判別するのでどの演習室でも同じコマンドで使えます)。
xspim &
起動した xspim の画面を見て下さい。ウィンドウの一番上の領域にはレジスタの一覧が表示されます (図 1)。プログラムを
実行すると使用したレジスタの値が変更されます。値は 16 進数で表示されます。
図 1 レジスタ
図 2 Text Segments
Text Segments と書かれている領域には、プログラムが格納されるテキストセグメントのメモリの内容が表示されます(図 2)。
Data Segments と書かれている領域には、データが格納されるデータセグメントのメモリの内容が表示されます(図 3)。
一番下の領域にはエラーなどのメッセ-ジが表示されます(図 4)。
(エラーは 8.2 P12 参照)
xspim の操作にはウィンドウの真中にあるボタン(図 5)を使います。
1
図 3 Data Segments
図 4 メッセージ
図 5 ボタン
以下に各ボタン(長丸)の説明をします。
・ quit xspim を終了します。quit ボタンを押すとポップアップウィンドウで、本当に終了していいか再度聞いてきま
す。abort command は quit 命令のキャンセルです。
・ load アセンブリファイルを読み込みます。ポップアップウィンドウでファイル名を入力し assembly file ボタンを
押すと、プログラムとデータが Text Segments と Data Segments にそれぞれロードされます。load を使うと、すでにロ
ードされているプログラムを上書きせずに、次のアドレスからプログラムをロードします。つまり、いまあるプログラム
のあとに、新しいプログラムを読み込んでしまいますので、使用する前に必ず clear ボタンで、すでに読み込んであるプ
ログラムを消去してから load して下さい。この際、clear ボタンを押すと出てくる複数のプルダウンメニューの中から
memory & register を選択することで、すでに読み込んであるプログラムを消去できます。
・ reload 一度 load したファイルをもう一度読み直します(この時にレジスタとメモリの内容を自動的に初期状態に
戻します)。load 中にエラーが出て止まった場合や、アセンブリファイルを修正した場合にこちらを使います。
・ run プログラムを終了するまで実行します。ポップアップウィンドウの ok ボタンを押すと、Starting address から
実行が開始されます。通常はあらかじめ設定されている 0x00400000 のままにします。コマンドライン引数が必要な場合
には、args に引数を入力します。
・ step 一行ずつプログラムを実行します。実行を開始するアドレスは run と同じです。 step でステップ実行します。
continue を選ぶと run と同じように最後まで実行します。
・ clear レジスタとメモリの内容を初期状態に戻します。新しいプログラムを load する時は必ず clear の memory &
register を行なって下さい。
・ terminal SPIM のシステムコールを使って(例題 2)文字の表示やキーボードからの読み込みをするためのウィンド
ウを開いたり閉じたりします(押す度に閉じたり開いたりする)
。もしエラーメッセージが出て、止まらなくなった時は
Ctrl-C で止められます。
以下は今回は使用しません。
・ set value レジスタまたはメモリのあるアドレスの内容を直接設定します。
・ print 特定のメモリの領域またはグローバルシンボルをメッセージ領域に表示します。
・ breakpoints プログラムのアドレスを設定すると、run した時にそのアドレスで実行が止まります。
・ help メッセージ領域にボタンの簡単な説明を表示します。
・ mode 今回は使用しません
2
使用する命令は、ハンドアウトの一覧表、教科書添付 CD に収録された付録 A を適宜参照して下さい。また、
ホームページにレジスタの表やアセンブラの疑似命令などのリファレンスを用意しましたので活用して下さい。
http://www.u-aizu.ac.jp/course/csI/spim/ (学外)
file:///home/course/csI/public_html/2016/spim/welcome.html (学内)
以下の課題1~5は授業時間内、課題6はレポ-トにまとめて1週間後の授業の時に提出して下さい。
課題1~5は最後のページに解答用紙がありますから、記入して提出して下さい。課題6は作ったプログラムと
実行結果、プログラムの各行の簡単な説明を書いて下さい。(A4 の紙を使用し左上をホッチキスで止めて提出してください。)
※レポートの書き方は、ホームページ上の「良いレポートの書き方」
file:///home/course/csI/public_html/2016/spim/goodrpt.html (学内)を参照して下さい。
2. 例題1・課題1
以下の手順に沿って例題1プログラムを実行し xspim の使い方を理解し、課題1を作成して下さい。
2.1 例題1のアセンブリプログラムを書く
リスト1のアセンブリプログラムを emacs で打ち込んで下さい。ファイル名は適当につけて下さい。アセンブリファイル
の拡張子は *.s です。(例: ex1.s)。これはアセンブラによる足し算の例です。リスト中、1(いち)と l(小文字エル)
、0
(ゼロ)と O(オー)などを間違わないようにして下さい。なお C 言語で書くとリスト2のようになります。
main:
.text
.globl
li
li
add
jr
main
$t0, 0x000f
$t1, 0x0001
$t2, $t0, $t1
$ra
リスト1
#include <stdio.h>
main(){
int t0,t1,t2;
t0 = 0xf;
t1 = 0x1;
t2 = t0 + t1;
}
リスト2
2.2 SPIM 上にプログラムをロードする
xspim の load ボタンで打ち込んだアセンブリファイルをロードします。xspim を起動するとすでに 0x00400000 番地から
プログラムが入っています。これは main を起動するためのスタートアップルーチンです。ロードしたプログラムはこのスタ
ートアップルーチンの後ろにロードされます(次ページの図 7 のように Text Segments にプログラムがロードされた状態に
なります)。
2.3 プログラムを実行する
このプログラムは結果を画面に出力しないので、結果はレジスタを見て確認します。プログラムの流れを追うためにステッ
プ実行をします。このプログラム中ではレジスタ $t0, $t1, $t2 を使用しているので、それらのレジスタに注目して下さい
(図 6)。
3
図 6 レジスタ $t0-$t2
xspim の step ボタンで一行ずつプログラムを実行していきます。実行はスタートアップルーチンのある 0x00400000 から開
始されます。step を数回実行すると、スタートアップルーチンの 0x00400014 番地にある jal main(jal: Jump And Link)
で、次のアドレスが レジスタ $ra に待避され、main にジャンプするようになっています。自分のプログラムの開始位置に
main: というシンボル(ラベルとも呼びます)をつけることで、スタートアップルーチンに自分のプログラムを実行させること
ができます。
図 7 実行の流れ
[0x00400020]番地 から自分のプログラムに入ります。
順を追ってプログラムとレジスタの値の変化を見てみましょう。
打ち込んだプログラムの 3 行目: li $t0, 0x000f
即値 0x000f をレジスタ$t0 にロードします。ここで 0x・・・・は 16 進数を表します。0x000f は 10 進数で 15 になります。
次のようにレジスタの内容が変わります。
R8 (t0) = 0000000f
R9 (t1) = 00000000
R10 (t2) = 00000000
4 行目: li $t1, 0x0001
即値 0x0001 をレジスタ$t1 にロードします。
R8 (t0) = 0000000f
R9 (t1) = 00000001
R10 (t2) = 00000000
5 行目: add $t2, $t0, $t1
レジスタ $t0, $t1 の値を足して、レジスタ$t2 に結果を格納します。
16 進数は 16 (10 進数)で桁上がりするので、16 (10 進数)は 16 進数で 10 (16 進数)となります。
R8 (t0) = 0000000f
R9 (t1) = 00000001
R10 (t2) = 00000010
これで足し算がきちんと行なわれたことが確認できました。
自分のプログラムの最後で jr $ra (jr: Jump to Register) を実行することで、スタートアップルーチンに戻ります。スタ
4
ートアップルーチンに戻ると、0x00400018 ~ 0x0040001c で exit システムコールが呼び出されてプログラムの実行が終了
します。
以降の例題も同じように emacs でアセンブリプログラムを作成し、SPIM で読み込み、実行して下さい。ステップ実行でプログ
ラムの各行でどのような操作が行なわれているか確認しながら進めて下さい。但しループなどの場合は最初の数回動作を確認
したら「continue」で最後まで進めても構いません。
2.4 課題1
①計算結果はどこの何を見れば分るか?
②リスト1、5行目の add 命令を and に変えた時、計算結果はどうなるか?
3. 例題2・課題2
3.1 例題2
例題1はレジスタだけで総ての演算を行いましたが、例題2(リスト3)は メモリに対してロードとストアを行います。
データとしてのメモリは「Data Segments」と書かれた 部分に表示されており、0x10010000 番地から始まることになっていま
す。
.data
data1: .word 0xff
data2: .word 0xeeee
data3: .word 0
.text
.globl main
main: lw
$t0, data1
lw
$t1, data2
add
$t2, $t0, $t1
sw
$t2, data3
jr
$ra
リスト3
プログラムを spim にロードすると分かるのですが、lw $t0, data1 と言う命令は、実際には
lui $1, 4097
lw $8, 0($1)
と言う2つの命令に分解されています。前の命令は 「レジスタの上位半分(16ビット)に即値の値を代入する」 命令で、
レジスタ1の上半分に 4097(0x1001)が代入され、全体では 0x10010000 が代入されます。後ろのロード命令は 「レジスタ1
+0 と言うアドレスから4バイトロード」します。
また、lw $t1, data2 は以下の2命令に分解されます。
lui $1, 4097
lw $9, 4($1)
前の命令は先ほどと同じ動作をします。後ろのロード命令は「レジスタ1+4 と言うアドレスから4バイトロード」します。
各アドレスからデータをロードし、更に次のアドレスにストア する様子をステップ実行で追って行って下さい。
3.2 課題2
①計算結果はメモリのどのアドレスを見れば分るか?
②リスト3から sw 命令を削除した時、計算結果はどこを見れば分るか?
5
4. 例題3・課題3
4.1 例題3
SPIM 版の Hello World です。SPIM ではシステムコール命令(syscall)を使って、オペレーティングシステムのようなサー
ビスを利用することができます。これによって端末への文字、数値の出力、キーボードからの文字、数値の入力をすることが
できます。syscall を使用する時は、レジスタ$v0 に使用するシステムコールの番号、レジスタ$a0~$a3 に引数をロードして
からシステムコールを呼び出します。システムコール番号と引数に関しては、教科書添付 CD に収録された付録 A、または web
ページを参照して下さい。
.data
.asciiz “Hello World¥n”
.text
.globl main
main: li
$v0,4
#print_string
la
$a0, str
syscall
jr
$ra
str:
リスト4
・ 1 行目 .data 以下に続くコードをデータセグメントに格納することをアセンブラに知らせるための疑似命令です。
xspim
の Data Segment ウィンドウにロードされます。
(図8参照)
図8 メモリ構造
・ 2 行目 .asciiz は文字列をメモリに格納するための疑似命令です(文字列の最後に null が入ります)。C 言語と同じ
ように ¥n、¥t、¥”が使えます。データが格納されるアドレスは自動的に割り振られるので、プログラムから文字列の格
納されるアドレスを参照できるようにシンボル(str)をつけます。シンボルにはシンボルの直後にあるデータまたは命令
のアドレスが入ります(この場合、文字列 Hello World\n の H が格納されるアドレス。図9参照)。シンボル名には英数
字、 _、 .、が使えます。ただし数字で始まる名前と、命令や疑似命令と同じ名前は使えません。
・
・
・
・
・
図9 文字列
3 行目 .text 以下に続くコードをテキストセグメントに格納することをアセンブラに知らせるための疑似命令です。
xspim の Text Segments ウィンドウにロードされます。
4 行目 シンボル main を他のファイルから参照できるように宣言します。
5 行目 レジスタ $v0 に print_string システムコールの番号 4 をロードします。# 以降はコメントになります。
6 行目 レジスタ $a0 に print_string の引数である文字列(str)のアドレス(C 言語でいうポインタ)をロードします。
つまり str の指し示すアドレスをレジスタ $a0 にロードします。アドレスをロードする時は la(Load Address) 命令
を使います。
7 行目 print_string システムコールを呼びます。
4.2 課題3
①実行後の$v0 の値はいくらか?
②実行後の$a0 の値はいくらか?
6
5. 例題4・課題4
5.1 例題4
特にC言語での配列やアドレスについて知っておくべき事は 以下のような事です。
1. 配列は連続したメモリ領域に取られる
2. 要素の添字(インデックス)が0の要素から順にアドレスが高くなる方向に
3. データが連続して配置される。
4. 変数のアドレスを知るにはその変数に「&」を付ける。配列の場合も同じ。
(例えば配列の要素 a[5]のアドレスは &a[5]とすれば良い。
)
5. printf でアドレスを表示するには「%p」書式を 使用する
以下のCプログラムは配列の各要素のアドレスを順に表示するプログラムです。 コンパイルして実行してみてください。
アドレスが4(つまり int 型の大きさ分)飛ばしに大きくなるのが分かると思います。
(実行結果のアドレスは実行する環境
によって変わります)
#include <stdio.h>
main()
{
int items[10] = {0x59, 0x3, 0xc8, 0xbd6, 0x443,
0x3e6, 0xf, 0x1e, 0x13f05, 0x150};
int i;
for(i = 0; i < 10; i++){
printf("&items[%d]:%p¥n",i,&items[i]);
}
}
リスト5
実行結果(アドレスは環境によって変わる)
&items[0]:ffbefac8
&items[1]:ffbefacc
&items[2]:ffbefad0
&items[3]:ffbefad4
...
それでは今度はリスト5と同じ配列と、その要素0、1の値をレジスタに持って来るアセンブリプログラムで作ってみまし
ょう。
(リスト6)
.data
items: .word
0x59, 0x3, 0xc8, 0xbd6, 0x443, 0x3e6, 0xf, 0x1e, 0x13f05, 0x150
.text
.globl
main: la
lw
lw
jr
main
$t2, items
$t0, 0($t2)
$t1, 4($t2)
$ra
リスト6
items...と言う行が配列(この場合 int 型の配列)を表しています。 .word は1ワード(4バイト)のデータの(一個以上
の)羅列を表しています。la $t2, items と言う命令で配列の先頭アドレス(一番低いアドレス、又は要素0のアドレス
&items[0])をレジスタt2に セットします。そうすると二つのロードのアドレス、0($t2)、 4($t2)はそれぞれ、要素0と
要素1のアドレス (&items[0] と&items[1] )を表すことになります。 従ってレジスタt0とt1にはそれぞれ items[0]
と items[1] の値が読まれることになります。
実際に実行して確かめてみてください。
5.2 課題4
① &items[4]は何か?その値と$t2 との差はいくらか?
② items[4]の値を$t3 にロードする命令を書きなさい
7
6. 例題5・課題5
6.1 例題5
リスト6を少し改造して、要素 items[0]の値を spim コンソールに表示するように変えてみました。
(リスト7)
このプログラムはリスト6と同じ事を行っていますが、syscall を使用してロードしたデータを spim コンソールに表示する
ようにしたものです。
・ 一回目の syscall は items[0]の内容を10進整数で 表示します。
・ 二回目の syscall は文字列”\n”(つまり改行) を表示します。
・ 三回目の syscall は、一回目と同様に、items[1] の内容を10進整数で表示します。
.data
items: .word
0x59, 0x3, 0xc8, 0xbd6, 0x443, 0x3e6, 0xf, 0x1e, 0x13f05, 0x150
nl:
.asciiz "¥n"
.text
.globl main
main: la
$t2, items
lw
$a0, 0($t2)
li
$v0, 1
syscall
#print items[0]
la
$a0, nl
li
$v0, 4
syscall
#print newline
lw
$a0, 4($t2)
li
$v0, 1
syscall
#print items[1]
jr
$ra
リスト7
6.2 課題5
・syscall の種類を引数と共にまとめなさい。
7. 例題6・課題6
7.1 例題6
リスト8は、メモリ中に並ぶ 10 個の 32 ビットの整数値(配列)の合計を求めるものです。
#include <stdio.h>
main()
{
int i, sum = 0;
int items[10] = {0x59, 0x3, 0xc8, 0xbd6, 0x443,
0x3e6, 0xf, 0x1e, 0x13f05, 0x150};
for(i = 0; i < 10; i++){
sum += items[i];
}
printf("sum = %d¥n",sum);
}
リスト8
8
これをアセンブリプログラムにしたのが、リスト9です。このプログラムでは、$t0 をループ用のカウンタ(i)、$t1 を合計
(sum)、$t2 を配列データ(items)のアドレス、$t3 を配列の 1 要素(items[i])を格納する一時的な変数として使っています。
str:
.data
.asciiz "sum = "
items: .word
main:
loop:
.text
.globl
add
add
la
lw
add
addi
addi
blt
0x59, 0x3, 0xc8, 0xbd6, 0x443, 0x3e6, 0xf, 0x1e, 0x13f05, 0x150
main
$t0,
$t1,
$t2,
$t3,
$t1,
$t2,
$t0,
$t0,
li
$v0,
la
$a0,
syscall
li
$v0,
move
$a0,
syscall
jr
$ra
$0, $0
$0, $0
items
0($t2)
$t1, $t3
$t2, 4
$t0, 1
10, loop
4
str
1
$t1
リスト9
以後プログラムを順に説明します。
・ 3 行目 カンマで区切られた数値(32bit)をメモリ中の連続した領域に格納します。データの先頭が 4 の倍数になるアド
レスに格納されます。この場合 .asciiz で格納した文字列が中途半端なアドレスで終っているので、次の 4 バイトの境
界から格納されることになります(図10)。
図10 データの格納のされ方
・ 6~7 行目 レジスタ\$t0, $t1, $t2 を 0 で初期化します。move 命令はレジスタからレジスタへ値を移動(コピー)する
時に使用します。 レジスタ$zero は値が 0 に固定された特殊なレジスタです。
・ 8 行目 シンボル items が指すアドレスをレジスタ$t2 にロードします。
・ 9 行目 レジスタ$t2 のアドレスの内容をレジスタ$t3 にロードします。items には配列の先頭アドレスが入っており、1
回目のループでは items($2) の指すデータは 0x59 になります。なお、それに 4 を足したアドレスに配列の 2 番目の
要素(0x3)、8 を足したアドレスに配列の 3 番目の要素(0xc8)が入っています。
・ 10 行目 $t3 を $t1 に足して $t1 に格納します。10 行目が sum += items[i]; に相当します。
・ 11 行目 $t2 を配列の次のデータのオフセット(+4)にします。ここで$t2 の指すアドレスは配列の次の要素になります。
・ 12 行目 ループ用のカウンタを 1 増やします。
・ 13 行目 カウンタの値が 10 より小さければ、シンボル loop が指すアドレス(10 行目)に戻ります。blt は branch less
than です。
・ 15~20 行目 合計を表示します。15~17 行目で sum = という文字列を表示し、18~20 行目で合計を表示します。整数
値の表示には print_int というシステムコールを使います。このシステムコールは引数のレジスタ$a0 の値を表示しま
す。
9
7.2 課題6
例題6を参考に 1 から n (n > 1)までの整数の和を求めて表示するアセンブリプログラムを作りなさい。n は syscall で読
んでも、定数として持っても構いません。
(注)本課題を、例題 6 のプログラムの配列データの内容を、0x01、0x02、0x03・・と置き換えて実現するのは、同じ答えは出
ますが、不可とします。
ヒント
1) プログラムを書く前にまず、変数として使用するレジスタを決めます。例えば、$t0 を n, $t1 を合計、$t2 をループのカ
ウンタとします。それぞれのレジスタの役割を書き留めておくといいでしょう。
2) 本来は n(n+1)/2 で求めるべきですが、乗算、割算命令を使わなければいけないので、今回は n 回ループしてループカウ
ンタを足して合計を求めて下さい(余裕のある人は乗算、割算命令でもやってみて下さい)。
3) プログラムを言葉で書くと次のようになります(あくまでも例です)。
.text
.globl main
main: 各レジスタの初期化(合計、カウンタは 0 、n は適当な値)
loop: カウンタを 1 増やす
合計にカウンタを加える
カウンタが n より小さければ loop にジャンプ
結果の出力
jr
$ra
4) 結果の出力は例題6の 15~20 行を参考にして下さい。
10
8. 補足
8.1 エラーメッセージ
・Cannot open file: ‘hoge.s' (ロード時)
ファイルがありません。ファイル名やパスが違っていないか確認して下さい。
・spim: (parser) Label is defined for the second time on line xx of file hoge.s (ロード時)
ラベルが 2 重に定義されています。二回ロードしてしまった可能性が高いです。一度 clear → memory & register をし
てから load し直して下さい。
・spim: (parser) syntax error on line xx of file hoge.s (ロード時)
構文が間違っています。オペランド等が違っていないか確認して下さい。コンパイラで生成した場合、疑似命令でこのエラ
ーが出たらその疑似命令は消すかコメントアウトして下さい。
・Attempt to execute non-instruction at 0x00400030 (実行時)
実行しようとしたアドレスにプログラムがロードされていません。プログラムを実行する時に実行開始アドレスを確認して
下さい。プログラムの最後で jr $ra するか exit システムコールを呼び出してきちんと終了していない場合に、プログラ
ムのロードされていないところを実行しようとしてこのエラーが出ることもあります。
8.2 発生しやすいトラブルとその対処法
・アセンブリファイルがロードできない
・Cannot open file: ‘...' のメッセージが表示される。
→ パスが正しいか?(ファイル名は相対または絶対パスで指定)
相対パスは、xspim を起動した時の working directory が基準になります。
→ ‘hoge.s' のように最後に余分なスペースが入ってないか?
→ csh と違って '~' は使えません。
・main が 2 つあるといわれる (Label is defined for the second time ...)
→ 2 重にロードしていないか?
ロードは clear の memory & register を実行してから。
(同じファイルの場合は reload でも可)
・syntax error が出る
→ syntax error は文法ミス。問題の行を確認。
→ 最下行に syntax error があるといわれた場合、ファイルの最後に改行がない。
→ 改行コードが正しい(LF)か?(主にプログラムを家や研究室の PC で作成した場合)
確かめる方法は、現在のバッファの Encoding-system によって mule(emacs)の下のバーの表示が変わるので、それで判
断してください。例えば
[あ]E.:--**-Emacs: test
5桁目の E が EUC で、6桁目の.が unix 形式(LF 改行)を表しています。6桁目が:だと dos(CR+LF)、'だと Mac(CR)、
_はデフォルトエンコーディングを表しています。dos からもってきたファイルを読み込むと
[あ]S::--**-Emacs: test
となります。
→ 改行コードが違う場合は修正して下さい。方法としては mule(emacs)で C-x C-f でファイルを読み込むときや、 C-x Cw でファイルを書き出すときに C-u を前につけるとエンコードの指定ができますのでそれを使って下さい。
例えば、C-u C-x C-w とすると、セーブするファイル名を聞いてきたあとに Encoding-system を聞いてくるので、そこ
で*euc-japan*unix と指定すれば改行コードは LF になります。同様に*euc-japan*dos や *euc-japan*mac もあります。
読むときは自動判別してくれるようですが、特に指定するときは C-u C-x C-f でできます。
※正常にロードできた場合は何もメッセージが表示されません。
・打ち込んだアセンブリコードと xspim 上のニーモニック表示が違う
→ spim がマクロ展開をしているだけなので問題ありません。
「マクロ」とは実際にはない便宜的な命令で、アセンブラによって
本当の命令に置き換わります。
(下例参照)
マクロ展開の例:
move
$t1, $t0
->
addu
$t1, $zero, $t0
li
$t1, 1
->
ori
$t1, $zero, 1
11
(計算用紙)
12
解答用紙
学籍番号________________________
氏名__________________________
課題1
①________________________________
②________________________________
課題2
①________________________________
②________________________________
課題3
①________________________________
②________________________________
課題4
①________________________________
②________________________________
課題5
13