MIPS アセンブリ入門 🚀

コンピュータアーキテクチャ

コンピュータの仕組みを深く理解するための第一歩

こんにちは!このブログ記事では、MIPS アセンブリ言語の世界への扉を開きます。コンピュータサイエンスを学ぶ学生さんや、組み込みシステムに興味がある方、あるいは単にコンピュータが内部でどのように動作しているのか深く知りたいと考えている方にとって、MIPS アセンブリは非常に興味深く、教育的なトピックです。

MIPSって何?🤔

MIPS は “Microprocessor without Interlocked Pipeline Stages” の略で、1981年にスタンフォード大学のジョン・L・ヘネシー教授とそのチームによって始められたプロジェクトから生まれました。1984年には MIPS Computer Systems 社が設立され、R2000 (1985年)、R3000 (1988年) といったプロセッサが開発されました。MIPS は RISC (Reduced Instruction Set Computer) アーキテクチャの一種であり、命令セットが比較的シンプルで、学習に適しているため、多くの大学のコンピュータアーキテクチャの授業で教材として採用されています。PlayStation や Nintendo 64 といったゲーム機や、ルーターなどのネットワーク機器、組み込みシステムで広く利用されてきました。

なぜMIPSアセンブリを学ぶの?

高級言語(C言語やJava、Pythonなど)でプログラムを書いていると、コンピュータの内部動作は抽象化されて見えにくくなります。しかし、アセンブリ言語を学ぶことで、以下のようなメリットがあります。

  • コンピュータアーキテクチャの理解: CPUのレジスタ、メモリ、命令セットといった基本的な要素がどのように連携してプログラムを実行するのかを深く理解できます。
  • 低レベルの最適化: プログラムのどの部分がパフォーマンスに影響を与えているのかを理解し、効率的なコードを書くための洞察が得られます。(ただし、現代のコンパイラは非常に優秀なので、手書きのアセンブリで最適化が必要な場面は限定的です。)
  • デバッグ能力の向上: 低レベルでのデバッグやリバースエンジニアリングに役立ちます。
  • OSやコンパイラの理解: オペレーティングシステムやコンパイラがどのように動作しているのか、その基礎を理解する助けになります。

アセンブリ言語は特定のプロセッサアーキテクチャに依存するため、例えば MIPS アセンブリで書かれたコードは、一般的なPCで使われる x86 プロセッサやスマートフォンの ARM プロセッサでは直接動作しません。しかし、基本的な概念は他のアーキテクチャにも通じる部分が多く、学習する価値は十分にあります。

さあ、MIPS アセンブリの基本を探求していきましょう!🔧

MIPS アーキテクチャの基本 🏗️

MIPS アセンブリを理解するためには、まず MIPS プロセッサの基本的な構造を知る必要があります。

レジスタ

CPU は計算を行う際に、メモリからデータをいちいち読み書きするのではなく、CPU 内部にある高速な記憶領域であるレジスタを使用します。MIPS (ここでは MIPS32 を想定) には、通常 32 本の汎用レジスタがあり、それぞれ 32 ビット幅です。これらのレジスタには番号 ($0 から $31) と、用途を示す名前が付けられています。

以下は、特によく使われるレジスタとその慣例的な用途です。

名前 番号 用途 説明 関数呼び出しで保持されるか
$zero $0 定数 0 常に値が 0。書き込み不可。 N/A
$at $1 アセンブラ予約 アセンブラが擬似命令を変換する際に使用。通常は直接使わない。 No
$v0$v1 $2 – $3 戻り値 / システムコール番号 関数からの戻り値や、システムコールの種類を指定するために使用。 No
$a0$a3 $4 – $7 引数 関数に引数を渡すために使用 (最初の4つ)。 No
$t0$t7 $8 – $15 一時レジスタ 一時的な計算結果を保存するために使用。 No (呼び出し元が保存)
$s0$s7 $16 – $23 保存されるレジスタ 関数呼び出し後も値を保持したい場合に使用。 Yes (呼び出し先が保存)
$t8$t9 $24 – $25 一時レジスタ 一時的な計算結果を保存するために使用。 No (呼び出し元が保存)
$k0$k1 $26 – $27 OS カーネル予約 オペレーティングシステム用に予約されている。 N/A
$gp $28 グローバルポインタ 静的データ領域へのアクセスを効率化するために使用。 Yes
$sp $29 スタックポインタ スタック領域の現在のトップを指す。 Yes
$fp $30 フレームポインタ 現在の関数呼び出しのスタックフレームの基点を指す。 Yes
$ra $31 リターンアドレス 関数呼び出し (jal 命令) 時に、呼び出し元に戻るためのアドレスが格納される。 No

