🎄

易しいアセンブリ

2021/12/25に公開

易しいアセンブリ

この記事では自作アプリ(CASL2エディタとCOMET2エミュレータ)を用いて,実際にアセンブリ言語の雰囲気を紹介します.
後半では自作アプリの実装周りの紹介をします.最後まで読みましょう.

CASL2とCOMET2について

CASL2とは,IPA[1]が試験用に作成した仮想のアセンブリ言語です.その仮想言語に対応する仮想コンピュータをCOMET2と言います.
COMET2は16ビットマシンで命令数も30程度と少ないです.実用には向いていませんがビット数・命令数ともに少ないため学習するのに向いています.
また,仕様[2]も8ページと少ないため,軽い気持ちで学習を始めても怪我をする心配がありません.

自作アプリ

CASL2のシミュレータと呼ばれるものは公式や私のような他者含め多くの人が出していますが,今回は私が作成したアプリを利用していきます.

ここでは「CASL2シミュレータ」ではなく「CASL2エディタとCOMET2エミュレータ」と呼んでいますが,特に深い意味はありません.この自作アプリはウェブアプリとなっており,ブラウザ上で完結しています.特徴はシンタックスハイライトをサポートしているエディタと,レジスタの値をリアルタイムに反映する表示部分です.

レジスタの値がリアルタイムに反映される例

結果を表示するため,結果だけを表示する実行環境と比べて実行速度面では大きく劣りますが,内部で何を行っているのかが視覚的に分かるため,学習を目的としているのであれば大いに役立つと思います.
また限定的ではありますが,一部のシンタックスエラーにも対応しています.

自作アプリの制約事項

  1. プログラムはメモリアドレス100にロードされます.
  2. 現時点では,プログラムの終了条件をスタックポインタが0になることを条件にしています.
    これは,スタックポインタが0xffffから始まることと,RET命令時にスタックポインタが繰り上がることを利用しています.

CASL2

CASL2の基本文法は次のようになります([...]は省略可能).

[<ラベル>]<空白><オペコード><空白>[<オペランド>]

これらの命令(オペコード)は1語[3],もしくは2語のCASL2の機械語に1対1に対応します(CASL2専用の命令やマクロと呼ばれるものなど,機械語の命令に1対1で対応しない命令も存在します).
幾つかのサンプルコードを用意したので実際に遊んでみましょう.ビットが変化するのを眺めるのは楽しいですよ. 全ての命令を知りたい場合は仕様を読みましょう.

c = a + b

それぞれのリンクからコードの実行ができます

https://comet.askua.dev/#/6d59eabe0e6cd1ab1bfc8ac1fd420c62
https://dartpad.dev/6d59eabe0e6cd1ab1bfc8ac1fd420c62

GR1a, GR2b, GR0cに見立てています.CASL2には c = a + bを1行で表現できる命令がないため,c = ac += bの2つに分けて実行しています.

ループ

https://comet.askua.dev/#/d92f82940f8519958f6b0141ffd1105f
https://dartpad.dev/d92f82940f8519958f6b0141ffd1105f

簡単なループ処理であっても,アセンブリ言語だと少し面倒になります.
0初期化は LAD GR0,0などでも良かったのですが,ここでは XOR GR0,GR0を利用しています.「どっちが良いの?」という問いには答え難いのですが,このアプリにおいてはXOR r1,r2は1語命令,LAD r,adr,xは2語命令なのでここではXORの方がメモリ効率が良いと言えます.CPL GR2,GR1JMI ENDでループの終了条件を表しており,GR2がGR1より小さくなったら,ENDへJUMPしています.

再帰

https://comet.askua.dev/#/c3abae54bc7479b27b8d49ef8956c408
https://dartpad.dev/c3abae54bc7479b27b8d49ef8956c408

先程のループを再帰にしてみました.PUSHPOPを書かずにGR0にどんどん加算して行っても良いのですが,動かした時の挙動が面白いのでこのように書いています.このプログラムでは1から100まで再帰的に数え上げた後,100から順に加算していきます.

\sum_{i=1}^{n=100}i = 1+(2+(3...+(98+(99+100))...))

また,CALL命令はスタックにポインタを積んでいくため,先程のループと比べてSRが絶えず変化していくところも面白いポイントです(CALLの前にPUSH命令も実行しているため,SRは単にCALLを読んだだけの場合の倍積まれています).

スタックの中身のイメージ
...
ポインタ
ポインタ
...

print(str)

https://comet.askua.dev/#/bad13ae8247ee1dec05bcff6bf675a2d
https://dartpad.dev/bad13ae8247ee1dec05bcff6bf675a2d

