MIPSプログラムの基本型

MIPSのアセンブリコードは基本的に以下のような形をしています。本演習ではこのコードを必要に応じてコピーして使用してかまいません。

.data # ここから先はデータセグメント(データがなければ不要) [ここにデータを記載する] .text # ここから先はテキストセグメント .globl main # mainをグローバルラベルとする main: addi $sp, $sp, -4 # スタック領域の確保(第3回で解説) sw $ra, 0($sp) # 戻り先アドレスの退避(第3回で解説) [ここから計算の本体を書き始める] lw $ra, 0($sp) # 戻り先アドレスの復元 (第3回で解説) addi $sp, $sp, 4 # スタック領域の開放(第3回で解説) jr $ra # main を終了してリターンする

mainはシステムからサブルーチンとして呼び出され、呼び出し元のアドレスが$raに格納されている。このため、mainの終了が「jr $ra」となっている。プログラム内でこの$raを値を破壊してしまうと、呼び出し元に戻れず正しく終了できないので、レジスタ$raの扱いには特に注意すること。

1から10までの和を求めるプログラムの例

# 1 から 10 までの総和を求める .text .globl main # レジスタの割り当て # $t0:総和 # $t1:ループの残り回数 main: addi $sp, $sp, -4 # スタック領域の確保(第3回で解説) sw $ra, 0($sp) # 戻り先アドレスの退避 (第3回で解説) move $t0, $zero # $t0を0に初期化 li $t1, 10 # $t1を10に初期化 loop: add $t0, $t0, $t1 # $t0 ← $t0 + $t1 sub $t1, $t1, 1 # $t1 ← $t1 - 1 bne $t1, $zero, loop # $t1が0\でないならラベルloopへ跳ぶ li $v0, 1 # システムコール1(print_int)の指定 move $a0, $t0 # 引数$a0に$t0をセットする syscall # 値の表示 lw $ra, 0($sp) # 戻り先アドレスの復元(第3回で解説) addi $sp, $sp, 4 # スタック領域の開放(第3回で解説) jr $ra # プログラムの終了

アセンブリコードの書式

1行に1命令。

上記のmainやloopのように、":" が続く識別子は「ラベル」という。ジャンプ命令のジャンプ先アドレスや、データ領域のアドレスを参照するために使用します。名前がMIPSの命令と重複しないよう注意。

8文字ごとの字下げをして記述することが慣用となっています。

  • ラベルは行頭に置く
  • 命令は9文字目から書く(8文字目までの空白もしくはTABを挿入)
  • レジスタ等の指定は17文字目から書く(16文字目までの空白もしくはTABを挿入)

プログラム例「入力が0かの判定」

入力された整数値が0かどうかを判定し、その結果に応じて"zero"もしくは"non zero"の文字列を出力するプログラムの例を載せます。ここで .asciizは末尾がnull文字で終わる文字列をメモリに格納することを意味しています。

# 入力が 0 かどうか調べる .data str_z: .asciiz "zero\n" # 文字列データ "zero\n" str_nz: .asciiz "non zero\n" # 文字列データ "non zero\n" .text .globl main main: addi $sp, $sp, -4 # スタック領域の確保(第3回で解説) sw $ra, 0($sp) # 戻り先アドレスの退避(第3回で解説) li $v0, 5 # システムコールread_intの指定 syscall # 値の読み込み beq $v0, $zero, zero # 入力$v0が0ならラベルzeroへ跳ぶ la $a0, str_nz # 引数として文字列"non zero"を指定 b end # ラベルendへ無条件に跳ぶ zero: la $a0, str_z # 引数として文字列"zero"を指定 end: li $v0, 4 # システムコールprint_stringの指定 syscall # 文字列の表示 lw $ra, 0($sp) # 戻り先アドレスの復元(第3回で解説) addi $sp, $sp, 4 # スタック領域の開放(第3回で解説) jr $ra # プログラムの終了

ここではシステムコールread_intで読み込んだ値が返されるレジスタ$v0を$zeroレジスタと比較し、等しければラベルzeroにジャンプ (beqによる条件付き分岐) しています。ここがちょうどC言語のifの処理に当たります。

beqでジャンプせず、そのままを処理を継続した場合は、無条件ジャンプのb命令でラベルzeroからの処理を飛び越え、ラベルendに処理が移ります。こうした流れによりC言語のif ~ else ~の構造が実現されています。

改行の出力方法

改行コードの文字列データを用意し、これをシステムコールprint_stringで出力することで改行を実現します。