注意: これらの用途はあくまで慣例(ABI: Application Binary Interface による規約)であり、CPU 自体が強制するものではありません。しかし、他のコード(特にライブラリや OS)と連携するためには、この規約に従うことが非常に重要です。特に $t レジスタと $s レジスタの扱いの違い(関数呼び出し時に値を保持する必要があるか)は重要です。

メモリ

MIPS はバイトアドレッシングを採用しています。つまり、メモリの各バイトに一意のアドレスが割り当てられています。しかし、MIPS は 32 ビットアーキテクチャであり、多くの命令は 4 バイト(1 ワード)単位でデータを扱います。ワードアクセス (lw, sw など) を行う場合、アクセスするメモリアドレスは通常 4 の倍数である必要があります(アラインメント制約)。

プログラムのメモリ空間は、一般的に以下のようなセグメントに分けられます。

  • テキストセグメント (.text): プログラムの機械語命令が格納される領域。通常は読み取り専用。
  • データセグメント (.data): 初期化されたグローバル変数や静的変数が格納される領域。
  • BSSセグメント (.bss): 初期化されていないグローバル変数や静的変数が格納される領域。プログラム開始時に 0 で初期化される。(.space ディレクティブで作られる領域)
  • ヒープ領域: プログラム実行中に動的にメモリを確保・解放するための領域(例: C言語の malloc)。
  • スタック領域: 関数のローカル変数、引数、戻りアドレスなどを一時的に保存するための領域。通常、高位アドレスから低位アドレスに向かって伸長する。

命令フォーマット

MIPS の命令はすべて 32 ビットの固定長です。これにより、命令のデコードが単純化され、高速化に寄与します。主な命令フォーマットには以下の 3 種類があります。

  • R形式 (Register format): レジスタ間の演算に使用されます(例: add, sub, and)。オペコード (op)、ソースレジスタ (rs, rt)、デスティネーションレジスタ (rd)、シフト量 (shamt)、関数コード (funct) のフィールドを持ちます。
      op  |  rs |  rt |  rd | shamt | funct
    ------|-----|-----|-----|-------|--------
     6bit | 5bit| 5bit| 5bit|  5bit |  6bit
  • I形式 (Immediate format): 即値(命令内に直接埋め込まれた定数)を使用する演算や、ロード/ストア命令、条件分岐命令に使用されます(例: addi, lw, beq)。オペコード (op)、ソースレジスタ (rs)、ターゲット/デスティネーションレジスタ (rt)、即値/アドレスオフセット (immediate) のフィールドを持ちます。
      op  |  rs |  rt |   immediate
    ------|-----|-----|---------------
     6bit | 5bit| 5bit|     16bit
  • J形式 (Jump format): 無条件ジャンプ命令に使用されます(例: j, jal)。オペコード (op) とジャンプ先アドレス (address) のフィールドを持ちます。
      op  |          address
    ------|--------------------------
     6bit |          26bit

オペコード (op) フィールドの値によって、どの形式の命令かが決まります。R形式の命令は op が 0 で、funct フィールドで具体的な演算内容を区別します。

基本的な MIPS 命令 ⌨️

MIPS アセンブリプログラミングの基本となる命令をいくつか見ていきましょう。MIPS はロード/ストアアーキテクチャなので、メモリ上のデータを直接演算することはできず、まずレジスタにロードしてから演算を行い、必要であれば結果をメモリにストアします。

データ転送命令