文字列の出力は比較的簡単です.OUT命令(正確にはマクロ[4])のオペランドにメモリアドレスと文字数を受け取り,その文字数分だけ表示します.
実行すると,GR1GR2に値が入っていると思います.これはマクロであるOUT命令が展開されて別の命令になっているためです(COMET2にはOUTという命令は実際にはありません).OUT STR,13はコンパイル時に次のように展開されます.

        LAD     GR1,STR
        LAD     GR2,13
        SVC     2

print(n)

https://comet.askua.dev/#/8b789f6720356d26846cb1a8cca23d22
https://dartpad.dev/8b789f6720356d26846cb1a8cca23d22

文字列の出力は簡単ですが,数値の出力は簡単にはいきません.数値の出力を行うには,数値を数字に変換する必要があるためです.

https://dartpad.dev/39b2e1d86701db7b238111a2589dabdd

ここで問題が発生します.CASL2には乗算と除算に該当する命令がありません.なので,それらの命令を作成する必要があります.必要な関数は商と剰余を求める関数です.

CASL2 and Dart code

https://comet.askua.dev/#/79e9ff38591a9b0186fcd320a07574cc
https://dartpad.dev/79e9ff38591a9b0186fcd320a07574cc

dartの実装をほぼそのまま移しました.1-100の総和にすると,この実装ではそれなりに時間がかかります.なのでここでは1-10の総和を求め,出力しています.パフォーマンスどうなの?という実装になってはいますが,ここでは見た目の面白さを優先しています.意欲に満ち溢れているあなたはパフォーマンス改善に取り組みましょう.スタックが積まれていくのは面白いです.シンプルなコードであってもアセンブリで書くとそれなりの分量になります.

逆ポーランド記法電卓

CASL2 and Dart code

https://comet.askua.dev/#/5fbaeb29e4f124ceb6928157022f9d86
https://dartpad.dev/5fbaeb29e4f124ceb6928157022f9d86

逆ポーランド記法というものがあります.サンプルコードとして有名なので取り上げます.(Dartのコードを書いてからCASL2のコードを書いているのですが,それでもデバッグ機能の無さをを痛感しました.)
こちらは少し実行に時間がかかりますが,実行すると次のような結果を得られます(このサイズのコードは良いデバッグになりました).

自作アプリの実装周り

ここまで遊んできたアプリはCASL2・COMET2のコア実装に関する部分はDartのライブラリとして実装しており,そのライブラリを利用してFlutterで画面実装をしたものになります.

https://github.com/a-skua/tiamat

このライブラリは次のような方法で利用することができます.

import 'package:tiamat/tiamat.dart';

const src = '''
MAIN    START
        LAD     GR1,1
        RET
        END
''';

void main() async {
  final casl2 = Casl2.fromString(src);

  final result = casl2.compile();
  if (result.hasError) {
    print(result.errors);
    return;
  }

  final comet2 = Comet2();
  final resource = await comet2.loadAndRun(result);

  print(resource);
  // Output:
  //    GR0: 0, GR1: 1, GR2: 0, GR3: 0, GR4: 0, GR5: 0, GR6: 0, GR7: 0, SP: 0, PR: 0, FR: 0
}

ね,簡単でしょ?

なぜライブラリなのか

「自作OS入門[5]」という本を完走し,「自作x86エミュレータ本[6]」をRustでやり終え,「そろそろ本の内容を写すだけじゃなくて何か作りたい」と2020年の末に思い至り作り始めたのがこのライブラリになります.「仕様簡単だし比較的簡単にエミュレータ作れるのでは??」という安易な発想からCASL2に目をつけました.

この時の要件は次のようなものです.

  1. CASL2の入力に対して,実行結果を得られること.
  2. 実行環境として最低限WebとCLIを満たすこと.

WebとCLIを実行環境に選んだ時点で,自分の中では実装言語がTypeScriptかDartに絞られました.Dartを採用した大きな理由としてFlutterの存在があります.これは元々,iOSやAndroidのクロスプラットフォームアプリ開発用のフレームワークですが,将来的にDesktopアプリ(Linuxを含む)やWebブラウザもサポートするという話が出ていました(この時点でFlutter2は世に出ていません).そしてDart本家が「Sound Null Safety!」と高らかにNULL安全を言語に導入すると発表していたのも「Dartのベータ版試してみたい」という気持ちを後押ししたため,結果的にTypeScriptではなくDartを採用しています.
余談ですが,JSへのトランスパイル路線を捨ててWASMを利用するのであれば言語の選択肢はさらに広がります.

今回の自作アプリはFlutterを用いて実装していますが,DartをWebで実行する(JSへトランスパイルする)のにFlutterは必須ではないため,ライブラリのサンプルアプリはDartのみで実装されています.

https://a-skua.github.io/tiamat/

実行環境としてWebとCLIの両方をサポートする以上,ブラウザとCLIのIOに関わる部分(環境依存)を1つのコードで両方実装するというのは現実的ではありません.良い設計というものはそういった依存部分からコアロジックを切り離して行うものです.
であればCASL2のコンパイラやCOMET2のエミュレータを担うコアロジックをライブラリとして公開し,ライブラリを利用するアプリを作るのが良いのではないかということで,ライブラリとして実装しています.
そして公開されたこのライブラリは特定環境の依存を持たないため,Dart|Flutterがサポートする全ての環境で利用することができます.

