MIPS: 高級言語とアセンブリ言語と機械語の境界。

参考図書
ディジタル回路設計とコンピュータアーキテクチャ 第2版 単行本(ソフトカバー) – 2017/9/11
Sarah L. Harris (著), David Money Harris (著), 天野 英晴 (翻訳), 中條 拓伯 (翻訳), 鈴木 貢 (翻訳), 永松 礼夫 (翻訳)

p291くらいから


即値

アセンブリ言語や機械語のコード中に直に書き込まれた値。
変数にアクセスしないもの。メモリはおろかレジスタにすらアクセスしないもの。

高水準言語の場合

a = a + 4

ならば4の部分。

MIPSアーキテクチャにおけるアセンブリ

addi $s0, $s0, 4

ならば4の部分。

addiは即値を含む加算命令。
$s0はレジスタ。


機械語

RISCだと命令は100個くらいだが、CISCだと1000個くらいある。
MIPSはRISC。

MIPS 32bitアーキテクチャの場合

命令は32bitであり、
32bitの中に必要な情報を全部詰め込む。

異なる形式が3つある。

R形式命令

レジスタ形式。

op,rs,rt,rd,shamt,funct
の6つのフィールドからなり、各々で32ビットを分割する。

op:6bit,
オペコード。R形式なら6bitが全て0。opが0でないならI形式かJ形式。
R形式の場合、具体的な命令種はfunctに入る。

rs:5bit,
register source
レジスタソース
rt:5bit,
register target
レジスタターゲット
rd:5bit
register distination
レジスタディスティネーション
R形式における計算したデータの格納先。

add $s0, $s1, $s2

の場合、レジスタ$s0(distination)に$s1(source)と$s2(target)の値を足して格納すると読む。

アセンブリ言語上ではデータ格納先のdistinationが先頭だが、
機械語に変換されるとrs,rtに次いだ3番目になる。

shamt:5bit
shift amount
シフト演算で使用

funct:6bit
R形式における具体的な命令。
add命令なら十進数32、二進数の100000
sub命令なら十進数34、二進数の100010

R形式命令セット

add: 整数加算
addu: 符号なし整数加算
sub: 整数減算
subu: 符号なし整数減算
and: ビットごとの論理積
or: ビットごとの論理和
xor: ビットごとの排他的論理和
nor: ビットごとの否定論理和
slt: 整数の小なり比較
sltu: 符号なし整数の小なり比較
sll: 左論理シフト
srl: 右論理シフト
sra: 右算術シフト
jr: ジャンプレジスタ

I形式命令

即値(immediate)形式

op,rs,rt,imm
の4つのフィールドからなり、

op:6bit
I形式はここに命令種が入る。例えばaddi命令なら十進数8、二進数で001000
他にlw,sw命令などがある。
rs
:5bit
rt
:5bit
I形式における計算したデータの格納先。
imm
:16bit
この値はそのまま計算に使用される。

addi $s0, $s1, 5

ならば$s0(target)に$s1(source)と5(即値)を足した値を格納する。

アセンブリ言語上ではデータ格納先のtargetが先頭だが、
機械語に変換されるとrsに次いだ2番目になる。

I形式命令セット

addi: 即値加算
addiu: 符号なし即値加算
andi: 即値論理積
ori: 即値論理和
xori: 即値排他的論理和
slti: 即値小なり比較
sltiu: 符号なし即値小なり比較
lw: メモリからレジスタへのロード
sw: レジスタからメモリへのストア
lb: バイト単位のロード
sb: バイト単位のストア
lbu: 符号なしバイト単位のロード
lh: ハーフワード単位のロード
sh: ハーフワード単位のストア
lhu: 符号なしハーフワード単位のロード
beq: 等しい場合の分岐
bne: 等しくない場合の分岐
lui: 上位即値ロード

J形式命令

ジャンプ形式。

op,addr
の2つのフィールドからなり、

op:6bit
beq(branch if equal)
イコールならaddrにジャンプする。
十進数なら4、二進数なら000100
bne(branch if not equal)
十進数なら5、二進数なら000101

他にj,jal,jr命令など

addr:26bit
アドレス。

j形式命令セット

j: 無条件ジャンプ
jal: ジャンプアンドリンク


分岐



条件付き分岐

beq(branch if equal)
イコールならラベルに飛ぶ
bne(branch if not equal)
イコールじゃないならラベルに飛ぶ

ジャンプ

無条件分岐、あるいはジャンプ。

j(jump)
ラベルにジャンプ
jal(jump and link)
jr(jump register)
jalでジャンプし、jrで元の場所に戻る。
jalはジャンプ地点のアドレスを退避させ、jrで戻ってこれるようにするもの。逆に言うとj命令は飛びっぱなしで戻ってくることを考えない。


if文

高級言語のif文はアセンブリではbne命令で実現される。

if(a==b)
{
  //ifブロック
}
//if以降