メモリとレジスタ間でデータを移動させるための命令です。

  • lw $rt, offset($rs) (Load Word): メモリアドレス $rs + offset から 1 ワード (4 バイト) のデータをレジスタ $rt にロードします。offset は 16 ビットの符号付き即値です。
  • sw $rt, offset($rs) (Store Word): レジスタ $rt の値をメモリアドレス $rs + offset に 1 ワード (4 バイト) 書き込みます。
  • li $rt, immediate (Load Immediate): 32 ビットの即値 immediate をレジスタ $rt にロードします。これは擬似命令であり、アセンブラが通常 lui (Load Upper Immediate) と ori (OR Immediate) の 2 つの命令に展開します。
  • la $rt, label (Load Address): ラベル label が示すメモリアドレスをレジスタ $rt にロードします。これも擬似命令です。
  • move $rd, $rs (Move): レジスタ $rs の値をレジスタ $rd にコピーします。これも擬似命令で、実際には add $rd, $rs, $zeroor $rd, $rs, $zero などに展開されます。
  • lb $rt, offset($rs) / lbu $rt, offset($rs) (Load Byte / Load Byte Unsigned): 1 バイトをロードします。lb は符号拡張、lbu はゼロ拡張を行います。
  • sb $rt, offset($rs) (Store Byte): 1 バイトをストアします。
  • lh $rt, offset($rs) / lhu $rt, offset($rs) (Load Halfword / Load Halfword Unsigned): 2 バイト(ハーフワード)をロードします。lh は符号拡張、lhu はゼロ拡張を行います。
  • sh $rt, offset($rs) (Store Halfword): 2 バイト(ハーフワード)をストアします。

算術演算命令

数値計算を行うための命令です。

  • add $rd, $rs, $rt (Add): $rs$rt の値を加算し、結果を $rd に格納します。符号付き演算で、オーバーフローが発生すると例外(トラップ)が発生します。
  • addu $rd, $rs, $rt (Add Unsigned): add と同様ですが、符号なし演算として扱われ、オーバーフローしても例外は発生しません。(計算自体は add と同じです)
  • sub $rd, $rs, $rt (Subtract): $rs から $rt の値を減算し、結果を $rd に格納します。符号付き演算で、オーバーフローで例外が発生します。
  • subu $rd, $rs, $rt (Subtract Unsigned): sub と同様ですが、符号なし演算として扱われ、オーバーフローしても例外は発生しません。
  • addi $rt, $rs, immediate (Add Immediate): $rs の値に 16 ビットの符号付き即値 immediate を加算し、結果を $rt に格納します。オーバーフローで例外が発生します。
  • addiu $rt, $rs, immediate (Add Immediate Unsigned): addi と同様ですが、符号なし演算として扱われ、オーバーフローしても例外は発生しません。(addi と同様に即値は符号拡張されます)
  • mult $rs, $rt (Multiply): $rs$rt の値を乗算します。結果は 64 ビットになり、上位 32 ビットが特殊レジスタ Hi に、下位 32 ビットが Lo に格納されます。
  • div $rs, $rt (Divide): $rs$rt で除算します。商が Lo に、剰余が Hi に格納されます。
  • mfhi $rd (Move From Hi): 特殊レジスタ Hi の値を $rd に移動します。
  • mflo $rd (Move From Lo): 特殊レジスタ Lo の値を $rd に移動します。

論理演算命令

ビット単位の論理演算を行います。

  • and $rd, $rs, $rt (AND): $rs$rt のビットごとの論理積を計算し、結果を $rd に格納します。
  • or $rd, $rs, $rt (OR): $rs$rt のビットごとの論理和を計算し、結果を $rd に格納します。
  • xor $rd, $rs, $rt (XOR): $rs$rt のビットごとの排他的論理和を計算し、結果を $rd に格納します。
  • nor $rd, $rs, $rt (NOR): $rs$rt のビットごとの否定論理和を計算し、結果を $rd に格納します。
  • andi $rt, $rs, immediate (AND Immediate): $rs の値と 16 ビットのゼロ拡張された即値 immediate のビットごとの論理積を計算し、結果を $rt に格納します。
  • ori $rt, $rs, immediate (OR Immediate): $rs の値と 16 ビットのゼロ拡張された即値 immediate のビットごとの論理和を計算し、結果を $rt に格納します。
  • xori $rt, $rs, immediate (XOR Immediate): $rs の値と 16 ビットのゼロ拡張された即値 immediate のビットごとの排他的論理和を計算し、結果を $rt に格納します。