https://pub.dev/packages/tiamat

ライブラリの機能

自分がCASL2シミュレータと呼んでいない理由として,このライブラリがCASL2をパースして機械語に変換するコンパイラを担う部分と,機械語をロードしてCOMET2の動作を真似るエミュレータを担う部分の2つから構成されているからです.
なので,安易にこのライブラリをCASL2シミュレータと呼ぶのは違う気がしています.

ライブラリの初版を作成したのは1月ごろです.機械語の意味を解釈して実行するというのは,既に「自作エミュレータ本」をやっていた為問題にはなりませんでしたが,CASL2を機械語に翻訳するのはそれとは訳が違うということに作り始めてから気が付きました.この時参考にしたのは「WEB+DB PRESS Vol.120 自作OSx自作ブラウザで学ぶWebページが表示されるまで[7]」という雑誌の特集です.特集のHTMLをパースする実装を参考に初版のライブラリは完成しました.

https://github.com/hikalium/liumos/tree/main/app/browser

初版のライブラリがやっていることを図式化すると次のようになります.

初版のライブラリでは,構文エラーを表示するための仕組みなどはありませんでしたが,当初の要件を満たすことができたため,ひとまず開発は終了しました.

ライブラリの育て方

6月ごろに「Goで作るインタプリタ[8]」という本に取り組んだことによりライブラリの反省点に対する修正目処が立ったため,ライブラリの再開発を始めました.

要件です.

  1. 可能なら構文エラーを表示すること.
  2. シンタックスハイライトを実装する手段の提供
  3. 途中経過を表示する手段の提供

この用件のもと出来上がったライブラリはこのようになっています.

図にすると分岐が増えた以外はAst変換が加わっただけですが,Token変換に関しては全て書き直しています.初版では1文字1文字が全てトークンでしたが,それを複数文字単位でトークン化しています.また,経過を取得するにあたり,JSの非同期とは(Dartの非同期とは)といった部分を忘れていたので調べ直したりもしていました.

これらの機能が加わったことで自作アプリはこのような実装になっています.

ライブラリを試す方法

ライブラリを作成する場合,実際にライブラリが正しく機能しているのかを試したくなりますが,ライブラリには関数void main()を含んでいないため,実際に動かすためのコードが必要になります.しかしライブラリを作っているのにその都度それを動かすアプリを実装すると言うのは不毛です.そこで活躍したのがテストコードになります.テストコードを動作確認として書くだけでわざわざアプリを作らなくても良いですし,そのテストコードが動作を保証するものにもなります.私自身純粋なライブラリを実装する経験は初めてだったので,これは個人的に大きな収穫でした.特に命令の実装などは関数のIOと挙動が明確に分かるため,TDD[9]の実践が出来たのも大きいです.

最後に

最後までお付き合いいただきありがとうございました.
記事やアプリの至らない点に気が付いてしまったあなたはフィードバックをしましょう.
至らない点以外のフィードバックもお待ちしております.

ライブラリの今後

このライブラリには記事執筆中に見つけてしまったバグや未実装の機能が幾つかあります.逆アセンブリの実装で苦しんだので実装されたら良いなという言語拡張も幾つか思いつきました.利用者が増えたり,フィードバック多くもらえて気分がよくなったら実装するかもしれません.それらを列挙してこの記事の締めとします.

未実装の機能

  • =を利用したリテラル記法(e.g. ='A')
  • 全てのシンタックスエラーパターン

発覚したバグ

  • マクロのラベルが効いていない
  • いつの間にか消えたNOP

入れたい言語拡張

  • JUMP先のラベルをサブルーチン内に限定
  • サブルーチン内のラベルスコープをサブルーチン内に限定
  • 独自のマクロ定義
脚注
  1. 情報処理推進機構 ↩︎

  2. https://www.jitec.ipa.go.jp/1_13download/shiken_yougo_ver4_3.pdf ↩︎

  3. 1語=16bits ↩︎

  4. コンパイル時に複数の命令に展開されるもの. ↩︎

  5. 30日でできる! OS自作入門 (ISBN 978-4-8399-1984-9)
    ゼロからのOS自作入門 (ISBN 978-4-8399-7586-9) ↩︎

  6. 自作エミュレータで学ぶx86アーキテクチャ (ISBN 978-4-8399-5474-1) ↩︎

  7. WEB+DB PRESS Vol.120 (ISBN 978-4-297-11811-2) ↩︎

  8. Go言語でつくるインタプリタ (ISBN 978-4-87311-822-2) ↩︎

  9. テスト駆動開発 ↩︎

Discussion