# 改行の出力方法 .data newln: .asciiz "\n" # 改行コードの文字列データ .text .globl main main: ...... li $v0, 4 # システムコールprint_stringの指定 la $a0, newln # 引数として改行の文字列データを指定 syscall # 文字列の表示 ...... jr $ra

スタックの利用−レジスタ値の待避や局所変数の作成−

CPU が持っているレジスタの数には限りがあり、各レジスタの使用方法も定められています。したがって、少しでも大きなプログラムを書くときにはメモリ上にデータ領域を確保し、メモリとレジスタの間でのデータ転送が不可欠になります。ここで、予めデータ領域を確保しておく方法 (大域変数に当たる) の他に、必要に応じて領域を確保する方法があります。後者のために使用されるのが「スタック」です。

一般にスタックはアドレスが大きいメモリからアドレスが小さいメモリに向かって順に使用していきます。どこまで使用しているかを指し示すのが「スタックポインタ」で、MIPSではレジスタ$sp がこれに当たり、$spが指すメモリまでが使用されていることになります。

スタックの図

たとえば$spから16を引くと 16バイト (= 4ワード) 分の領域を確保したことになり、4ワードの値をスタックに納めることが可能となります。この場合、0($sp), 4($sp), 8($sp), 12($sp) の4ワード分のメモリを使用できる。用途はレジスタ値の待避だけでなく、局所変数として使用しても良い。

なお、スタックに確保したデータ領域は、使用後には確保した分をきちんと解放しなくてはなりません。この例で言うと、使用後には $sp に 16 を加える必要があります。

よくあるプログラミングスタイルでは、計算を始める前にメモリを確保し、全ての計算が終わったらメモリを解放します。これにレジスタの値の待避と復元の処理を合わせると以下の例のようになります。ここでは戻り先アドレスを示す$raと、作業レジスタ$s0, $s1の3つのレジスタの内容について、スタックへの待避と復元をしています。

sample: sub $sp, $sp, 12 # スタックに3ワードの領域を確保 sw $ra, 0($sp) # 戻り先アドレスの退避 sw $s0, 4($sp) # $s0の退避 sw $s1, 8($sp) # $s1の退避 (計算処理をここに書く) (値を待避したので $s0 と $s1 を使用できる) lw $s1, 8($sp) # $s1の復元 lw $s0, 4($sp) # $s0の復元 lw $ra, 0($sp) # 戻り先アドレスの復元 addi $sp, $sp, 12 # スタックに確保した領域の開放 jr $ra # 呼び出し元へ戻る

サブルーチン利用の基本型

サブルーチンを使ったプログラムの基本的なテンプレートを載せる。

ここでは、サブルーチンsub1がサブルーチンsub2を呼び出し、sub2は他のサブルーチンを一切使用していないと仮定している。また $s0~$s2 等の他のサブルーチンを呼び出しても値が変更されない作業用レジスタを使用してる。

なお、sub2については他のサブルーチンを呼び出さないため、値の保存が保証されない$t0~$t9を値が変更される心配なしに使用することが可能である。

# サブルーチンsub1 # サブルーチンsub2を呼び出すので、$raの待避は必須。 # 作業用にレジスタ$s0~$s2を使用すると仮定している sub1: sub $sp, $sp, 16 # スタックに4ワードの領域の確保 sw $ra, 0($sp) # 戻り先アドレスの退避 sw $s0, 4($sp) # $s0の退避 sw $s1, 8($sp) # $s1の退避 sw $s2, 12($sp) # $s2の退避 ($s0~$s2を使用した計算) (sub2に渡す引数$a0~$a3などの設定) jal sub2 # サブルーチンsub2の呼び出し (sub2の計算結果$v0や$v1などの利用) ($s0~$s2を使用した計算) lw $s2, 12($sp) # $s2の復元 lw $s1, 8($sp) # $s1の復元 lw $s0, 4($sp) # $s0の復元 lw $ra, 0($sp) # 戻り先アドレスの復元 addi $sp, $sp, 16 # スタックに確保した領域の開放 jr $ra # サブルーチンsub2 # 他のサブルーチンやシステムコールを利用しない場合、 # $raを待避する必要はない。 # 作業用にレジスタ$s0を使用すると仮定している。 sub2: sub $sp, $sp, 4 # スタックに1ワードの領域を確保 sw $s0, 0($sp) # $s0の退避 (他のサブルーチンは利用していない) ($s0や$t0~$t9を使用した計算) lw $s0, 0($sp) # $s0の復元 addi $sp, $sp, 4 # スタックに確保した領域の開放 jr $ra
HOMEに戻る