シフト命令

レジスタ内のビットを左右にシフトさせます。

  • sll $rd, $rt, shamt (Shift Left Logical): レジスタ $rt の値を shamt ビット左に論理シフトし、結果を $rd に格納します。空いたビットには 0 が入ります。shamt は 5 ビットの即値です。
  • srl $rd, $rt, shamt (Shift Right Logical): レジスタ $rt の値を shamt ビット右に論理シフトし、結果を $rd に格納します。空いたビットには 0 が入ります。
  • sra $rd, $rt, shamt (Shift Right Arithmetic): レジスタ $rt の値を shamt ビット右に算術シフトし、結果を $rd に格納します。空いたビットには元の最上位ビット(符号ビット)がコピーされます。
  • sllv $rd, $rt, $rs (Shift Left Logical Variable): $rs レジスタの下位 5 ビットで指定されたビット数だけ $rt を左に論理シフトします。
  • srlv $rd, $rt, $rs (Shift Right Logical Variable): $rs レジスタの下位 5 ビットで指定されたビット数だけ $rt を右に論理シフトします。
  • srav $rd, $rt, $rs (Shift Right Arithmetic Variable): $rs レジスタの下位 5 ビットで指定されたビット数だけ $rt を右に算術シフトします。

制御フロー命令

プログラムの実行順序を変更するための命令です。

  • j label (Jump): 指定されたラベル label のアドレスに無条件にジャンプします。J形式命令。
  • jal label (Jump and Link): 指定されたラベル label にジャンプし、その際に次の命令のアドレス(戻りアドレス)を $ra レジスタに保存します。関数呼び出しに使用されます。J形式命令。
  • jr $rs (Jump Register): レジスタ $rs に格納されているアドレスにジャンプします。関数からの戻り (jr $ra) や、計算されたアドレスへのジャンプに使用されます。R形式命令。
  • beq $rs, $rt, label (Branch on Equal): レジスタ $rs$rt の値が等しい場合、指定されたラベル label に分岐します。I形式命令。
  • bne $rs, $rt, label (Branch on Not Equal): レジスタ $rs$rt の値が等しくない場合、指定されたラベル label に分岐します。I形式命令。
  • blt $rs, $rt, label (Branch on Less Than): $rs < $rt (符号付き比較) の場合に分岐します。(擬似命令)
  • ble $rs, $rt, label (Branch on Less Than or Equal): $rs <= $rt (符号付き比較) の場合に分岐します。(擬似命令)
  • bgt $rs, $rt, label (Branch on Greater Than): $rs > $rt (符号付き比較) の場合に分岐します。(擬似命令)
  • bge $rs, $rt, label (Branch on Greater Than or Equal): $rs >= $rt (符号付き比較) の場合に分岐します。(擬似命令)
  • slt $rd, $rs, $rt (Set on Less Than): $rs < $rt (符号付き比較) であれば $rd に 1 を、そうでなければ 0 を設定します。
  • slti $rt, $rs, immediate (Set on Less Than Immediate): $rs < immediate (符号付き比較) であれば $rt に 1 を、そうでなければ 0 を設定します。
  • sltu, sltiu (Set on Less Than Unsigned / Immediate Unsigned): 符号なし比較版の slt, slti です。
分岐遅延スロット (Branch Delay Slot):
初期の MIPS アーキテクチャには「分岐遅延スロット」という特徴がありました。これは、分岐命令(beq, bne, j など)の直後にある命令が、分岐するかどうかにかかわらず、必ず実行されるというものです。パイプラインの効率を高めるための設計でしたが、プログラミングを複雑にする要因でもありました。現代の MIPS プロセッサや、SPIM/MARS などのシミュレータでは、この遅延スロットの影響を隠蔽したり、無効にしたりする設定があることが多いです。この入門記事では、簡単のため分岐遅延スロットは考慮しない(シミュレータがよしなに扱ってくれる)前提で進めますが、実際のハードウェアを扱う際には注意が必要です。

