7839

雑草魂エンジニアブログ

命令セットアーキテクチャ

現在、CSの勉強のために、コンピュータアーキテクチャ (電子情報通信レクチャーシリーズ)を読んでいる。

今回は、コンピュータの命令はどのように作られるのか、命令の作り方を確認していく。また、アセンブリ言語との対応関係も確認していく。

命令セット(MIPS

最も洗練された MIPS Technologies 社の命令セット(あるアーキテクチャによって理解されるコンピュータの命令の語彙)を紹介する。その他の命令セットには、ARMv7., Intel x86., ARMv8. などがある。そして、命令セットにはアセンブリ言語機械語マシン語)がある。

  • 人間が理解しやすい命令や構文規則を備えた言語:高水準言語(C言語など)
    ex) a = b + c
    コンパイラ
  • 機械語の命令と1対1の対応関係を持つ、人間が理解できる言語:低水準言語(アセンブリ言語
    ex) add a, b, c
    アセンブラ
  • 0と1で構成され、CPUが実行できる命令からなる言語:機械語マシン語
    ex) 000000 10001 10010 01000 00000 100000

MIPSは命令語を32ビット幅で統一しており、命令形式は以下の3パターンのみとしている。コンピュータでは命令を極力単純化することでデコードにかかる時間を減らし、処理を高速化させている。 

f:id:serip39:20220227173943j:plain

  • op(opcode):命令操作コード(オペコード)と呼ばれる。すなわち、オペコードが命令の操作を決定し、R, I, J Typeを区別する。そして、命令の操作対象をオペランドといい、rs, rt, rd, shamt, funct, address, immediateにはレジスタ番号、メモリのアドレス、分岐条件、分岐先のアドレスなどを指定する。

    type opcode 用途
    R 0(000000) 算術論理演算命令
    Registerの「R」
    I 4以上 即値(定数)演算およびメモリ操作(読み出し、書き込み)命令
    immediate operandの「I」
    J 2, 3(000010, 000011) 分岐命令
    Jumpの「J」
  • rs(register source):第一オペランドレジスタ
  • rt(register target):第二オペランドレジスタ
  • rd(register destination):結果の格納先レジスタ
  • shamt(shift amount):シフト量
  • funct(function code):機能コード(命令操作コードの詳細な機能を表す)
  • address:メモリのアドレス、分岐先のアドレス
  • immediate:即値(定数)

MIPSレジスタは、32個のレジスタで構成されており、番号で識別される。アセンブリ言語ではレジスタ名で識別することができる。

f:id:serip39:20220314001825j:plain

詳細は、MIPS Reference Sheetを参照してほしい。

例として、いくつかの命令語を生成する。

加算命令

add $t0, $s1, $s2  # $t0 <- $s1 + $s2
# Instruction
op(0)  rs($s1)  rt($s2)  rd($t0)  shamt(0)  funct(add)
000000 10001 10010 01000 00000 100000
  • addはR typeのopcodeのためopcodeは0。そして、functはadd=32(100000)である。
  • $s1のレジスタ番号は、17(10001)
  • $s2のレジスタ番号は、18(10010)
  • $t0のレジスタ番号は、8(01000)
  • addではシフト量がないため、0。

上記をR typeの型に従って当てはめることで命令語が完成する。

メモリからレジスタへのデータ読み込み命令

lw $t0, 4($s3)   # load word, R[$t0] <- Mem4B[(R[$s3] + 4)]
# R[N]は、汎用レジスタのNを参照する
# Mem4B(X)は、メモリのアドレスXから4Byteを参照する
# メモリの$s3+4アドレスの値をレジスタ$t0に読み込む(lwが4Byte参照するので、+Nの部分は4の倍数となる。)
# Instruction
op(lw)  rs($s3)  rt($t0)  address(4)
100011 10011 01000 00000 00000 000100
  • メモリ操作(データ転送)のため、I typeを使う。lw=35(100011)である。
  • lw命令においては、rtに、結果であるrdを入れる必要があるので注意。
  • $s3のレジスタ番号は、19(10011)
  • 4はaddressに入れる

上記を I typeの型に従って当てはめることで命令語が完成する。

レジスタからメモリへのデータ書き込み命令