ifブロックは条件を満たしていればそのまま実行されるが、
満たしていないなら飛ばされる。

  bne a, b, label
  //ifブロック
label:
  //if以降

ただしここでのaだのbだのは本来レジスタ。$s0, $s1など。

また、

if(a!=b)ならばbneでなくbeqが対応する。

  beq a, b, label
  //ifブロック
label:
  //if以降

条件文に応じてbneかbeqかが変わる。
条件文 a==bにはbne, a!=bにはbeqが対応する。

if-else文

if-elseはbne命令とj命令で実現される。
つまりラベルを2つ使う

if(a==b)
{
  //ifブロック
}
else
{
  //elseブロック
}
//if-else以降
  bne a, b, L1
  //ifブロック
  j L2
L1:
  //elseブロック
L2:
  //if-else以降

やはりifの条件文に応じてbneとbeqが変わる。
if(a!=b)ならばbeqである。

while文

while(a==b)
{
  //whileブロック
}
while:  
  bne a, b, done
  //whileブロック
  j while
done:

やはりwhileの条件文に応じてbneとbeqが変わる。
while(a!=b)ならばbeqである。

for文

whileに対し、ループ変数が追加で考慮される。

for(int i = start; i!=end; i++)
{
  //forブロック
}
for:
  beq i, end, done
  //forブロック
  addi i, i, 1
  j for
done:

ただしここでiだのendだのは適切に初期化されたレジスタ。
また、startなどは即値である場合もある。
すなわち、for文は初期化、条件文、更新部分に応じて各々展開される。

for(初期化, 条件文, 更新部分)
//初期化
for:
  //条件文
  //forブロック
  //更新部分
  j for
done:

配列

MIPSアーキテクチャで配列を実現するには、メモリを使用して配列の要素を格納し、ロード(読み取り)およびストア(書き込み)命令を使用してアクセスします。以下に、C言語での配列操作をMIPSアセンブリに変換する例を示します。

C言語の配列操作

int arr[10];
arr[2] = 5;
int x = arr[2];

MIPSアセンブリでの実装

MIPSアセンブリでは、配列の各要素はメモリ上に連続して配置されます。例えば、`arr` がベースアドレスを持つと仮定します。ここでは、配列のベースアドレスを `$s0` レジスタに格納し、配列のインデックスと要素にアクセスします。

初期設定

配列のベースアドレスを設定するためのメモリ確保(シミュレーション)

    .data
arr: .space 40  # 10個の整数(4バイト×10 = 40バイト)を格納するためのメモリ空間

    .text
    .globl main
main:
    la $s0, arr  # 配列のベースアドレスを$s0にロード

配列の要素に値を代入

`arr[2] = 5` に相当するアセンブリコード

    li $t0, 5          # $t0に値5をロード
    li $t1, 2          # $t1にインデックス2をロード
    sll $t1, $t1, 2    # インデックスをバイトオフセットに変換(2 * 4 = 8バイト)
    add $t1, $t1, $s0  # ベースアドレスにオフセットを加算
    sw $t0, 0($t1)     # $t0の値(5)をメモリにストア

配列の要素を読み取る

`int x = arr[2];` に相当するアセンブリコード

    li $t1, 2          # $t1にインデックス2をロード
    sll $t1, $t1, 2    # インデックスをバイトオフセットに変換(2 * 4 = 8バイト)
    add $t1, $t1, $s0  # ベースアドレスにオフセットを加算
    lw $t2, 0($t1)     # メモリから$t2に値をロード

詳細な説明

  1. la (load address): 配列のベースアドレスをレジスタ `$s0` にロードします。

  2. li (load immediate): 即値をレジスタにロードします。ここでは、値5とインデックス2をそれぞれ `$t0` と `$t1` にロードしています。

  3. sll (shift left logical): インデックスをバイトオフセットに変換します。MIPSでは、整数が4バイトなので、インデックスを2ビット左シフトして4倍します。

  4. add: ベースアドレスにバイトオフセットを加算して、配列要素のアドレスを計算します。

  5. sw (store word): レジスタの値を計算したメモリアドレスにストアします。

  6. lw (load word): 計算したメモリアドレスからレジスタに値をロードします。

このようにして、MIPSアセンブリで配列を操作することができます。配列の各要素にアクセスする際は、インデックスをバイトオフセットに変換し、ベースアドレスに加算して要素のアドレスを計算します。

ポインタ

MIPSアーキテクチャでポインタを実現するには、レジスタを使用してメモリ内のアドレスを格納し、ポインタを通じてメモリ操作を行います。以下に、C言語でのポインタ操作をMIPSアセンブリに変換する例を示します。

C言語のポインタ操作

int a = 10;
int *p;
p = &a;
*p = 20;
int b = *p;

MIPSアセンブリでの実装

初期設定

まず、C言語の変数とポインタをMIPSレジスタにマッピングします。例えば:

  • `a` をメモリに格納し、そのアドレスをレジスタ `$s0` に格納

  • `p` をレジスタ `$t0` に格納