簡単な MIPS プログラムの作成 ✍️

基本的な命令を学んだところで、実際に簡単な MIPS アセンブリプログラムを書いてみましょう。

プログラムの構造

MIPS アセンブリプログラムは、通常、いくつかのセクションに分かれています。これはアセンブラへの指示(ディレクティブ)を使って指定します。

  • .data: このディレクティブ以降は、データセグメントに配置されるデータを定義します。変数や定数などを置きます。
  • .text: このディレクティブ以降は、テキストセグメントに配置されるプログラムの命令を記述します。
  • .globl label: 指定したラベル (label) をグローバルシンボルとして宣言します。これにより、他のファイルから参照可能になります。プログラムのエントリーポイント(通常は main)はグローバルにする必要があります。
  • label:: コロンで終わる識別子はラベルを定義します。これは特定のメモリアドレスや命令の位置を示す名前となります。ジャンプや分岐命令の飛び先として使われたり、データの先頭を示したりします。

データ定義ディレクティブ

.data セグメント内で使われる主なディレクティブです。

  • label: .word w1, w2, ...: ラベル label の位置から、指定された 32 ビット整数値 (w1, w2, …) を連続してメモリに配置します。
  • label: .half h1, h2, ...: 16 ビット整数値(ハーフワード)を配置します。
  • label: .byte b1, b2, ...: 8 ビット整数値(バイト)を配置します。
  • label: .asciiz "string": 指定された文字列をメモリに配置し、末尾にヌル文字 (ASCII コード 0) を自動的に追加します。C 言語スタイルの文字列です。
  • label: .ascii "string": .asciiz と似ていますが、末尾にヌル文字を追加しません。
  • label: .space n: ラベル label の位置から n バイト分の未初期化領域を確保します。
  • .align n: 次に配置されるデータのアドレスが 2n の倍数になるように調整します。例えば .align 2 はワード境界 (4 バイト) に合わせます。

例1: Hello, World!

古典的な “Hello, World!” プログラムです。文字列をコンソールに出力します。これにはシステムコール (syscall) を利用します(詳細は次のセクションで)。

# Hello World プログラム (hello.s)

.data       # データセグメントの開始
greeting: .asciiz "Hello, World!\n"  # 出力するヌル終端文字列

.text       # テキストセグメントの開始
.globl main # main ラベルをグローバルにする

main:       # プログラムの開始地点を示すラベル

    # 文字列を出力するためのシステムコールを準備
    li $v0, 4           # システムコールコード 4 (print_string) を $v0 にロード
    la $a0, greeting    # 出力する文字列のアドレスを $a0 にロード

    # システムコールを実行
    syscall             # OS (シミュレータ) に処理を依頼

    # プログラムを終了するためのシステムコールを準備
    li $v0, 10          # システムコールコード 10 (exit) を $v0 にロード

    # システムコールを実行
    syscall             # プログラムを終了

このプログラムでは、まず .data セグメントで greeting というラベルに “Hello, World!\n” という文字列(末尾にヌル文字が付加される)を定義します。 次に .text セグメントで main ラベルからプログラムが始まります。
文字列を出力するために、システムコールコード 4 を $v0 レジスタに、出力したい文字列のアドレス (greeting のアドレス) を $a0 レジスタにロードします。そして syscall 命令を実行すると、OS (またはシミュレータ) が $v0 の値を見て、対応する処理(この場合は文字列出力)を行います。
最後に、プログラムを正常に終了させるために、システムコールコード 10 を $v0 にロードして syscall を実行します。

例2: 二つの数の加算

メモリに格納された二つの数を読み込み、加算して結果をメモリに書き戻すプログラムです。

# 二つの数の加算 (add_nums.s)

.data       # データセグメント
num1:   .word 15        # 最初の数 (初期値 15)
num2:   .word 27        # 二番目の数 (初期値 27)
result: .space 4        # 結果を格納する 4 バイト (1 ワード) の領域を確保

.text       # テキストセグメント
.globl main

