技術部ネットワーク課の若松です。
私はプログラムを書かなくなって(というか技術的革新のスピードに着いていけず)だいぶ経ちますが昔MSXというパソコン上で、アセンブラを使ってZ80のプログラムを書いていたことをふと思い出しました。
今回Z80のアセンブラについて説明したいと思います。
「〇〇とは?」的な説明が最初続きますがご容赦ください。
MSX
MSXとは
MSX(エム・エス・エックス)とは、1983年に米マイクロソフトとアスキーによって提唱された8ビット・16ビットのパソコンの共通規格の名称です。 詳しくはリンク先に譲りますが、国内のそうそうたるメーカーから対応したパソコン本体や周辺機器が発売されました。
MSXはCPUにZ80を使っており、ゲーム用のカセットを挿入するスロットもあって、さまざまなゲームもリリースされましたし、単体でMSX BASICというプログラミング言語が使えました。
家庭用のアナログテレビにつないで使用することができ、比較的低価格(数万円)でしたので、私も新聞配達をして稼いだお金で買ったことは良い思い出ですし、将来コンピュータに関連する仕事をしたいと思ったきっかけでもあります。
Z80
Z80とは
Z80 は、米国ザイログ社によって製造された 8ビット・マイクロプロセッサーです。"ゼットハチマル"とか"ゼッパチ"という名前を聞いたことがあるかたもいらっしゃるかも知れません。一時期8bitマイコンのデファクトスタンダードとしてポピュラーなものでした。
Z80のレジスタ構成
通常プログラムを作成するときは、変数に数値や文字列を代入すると思いますが、実際のCPUではレジスタとメモリを使います。
Z80には、以下の図のようにA/F/B/C/D/E/H/L という8bitレジスタと、PC/SP/IX/IYという16bitレジスタ、そのほかにR/Iレジスタなどがあります。
この中でAレジスタはアキュムレータ(Accumulator)の頭文字Aを意味し、演算の中心的役割を担います。
またフラグ(Flag)の頭文字Fを付けたFレジスタは、さまざまな演算結果の状態を記憶します(具体的には演算結果により特定のビットが変化します)。
そのほか汎用レジスタのB/C/D/E/H/LレジスタはそれぞれBC/DE/HLという構成で16bitレジスタとして使うことが出来ます。
Z80の命令群
Z80には後述するニーモニックの形として以下のような命令があります(この分類は個人的な主観が多分にあります)。
■データ転送/交換 LD、PUSH、POP、EX、EXX ■ブロック転送とブロックサーチ LDIR、LDDR、LDI、LDD、CPIR、CPDR、CDP、CPD ■演算命令 ADD、SUB、ADC、SBC、CP、INC、DEC、NEG、AND、OR、XOR、CPL、DAA ■ローテイト、シフト RLC、RRC、RL、RR、SLA、SRA、SRL、RLD、RRD、RLCA、RRCA、RLA、RRA ■ビット操作、フラグ操作 BIT、SET、RES、SCF、CCF ■ジャンプ、コール、リターン JP、JR、DJNZ、CALL、RST、RET、RETI、RETN ■入出力 IN、INI、INIR、IND、INDR、OUT、OUTI、OUTIR、OUTD、OUTDR ■CPUコントロール命令 NOP、HALT、DI、EI、IM
ざっと見てたくさんの(意味の分からない)命令があるなと思われるかも知れません。 しかし実際には足し算と引き算の命令はありますが、掛け算や割り算の命令はありません。ましてや便利な三角関数や文字列を操作する命令もありません。もちろん、配列変数もありません。
アセンブラやマシン語でプログラムを作る際は、こういった「欲しいけど無い命令」はアルゴリズムから自分で考える必要がありますし、また如何に短く高速なプログラムを作るかが腕の見せ所でもあります。
アセンブラ
マシン語とは
マシン語とは、コンピュータ内のCPUが直接理解し実行することが出来る命令からなる言語で、機械語ともいいます。 みなさんがC言語やPython、PHPで書いたプログラムもコンパイラやOSを通じて最終的にはマシン語として実行されます。 また、基本的にCPUのアーキテクチャが異なれば、マシン語は全て異なります。同じ実行結果を得るにもCPUが異なれば必然的に内部で異なるマシン語が実行されることになります。
昔はBASICのプログラムでも、実行速度を上げるために部分的にマシン語(というか見た目は16進数の羅列)で書かれているものがあって、羨望の眼差しで見ていたことを思い出します。
アセンブラとは
さきほど、CPUが理解し実行するのはマシン語であるという説明をしました。 例えば、"Aレジスタに10を代入する"という命令はマシン語では
00111110 00001010 (16進数で 3E 0A)
となります。 正直、2進数でも16進数でも難解ですよね。 これを
LD A,10
という表記にしたものがアセンブリ言語です。 また、アセンブリ言語で書かれたプログラムをマシン語に変換するソフトウェアをアセンブラと呼びます。
つまり「アセンブリ言語でプログラムを書く」、ということは「直接CPUやメモリを操作する」と同じことになります。
レジスタやメモリに数値を代入する時は "LD(Loadの意味)"という表記を使うわけですが、この"LD"の部分を「ニーモニック(mnemonic)」と呼びます。 当然CPUが実装している命令群やアセンブラソフトにより使えるニーモニックや表記方法も変わってきます。
簡単なプログラムを書いてみる
MSXPen
今回MSXPenという環境でプログラムを書いてみました。 MSXPenは、WebMSXというブラウザ上で動くMSXのエミュレータにMSX BASICおよびZ80アセンブラの環境をセットにしたものです。 プログラムを入力する際に自動的にコマンド候補を表示してくれたり、作成したプログラムを簡単にインターネット上で共有する仕組みもあります。
入力した文字の文字コードを表示するプログラム
試しに非常に簡単なプログラムを書いてみました。 入力した文字のASCIIコードを16進数で表示するプログラムです。
なお、アセンブラやマシン語ってどんなものなのかを、なんとなくイメージしていただくことを念頭に書きましたのでアルファベット(A~I/a~i)と数字にしか対応しておりません(それ以外のアルファベットや記号を入力すると変な出力になります)。 また、高速性や少ないバイト数で書くことも意識しておりません。
CHGET: EQU 0x009F ;キーボードから1文字読み取るBIOS CHPUT: EQU 0x00A2 ;画面に1文字出力するBIOS ORG 0xD000 ;プログラムをD000番地から開始する START: MAINLOOP: CALL CHGET ;キーボードから1文字読み込む CP 90 ;Aレジスタの内容を大文字の Z と比較し RET Z ;一致すればプログラム終了 CALL CHPUT ;入力された文字をそのまま1文字表示 LD B,A ;入力された文字を一旦Bレジスタに退避 LD A,"=" ;Aレジスタに = の文字コードをセットし CALL CHPUT ;= を表示 LD A,B ;Bレジスタの値を再度Aレジスタに読み込み SRA A ;右に1ビットシフト SRA A ;右に1ビットシフト SRA A ;右に1ビットシフト SRA A ;右に1ビットシフト ADD A,30h ;Aレジスタの値に30を足し、アスキーコードに変換 CALL CHPUT ;1文字表示 LD A,B ;Bレジスタの値を再度Aレジスタに読み込み AND 00001111b ;下位4ビットのみ取り出し ADD A,30h ;Aレジスタの値に30を足し、アスキーコードに変換 CALL CHPUT ;1文字表示 LD A,10 ;改行(LF) CALL CHPUT LD A,13 ;復帰(CR) CALL CHPUT JP MAINLOOP ;ループ先頭に戻る END START
画面の青い部分にマウスのフォーカスを持って行き、数字やアルファベットを入力してみてください。 対応する文字コードを16進数で表示します。大文字のZを入力するとOkを表示し、BASICの入力画面に戻ります。
簡単な解説
次に簡単に要所の解説をします。 まず冒頭の
CHGET: EQU 0x009F ;画面に1文字出力するBIOS CHPUT: EQU 0x00A2 ;キーボードから1文字読み取るBIOS
の部分ですが、アセンブラに対する疑似命令です。 C言語で言うところの #include や #define のようなものと言えば分かりやすいでしょうか。 例えば、このプログラム途中に出てくる
CALL CHGET
は最終的に
CALL 009Fh
に変換されます(その後 CD 9F 00 というマシン語になります)。
じゃぁ、009Fh とは何なのか、という疑問が浮かぶと思います。 先ほど説明した通り、Z80自体は非常に少ない命令群しかありません。 その代わりROM内にBIOSというキーボードやディスプレイなどの入出力装置を利用するためのサービスを提供するプログラムがあり、「入出力のためのAPI」いうイメージです。
BIOSは特定のレジスタに値をセットしてCALLすると、必要な動作が得られたり、逆にCALLすることで必要な情報が特定のレジスタに返される、という機能があります。 CHGET(009Fh)の正体はROM内の009F番地をCALLすることでキーボードから一文字入力されるまで待ち、入力された文字コードがAレジスタに返されるというという小さなプログラムです。これを呼ぶ(CALLする)ことで入力された文字を取得します。
こうなると、CHPUT(00A2h)もお分かりですね。こちらはAレジスタに文字コードを代入(LD)してCALLすることで画面に1文字出力してくれるBIOSです。
MSXのBIOSは非常に多くの種類が用意されており、これらを活用することで音を出したり、メモリの内容をVRAMにブロック転送したり、周辺機器を使用することが出来ます。
プログラムの解説に戻ります。
今回は入力された文字のASCIIコードを16進数で出力するというプログラムですので、キーボードから1文字の入力に対して上位4bitと下位4bitを分けて2文字で出力します。
まず上位4bitを取り出す方法ですが、
SRA A ;右に1ビットシフト SRA A ;右に1ビットシフト SRA A ;右に1ビットシフト SRA A ;右に1ビットシフト
とSRA A を4回実行することで、Aレジスタの内容を右に4bitシフトさせ、結果的に上位4bitの値が下位4bitに入ってくるようにします(上位4bitは0が入ります)。例えば、Aレジスタに2進数表記で0011xxxx という値があった時に00000011に変換されるような形です。 この後Aレジスタに30を足すことで、ASCIIコードに変換し、CALL CHPUTで1文字表示します。
ADD A,30h ;Aレジスタの値に30を足し、アスキーコードに変換 CALL CHPUT ;1文字表示
次に2文字目ですが、Aレジスタに対して00001111(2進数)でANDを取ることで下位4bitを取り出します。例えばAレジスタに2進数表記で10101011という値があった時に、ANDを取ることで下位4bitの00001011がAレジスタに入ります。
AND 00001111b ;下位4ビットのみ取り出し
この後再度Aレジスタに30を足し、CALL CHPUTする部分は上位4bitと同じです。
プログラム7行目と29行目の
MAINLOOP: (途中略) JP MAINLOOP ;ループ先頭に戻る
MAINLOOP もラベルと呼ばれる疑似アドレスです。 マシン語が生成される際にはメモリ上の番地に変換されます。 JP は、MAINLOOPに該当するアドレスにジャンプするという命令で、このプログラムの場合、冒頭のMAINLOOPに戻りキーボードから1文字入力されるのを待つ形になります。
試しにメモリをダンプしてみた
試しに、メモリ上にどのようにマシン語が記述されているかダンプしてみました(大文字のZを入力してBASICに戻った後、goto 30 と入力すれば実行されます)。
例えば、D010番地以降に CB 2F という16進数が4個ありますが、これが SRA A を4回分マシン語に変換した結果です。 最後に、C3 00 D0 となっている箇所がプログラム冒頭のD000番地にジャンプしている箇所です。
まとめ
アセンブラとマシン語
- すべてのプログラムは最終的にはCPU上でマシン語の形で実行されている。
- マシン語は直感的に理解しづらい面もあるが、そのためにアセンブリ言語がある。
- CPUに実装されていない命令や関数はアルゴリズムから考えて実装する必要がある。
- 私たちが普段使っているアプリは、何層にも渡る環境依存(CPU/OS/ハードウェア)の差を乗り越えて動いている。
感想
- 昔は、技術的情報を得るには雑誌や高価な専門書を買うしかなく、今のようにインターネットで情報収集が出来るなんて想像もできませんでした。
- 今のCPUはパイプライン制御や分岐予測、キャッシュメモリは当然として仮想化支援機能とかもあって凄いと思います。
- MSXPenのようにブラウザ のみでプログラム開発が(やろうと思えば)出来るというのは、本当にいい世の中になったと思います。
- 最後に、2022年になってZ80アセンブラの記事を書くとは我ながら思ってもいませんでしたが、いろいろ試行錯誤をする楽しみ(技術的好奇心)を久しぶりに思い出すことが出来ました。