メモリ確保と初期設定

    .data
a:  .word 10  # 変数aの初期値は10

    .text
    .globl main
main:
    la $s0, a  # 変数aのアドレスを$s0にロード

ポインタの初期化と操作

C言語でのポインタ操作を以下のようにMIPSアセンブリで実装します。

    # ポインタpを初期化
    move $t0, $s0  # p = &a; ポインタpにaのアドレスを設定

    # ポインタpを通じて値を変更
    li $t1, 20     # $t1に値20をロード
    sw $t1, 0($t0) # *p = 20; ポインタpが指すアドレスに値20をストア

    # ポインタpを通じて値を読み取る
    lw $t2, 0($t0) # b = *p; ポインタpが指すアドレスから値をロードして$t2に格納

詳細な説明

  1. la (load address): `a` のメモリアドレスをレジスタ `$s0` にロードします。

  2. move: レジスタ `$s0` の値(`a` のアドレス)をレジスタ `$t0` にコピーします。これで `$t0` はポインタ `p` として機能します。

  3. li (load immediate): 値20をレジスタ `$t1` にロードします。

  4. sw (store word): レジスタ `$t1` の値をポインタ `$t0` が指すアドレスにストアします。これにより、変数 `a` の値が20に更新されます。

  5. lw (load word): ポインタ `$t0` が指すアドレスから値をロードし、レジスタ `$t2` に格納します。これで変数 `b` に `*p` の値が代入されます。

完全な例

以下に、上記のすべてのステップを組み合わせた完全なMIPSアセンブリプログラムを示します。

    .data
a:  .word 10  # 変数aの初期値は10

    .text
    .globl main
main:
    la $s0, a  # 変数aのアドレスを$s0にロード

    # ポインタpを初期化
    move $t0, $s0  # p = &a; ポインタpにaのアドレスを設定

    # ポインタpを通じて値を変更
    li $t1, 20     # $t1に値20をロード
    sw $t1, 0($t0) # *p = 20; ポインタpが指すアドレスに値20をストア

    # ポインタpを通じて値を読み取る
    lw $t2, 0($t0) # b = *p; ポインタpが指すアドレスから値をロードして$t2に格納

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

この例では、ポインタを使用してメモリ操作を行う方法を示しました。ポインタはレジスタに格納されたメモリアドレスであり、これを通じてメモリの読み書きを行います。

printf

システムコールを呼ぶらしいので、結局OSにやらす。

`printf` 関数をMIPS命令セットで実現することは、非常に複雑です。`printf` は標準Cライブラリの一部であり、様々なフォーマット指定子を解釈して出力を整形する高度な関数です。これをMIPSアセンブリで実装することは、低レベルのメモリ操作、文字列操作、システムコールを駆使する必要があります。

ただし、簡単な例として、標準出力に固定の文字列を出力する方法を以下に示します。これはMIPSアーキテクチャでのシステムコールを使用した基本的な文字列出力です。

MIPSアセンブリでの基本的な文字列出力

以下の例では、標準出力に "Hello, World!" を出力します。MIPSシステムコールを使用して実現します。

.data
hello_str: .asciiz "Hello, World!\n"  # 出力する文字列

.text
.globl main
main:
    # $v0 = 4 (print string syscall)
    li $v0, 4             # システムコール番号 4 を $v0 にロード
    la $a0, hello_str     # 出力する文字列のアドレスを $a0 にロード
    syscall               # システムコールを実行

    # プログラム終了
    li $v0, 10            # システムコール番号 10 (exit) を $v0 にロード
    syscall               # システムコールを実行

詳細な説明

  1. データセクションの定義:

    • `.data` セクションで、出力する文字列を定義します。`hello_str` ラベルには、ヌル終端の文字列 "Hello, World!\n" が格納されます。

  2. 文字列出力の実装:

    • `$v0` レジスタにシステムコール番号 4 をロードします。これは `print string` システムコールを示します。

    • `$a0` レジスタに出力する文字列のアドレスをロードします。これは `.data` セクションで定義した `hello_str` のアドレスです。

    • `syscall` 命令を実行して、システムコールを発動し、文字列を標準出力に出力します。

  3. プログラムの終了:

    • `$v0` レジスタにシステムコール番号 10 をロードします。これは `exit` システムコールを示します。

    • `syscall` 命令を実行して、プログラムを終了します。

注意点

  • この例は、固定の文字列を出力する単純なケースです。`printf` の全機能(フォーマット指定子の解釈、変数引数リストの処理など)をMIPSアセンブリで再現するには、非常に多くのコードと複雑なロジックが必要です。

  • 実際には、`printf` の完全な実装は標準Cライブラリ(例えば `glibc`)によって提供されており、アセンブリレベルでの実装は避けるべきです。

このように、MIPSアセンブリで簡単な文字列出力は可能ですが、`printf` のような高度な関数を実装するのは現実的ではありません。

この記事が気に入ったらサポートをしてみませんか?