main:
    # num1 の値をレジスタ $t0 にロード
    lw $t0, num1        # lw $t0, 0($gp) のような形で展開されることも (アドレス指定による)

    # num2 の値をレジスタ $t1 にロード
    lw $t1, num2

    # $t0 と $t1 を加算し、結果を $t2 に格納
    addu $t2, $t0, $t1  # addu を使うとオーバーフローで例外が発生しない

    # 結果 ($t2) をメモリの result の位置にストア
    sw $t2, result

    # プログラム終了
    li $v0, 10          # exit システムコール
    syscall

この例では、.data セグメントで num1num2 に初期値を設定し、result 用の領域を確保しています。 main 関数内では、lw 命令で num1num2 の値をそれぞれ $t0$t1 レジスタにロードします。 次に addu 命令で $t0$t1 を加算し、結果を $t2 に格納します。 最後に sw 命令で $t2 の値をメモリ上の result のアドレスに書き込みます。そしてプログラムを終了します。

これらの例は非常に単純ですが、MIPS アセンブリプログラムの基本的な構造と、レジスタ、メモリ、命令、ディレクティブがどのように連携するかを示しています。

アセンブリプログラムから、OS (やシミュレータ) が提供する機能、例えばコンソールへの出力や入力、ファイルの操作などを行うには、システムコール (System Call) を使用します。MIPS では syscall という専用の命令が用意されています。

システムコールを使用する手順は以下の通りです。

  1. 実行したいシステムコールの種類を示すシステムコールコードをレジスタ $v0 に設定します。
  2. システムコールに必要な引数があれば、規約に従ってレジスタ ($a0, $a1, $a2, $a3 や浮動小数点レジスタ $f12 など) に設定します。
  3. syscall 命令を実行します。
  4. システムコールが値を返す場合、その結果は規約に従ってレジスタ ($v0 や浮動小数点レジスタ $f0 など) に格納されます。

利用できるシステムコールコードとその機能、引数、戻り値は、使用している OS やシミュレータ(SPIM や MARS など)によって異なります。以下は、教育用シミュレータである SPIM や MARS でよく使われる代表的なシステムコールコードの例です。

サービス名 コード ($v0) 引数 戻り値 説明
print_int 1 $a0 = 出力する整数 整数を10進数でコンソールに出力します。
print_float 2 $f12 = 出力する単精度浮動小数点数 単精度浮動小数点数を出力します。
print_double 3 $f12 = 出力する倍精度浮動小数点数 倍精度浮動小数点数を出力します。
print_string 4 $a0 = 出力するヌル終端文字列のアドレス メモリ上の文字列をコンソールに出力します。
read_int 5 $v0 = 読み込んだ整数 コンソールから整数を読み込みます。
read_float 6 $f0 = 読み込んだ単精度浮動小数点数 コンソールから単精度浮動小数点数を読み込みます。
read_double 7 $f0 = 読み込んだ倍精度浮動小数点数 コンソールから倍精度浮動小数点数を読み込みます。
read_string 8 $a0 = 入力バッファのアドレス, $a1 = バッファの最大長 (バイト数) コンソールから文字列を読み込み、指定されたバッファに格納します。改行文字も読み込み、ヌル文字で終端します。
sbrk 9 $a0 = 確保したいバイト数 $v0 = 確保されたメモリ領域の先頭アドレス ヒープ領域から指定されたバイト数のメモリを動的に確保します。C言語の malloc に相当します。
exit 10 プログラムの実行を正常に終了します。
print_char 11 $a0 = 出力する文字のASCIIコード 1文字をコンソールに出力します。
read_char 12 $v0 = 読み込んだ文字のASCIIコード コンソールから1文字を読み込みます。
open 13 $a0 = ファイル名の文字列アドレス, $a1 = フラグ, $a2 = モード $v0 = ファイルディスクリプタ ファイルを開きます。フラグ例: 0=読み取り専用, 1=書き込み専用(作成), 9=追記書き込み(作成)。
read 14 $a0 = ファイルディスクリプタ, $a1 = 読み込みバッファアドレス, $a2 = 最大読み込みバイト数 $v0 = 実際に読み込んだバイト数 or エラーコード 開いたファイルから読み込みます。
write 15 $a0 = ファイルディスクリプタ, $a1 = 書き込みバッファアドレス, $a2 = 最大書き込みバイト数 $v0 = 実際に書き込んだバイト数 or エラーコード 開いたファイルへ書き込みます。
close 16 $a0 = ファイルディスクリプタ 開いたファイルを閉じます。
exit2 17 $a0 = 終了コード プログラムを終了し、終了コードを返します。
print_int_hex 34 $a0 = 出力する整数 整数を16進数で出力します。
print_int_binary 35 $a0 = 出力する整数 整数を2進数で出力します。
print_int_unsigned 36 $a0 = 出力する整数 整数を符号なし10進数で出力します。

