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に値をロード
詳細な説明
la (load address): 配列のベースアドレスをレジスタ `$s0` にロードします。
li (load immediate): 即値をレジスタにロードします。ここでは、値5とインデックス2をそれぞれ `$t0` と `$t1` にロードしています。
sll (shift left logical): インデックスをバイトオフセットに変換します。MIPSでは、整数が4バイトなので、インデックスを2ビット左シフトして4倍します。
add: ベースアドレスにバイトオフセットを加算して、配列要素のアドレスを計算します。
sw (store word): レジスタの値を計算したメモリアドレスにストアします。
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に格納
詳細な説明
la (load address): `a` のメモリアドレスをレジスタ `$s0` にロードします。
move: レジスタ `$s0` の値(`a` のアドレス)をレジスタ `$t0` にコピーします。これで `$t0` はポインタ `p` として機能します。
li (load immediate): 値20をレジスタ `$t1` にロードします。
sw (store word): レジスタ `$t1` の値をポインタ `$t0` が指すアドレスにストアします。これにより、変数 `a` の値が20に更新されます。
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 # システムコールを実行
詳細な説明
データセクションの定義:
`.data` セクションで、出力する文字列を定義します。`hello_str` ラベルには、ヌル終端の文字列 "Hello, World!\n" が格納されます。
文字列出力の実装:
`$v0` レジスタにシステムコール番号 4 をロードします。これは `print string` システムコールを示します。
`$a0` レジスタに出力する文字列のアドレスをロードします。これは `.data` セクションで定義した `hello_str` のアドレスです。
`syscall` 命令を実行して、システムコールを発動し、文字列を標準出力に出力します。
プログラムの終了:
`$v0` レジスタにシステムコール番号 10 をロードします。これは `exit` システムコールを示します。
`syscall` 命令を実行して、プログラムを終了します。
注意点
この例は、固定の文字列を出力する単純なケースです。`printf` の全機能(フォーマット指定子の解釈、変数引数リストの処理など)をMIPSアセンブリで再現するには、非常に多くのコードと複雑なロジックが必要です。
実際には、`printf` の完全な実装は標準Cライブラリ(例えば `glibc`)によって提供されており、アセンブリレベルでの実装は避けるべきです。
このように、MIPSアセンブリで簡単な文字列出力は可能ですが、`printf` のような高度な関数を実装するのは現実的ではありません。
この記事が気に入ったらサポートをしてみませんか?