sw $t0, 8($s3)   # store word, Mem4B[(R[$s3] + 8)] <- R[$t0]
# レジスタ$t0の値をメモリの$s3+4アドレスに書き込む(swが4Byte参照するので、+Nの部分は4の倍数となる。)
# Instruction
op(sw)  rs($s3)  rt($t0)  address(8)
101011 01000 10011 00000 00000 001000
  • メモリ操作(データ転送)のため、I typeを使う。sw=43(101011)である。
  • sw命令においては、rtに、結果であるrdを入れる必要があるので注意。
  • 8はaddressに入れる

上記を I typeの型に従って当てはめることで命令語が完成する。

無条件分岐命令

j 14   # PC ← {(PC + 4)[31:28], 14, 00}
# PC(プログラムカウンタ)に任意のアドレスを設定する
# Instruction
op(jump)  address(14)
000010 00000 00000 00000 00000 001110
  • 無条件分岐命令のため、J typeを使う。jump=2(000010)である。
  • 14はaddressに入れる

上記を J typeの型に従って当てはめることで命令語が完成する。

条件判定

コンピュータが単なる計算機である電卓とは異なる点の1つに条件判定の機能があることが挙げられる。プログラムでは、条件を表すのにif文を使用する。アセンブリ言語で条件判定の命令がどのように実装されるか確認しておく。

if-then-else

if (i == j) {   // $s3 == $s4
    f = g + h;  // $s0 = $s1 + $s2
} else {
    f = g - h;  // $s0 = $s1 - $s2
}

f=$s0, g=$s1, h=$s2, i=$s3, j=$s4とする。$s3と$s4が一致していれば、$s1 + $s2の加算を行う。$s3と$s4が一致していなければ、$s1 - $s2の減算を行う。MIPSコードで書くと、以下のようになる。

      bne $s3, $s4, Else
      add $s0, $s1, $s2
      j Exit
Else: sub $s0, $s1, $s2
Exit:

条件分岐if文では、beqbneを用いる。

  • beq $rs, $rt, imm // if(R[$rs] = R[$rt]) PC ← PC + 4 + imm
    • beq = branch if equal
  • bne $rs, $rt, imm // if(R[$rs] != R[$rt]) PC ← PC + 4 + imm
    • bne = branch if not equal

アセンブリ言語では、分岐先のアドレスにラベルを使用することで、実装しやすいようになっている。

while ループ

while (save[i] == k) {
    i += 1;
}

i=$s3, k=$s5, save[]の先頭アドレス$s6とする。MIPSコードで書くと、以下のようになる。

Loop: sll $t1, $s3, 2 // $t1 = i * 4
      add $t1, $t1, $s6 // $t1 = &save[0] + $t1
      lw $t0, 0($t1) // $t0 = save[i]
      bne $t0, $s5, Exit // if (save[i] != k) goto Exit
      addi $s3, $s3, 1 // i += 1
      j Loop // goto Loop
Exit:
  1. i * 4を一時レジスタ$t1に代入する。Nbit左シフトは2N倍することと同じである。命令長が32bit=4Byteなので、iを配列の先頭アドレスに変換するために4倍する必要がある。
    • i=0の時、配列の先頭アドレスは0。
    • i=1の時、配列の先頭アドレスは4。
    • i=2の時、配列の先頭アドレスは8。
  2. save[]配列のベースアドレスを$t1に足す。これで$t1に取り出したいsave[i]の先頭アドレスが格納される。
  3. save[i] != kであればExitに遷移して、while内の処理は実行しない。
  4. i += 1でiをインクリメントする
  5. while内の処理が終わったので、whileの判定の分岐に戻る。

for ループ

result = 1;
for (i=1; i < 10; i++) {
    result += i;
}

result=$s0, i=$s1とする。MIPSコードで書くと、以下のようになる。

      addi $s0, $zero, 1 // result = 0 + 1
      addi $s1, $zero, 1 // i = 0 + 1
For: slti $t0, $s1, 10 // $t0 <- i < 10
      beq $t0, $zero, Exit // if ($t0 == 0) goto Exit
      add $s0, $s0, $s1 // result += i
      addi $s1, $s1, 1 // i++
      j For // goto For
Exit:

for文の条件部分においては、slt(set on less than)を用いる。

  • slt $t0,$s1, $s2($s1<$s2であれば、$t0に1を設定する)
  • slti $t0, $s1, 10($s1<10であれば、$t0に1を設定する)

まとめ

今回、初めてアセンブリ言語を少し触ってみることができた。C言語などで実装したプログラムもコンパイルアセンブリ言語に自動的に変換される、コンパイラの偉大さが少し垣間見えたような気がした。

関連書籍