ヒント: 全てのシステムコールを覚える必要はありません。よく使うもの(出力、入力、終了)から覚え、必要に応じてリファレンスを参照しましょう。

システムコールは、アセンブリ言語で書かれたプログラムが外部の世界(コンソール、ファイルシステムなど)と対話するための重要なインターフェースです。

ツールとシミュレータ 🛠️

MIPS アセンブリプログラムを作成し、実行・デバッグするには、専用のツールが必要です。MIPS プロセッサが搭載された実機(古いゲーム機や組み込みボードなど)があればそこで動かすこともできますが、通常は PC 上で動作するシミュレータを使用するのが最も手軽で効率的です。

教育目的で広く使われている MIPS シミュレータとして、以下の二つが有名です。

MARS (MIPS Assembler and Runtime Simulator)

MARS は Java で書かれた、グラフィカルなインターフェースを持つ MIPS シミュレータです。

  • 特徴:
    • GUI ベースで直感的に操作しやすい。
    • エディタ、アセンブラ、シミュレータ、デバッガが統合されている。
    • レジスタやメモリの内容を視覚的に確認できる。
    • ステップ実行、ブレークポイント設定などのデバッグ機能が充実している。
    • システムコールも豊富にサポートされている。
    • Java が動作する環境(Windows, macOS, Linux)であれば利用可能。
  • 入手方法: Missouri State University の Kenneth Vollmar 教授のウェブサイトなどからダウンロードできます。(”MARS MIPS simulator” で検索してみてください)
  • 簡単な使い方:
    1. MARS を起動します (java -jar Mars4_5.jar など)。
    2. [File] -> [New] で新しいファイルを作成し、MIPS アセンブリコードを記述します。
    3. [File] -> [Save] で .asm または .s 拡張子で保存します。
    4. [Run] -> [Assemble] (または F3 キー) を押してアセンブルします。エラーがなければ実行可能な状態になります。
    5. [Run] -> [Go] (または F5 キー) を押してプログラムを実行します。
    6. ステップ実行するには、[Run] -> [Step] (または F7 キー) を押します。
    7. 実行結果やレジスタ、メモリの状態は各ウィンドウで確認できます。

SPIM / QtSpim

SPIM も古くから使われている MIPS シミュレータです。テキストベースの spim と、Qt フレームワークを使った GUI 版の QtSpim があります。

  • 特徴:
    • シンプルで軽量。
    • コマンドライン (spim) または GUI (QtSpim) で利用可能。
    • 基本的なデバッグ機能(ステップ実行、レジスタ/メモリ表示など)を持つ。
    • 多くの OS (Windows, macOS, Linux) で利用可能。
    • 教科書などでよく参照される古典的なシミュレータ。
  • 入手方法: SourceForge などで “spim simulator” や “qtspim” で検索して探すことができます。
  • 簡単な使い方 (QtSpim):
    1. QtSpim を起動します。
    2. [File] -> [Reinitialize and Load File] を選択し、作成したアセンブリファイル (.s または .asm) を読み込みます。
    3. [Simulator] -> [Run/Continue] (または F5 キー) でプログラムを実行します。
    4. ステップ実行するには [Simulator] -> [Single Step] (または F10 キー) を押します。
    5. [Window] メニューからレジスタやデータセグメントなどを表示するウィンドウを開けます。コンソール出力は [Console] ウィンドウに表示されます。
  • 簡単な使い方 (spim – コマンドライン):
    1. ターミナルを開き、spim コマンドを実行します。
    2. (spim) というプロンプトが表示されます。
    3. load "your_program.s" と入力してプログラムを読み込みます。
    4. run と入力してプログラムを実行します。
    5. step でステップ実行、print $t0 などでレジスタ内容表示、exit で終了します。? でコマンド一覧を表示できます。

どちらのシミュレータも MIPS アセンブリを学ぶ上で非常に役立ちます。GUI の使いやすさや統合環境を重視するなら MARS、シンプルさや CUI での操作を好むなら SPIM/QtSpim を選ぶと良いでしょう。まずはどちらか一方をインストールして、これまでの例題を実際に動かしてみることをお勧めします。実際に手を動かすことで、理解が格段に深まります! 💪

まとめと次のステップ 🏁

この入門記事では、MIPS アセンブリ言語の基本的な概念について解説しました。振り返ってみましょう。

  • MIPS アーキテクチャ: RISC ベースのシンプルな命令セットを持つアーキテクチャであり、教育用によく使われること。
  • 基本要素: 32 本の汎用レジスタとその規約的な用途、バイトアドレッシングのメモリ、R/I/J の 3 つの命令フォーマット。
  • 基本命令: ロード/ストア (lw, sw)、算術演算 (add, sub, addi)、論理演算 (and, or)、制御フロー (j, beq, jal, jr) など。
  • プログラム構造: .data, .text ディレクティブ、ラベル、データ定義 (.word, .asciiz, .space)。
  • システムコール: syscall 命令を使って OS/シミュレータの機能(入出力など)を呼び出す方法。
  • ツール: MARS や SPIM/QtSpim といったシミュレータの紹介。

MIPS アセンブリは、一見すると複雑で難しそうに感じるかもしれません。しかし、一つ一つの命令の機能は単純であり、レジスタやメモリとのやり取りを丁寧に追っていくことで、プログラムがどのように動作するかを具体的に理解することができます。これは、コンピュータサイエンスの基礎を固める上で非常に貴重な経験となります。✨

次のステップとして何を学ぶべきか?

今回の入門で MIPS アセンブリに興味を持った方は、さらに以下のようなトピックを探求してみると良いでしょう。

  • 関数呼び出し規約: スタックフレームの構築、引数の渡し方(スタック経由も含む)、$s レジスタの保存/復帰、$fp$sp の使い方など、より複雑な関数呼び出しの実装。
  • 制御構造の実装: if-else 文、while ループ、for ループなどをアセンブリでどのように実装するか。
  • 配列とポインタ: 配列要素へのアクセスやポインタ演算をアセンブリレベルでどう扱うか。
  • 浮動小数点演算: MIPS の浮動小数点コプロセッサ (Coprocessor 1) とその命令セット (add.s, lwc1 など)。
  • 再帰: 再帰関数の実装(スタックの使い方が重要)。
  • 擬似命令: アセンブラが提供する便利な擬似命令とその展開後の実際の命令。
  • 他のアーキテクチャ: x86 や ARM といった他のアーキテクチャのアセンブリ言語と比較してみる。

MIPS アセンブリの世界は奥深く、探求すればするほどコンピュータの動作原理に対する理解が深まります。ぜひシミュレータを使って、様々なプログラムを書いて試してみてください。Happy Hacking! 🎉

参考情報

  • コンピュータの構成と設計 MIPS Edition 第6版 (上)(下) – David A. Patterson, John L. Hennessy 著: MIPS アーキテクチャを学ぶ上での定番教科書(通称パタヘネ本)。非常に詳細で網羅的です。(出版社等の情報は省略)
  • MARS (MIPS Assembler and Runtime Simulator) Home Page: http://courses.missouristate.edu/KenVollmar/mars/ – MARS シミュレータの公式サイト(アクセス可能かは確認が必要です)。
  • SPIM MIPS Simulator Home Page: (SPIM の公式配布元は変更されている可能性があります。”SPIM MIPS Simulator” で検索してください。)

コメント

タイトルとURLをコピーしました