見出し画像

マシン語エトセトラ

さて、今回はMSXからちょいと話をずらして「マシン語」のエトセトラについての記事になります。ここまで連載46回!筆者もびっくりの連載記事になっていますが、マシン語はテーマの中にちょこちょこはさむ程度でしたので復習をかねて「マシン語おぼえたてあるある」を記事にまとめてみようと思います。

ハマりポイント1:アドレス指定と変数定義

BASICではGOTO 行番号のように行番号を指定することでジャンプできます。また、BASICに限らずですがプログラミング言語には変数というものを定義することができます。マシン語を覚えたての場合、この行番号と変数がない。というところでいったんつまづきます。
まずマシン語(アセンブリ言語とかニーモニックともいいます)には、行番号もなければ変数も存在しません。すべて自分で作る。これが基本です。
変数VALに1を入れてその変数に1を足すサブルーチンを作ろう。とすると以下のようなコードになります。

                        BINARY(HEX)       
C000   LD D, 1          16 01
C002   CALL C080H       CD 80 C0
C005   RET              C9
C080   LD A, (C100H)    3A 00 C1
C083   ADD A, D         82
C084   LD (C100H), A    32 00 C1
C087   RET              C9
 
C100   DEFB 0

一番左のC000とかC100とか書かれてあるやつは行番号と思ってしまいがちですが、まったく違います。アドレスです。またLD D, 1と書いてあるからDレジスタに1を入れるんだなあ。ということはDレジスタは変数ということ?と思いがちですが、レジスタは変数ではありません。
LD D, n
はロード(転送)命令と言われるものですが、D変数に代入。ではありません。
Dレジスタにnの値を転送。する命令です。
繰り返しますがマシン語に変数は存在しないのです。
今回のコードでいえば、C100 DEFB 0と書かれてあるやつが変数VALに相当します変数は「自分で作る」のです。DEFB 0だと1バイト(8ビット)の0になりますがDEFW 0だと16ビットの0000Hになります。DEFWにすると16ビットなのでC100からC101までの2バイトがその変数が使う領域として定義されることになります。定義されるという言葉も変です。プログラムを書く人間が自分で「そこはそう使う」と決めただけなのです。
アドレスだけの世界だととてもプログラミングしづらいのでアセンブラとかではラベルをつけることができます。ラベルは「そのアドレスはプログラムの中でそう呼びますよ」という定義づけです。上記コードをラベル付けしてみるとこんな感じになります。

                        BINARY(HEX)       
C000   START:
       LD D, 1          16 01
C002   CALL ADD_VAL     CD 80 C0
C005   RET              C9
C080   ADD_VAL:
       LD A, (C100H)    3A 00 C1
C083   ADD A, D         82
C084   LD (C100H), A    32 00 C1
C087   RET              C9
 
C100   VAL:
       DEFB 0

C000HにSTART、C080HにADD_VAL、C100HにVALというラベル付けをした例です。ラベル付けはアセンブラと人間がわかるようにアドレスを名前付けしただけのものです。ラベル名は行番号でもないし、VALという変数ができたわけではないのです。
ちなみにMSXの非MSX-DOS環境の場合、変数領域はC000H以降に定義するのがベストです。8000Hでも良いですが、8000H以降はMSX-BASICが使用するため推奨しません。C000H以降であればRAMなのでアドレスに対して読み書きができます。0000Hから7FFFHまではROMです。前回記事までで何度かでててますが0000Hから3FFFHまでのPage#0と呼ばれるアドレスと、4000Hから7FFFHまでのPage#1と呼ばれるアドレスはROMなのでアドレスに値を書き込むことができません。アドレスの値を書き換えできないということは変数の場所として使うことはできない。ということです。
なお、C000H以降にプログラムを書くと読み書き可能なのでプログラムそのものを書き換える自己書き換えと呼ばれるテクニックも使えるようになります。筆者はスパゲティコードが嫌いなので使いませんが、容量を節約するために使ったりするようです。

                
C000   START:
       LD D, 1
C002   CALL ADD_VAL
C005   RET              
C080   ADD_VAL:
       LD A, (C100H)    
C083   ADD A, D         
C084   LD (C100H), A
C087   CP 5
C089   JP Z, SKIP_JIKOKAKIKAE
C08C   LD A, 2
C08E   LD (C001H), A
C091   SKIP_JIKOKAKIKAE:
       RET              
 
C100   VAL:
       DEFB 0

↑このコードではC087Hの処理で、Aレジスタの値が5だったらC001Hに2をセットしています。C001Hはプログラムの以下の部分です。

C000    START:
LD D, 1

この1の部分を2に書き換えるので、2をセットしたあとのプログラムは

C000    START:
LD D, 2

になります。プログラム自体を自分で書き換えてしまう大作戦。そんなことがマシン語ではできてしまいます。(おすすめはしません)
自己書き換えは話が脱線するので元に戻しましょう。
とにかく、マシン語にはアドレスしかなく、そのアドレスに書かれたある値をプログラム(=命令)とみなすか変数領域とみなすかはプログラムを書く人間次第。ということです。
どのアドレスをプログラミングの領域にして、どのアドレスを変数っぽい使い方にして・・というのがマシン語の設計の基本になります。
変数なんてえ便利なものは存在しねえ!ということは何度もプログラムを書いて暴走させればおのずと身についてくると思います(汗)

ハマりポイント2:キャリーフラグの罠

キャリーフラグというものがありますね。CYとかCとも言ったりします。
これは加算(ADD)や減算(SUB)を実施したときに桁あがりや桁下がりが発生するときに1になるフラグです。条件分岐するときに多用しますが、ちょっとした罠があったりします。次のコードを見てみましょう。

    LD B, 0
    LD C, 80H  ; BC = 80H
    LD L, 00H
    LD H, 01H  ; HL = 0100H
    LD A, 1
    SUB 10  ; A = 1 - 10
    CALL SUB_HL_BC

    ; MODORI
 
SUB_HL_BC:
    SBC HL, BC ; HL = HL - BC - CY
    RET

HLレジスタから16ビットレジスタの値を減算する場合、Z80の命令では
SBC HL, rp
しか存在しないため、割りと重宝して使うことも多いのですがここに実は罠が潜んでいます。
BCレジスタに80Hをセットして、HLレジスタには0100Hをセットしてます。
その後、Aレジスタで減算処理をしてからSUB_HL_BCを呼び出します。
SUB_HL_BCではHLレジスタの値からBCの値を引いてRETしてますね。
MODORIと書かれたアドレスに戻ってきたときの値はいくつでしょう?
0100H - 80H なので 80H?と思いがちですが違います。
SBC HL, BC の命令は HL - BC - CY なのです。
SUB 10 の箇所でA=1 - 10をしているのでキャリーが成立(CY=1)になっていて、そのまま何もせず SBC HL, BCの命令を実施しているため
HLの値は 0100H - 80H - 1 で 7FH になってしまうのです。
これは筆者もどハマりしました。。。
SBC HL, BCをCYの結果を関係なしで実施したい場合は以下のようにいったんCYを打ち消してから実施するようなコードを書くようにしましょう。
OR AってやるとAレジスタの値に変化はありませんが、CYフラグは0になります。そういうテクニック(?)
ちなみにADCとかSBCは8ビット演算にも存在します。
使うときはCYを意識したほうが良いですね(というか8ビットのADC、SBCはCYを意識した使いかたしかたぶんしない)

    LD B, 0
    LD C, 80H  ; BC = 80H
    LD L, 00H
    LD H, 01H  ; HL = 0100H
    LD A, 1
    SUB 10  ; A = 1 - 10
    CALL SUB_HL_BC
    ; MODORI
 
SUB_HL_BC:
    OR A ; A OR Aを実施して、とりあえずCYをゼロにしちゃう
    SBC HL, BC ; HL = HL - BC - CY
    RET

ハマりポイント3:8ビットで考えるんだ

Z80は8ビットCPUです。16ビット命令もたくさんあって便利は便利なのですが基本的に16ビット命令は遅いです。
ループの中で16ビット命令を大量に使用すると激オモになります。なので基本的には以下をこころがけましょう。

・ちょっと待て、その処理16ビットの必要ある?
・IX、IYの優しさに甘えるな
・アドレスの値を読み書き(変数の使用)も遅いんだなこれが

できるだけ8ビットで済む方法はないのか?
そういうパズル的な要素が必要になったりします。もちろん、そんなループを繰り返すようなプログラムでなければ必要ないですけど、ゲームを作ったりするとどうしても楽だから描きがち。そして、あとで後悔するはめになります。

「処理が遅すぎる!ああ、こんなところにIXレジスタ!ああ、こんなところに16ビット演算!!ああ、こんなところに16ビット転送!!ああ、詰んだ!!」
ってなります・・(筆者体験談)

ハマりポイント4:リトルエンディアンですよ〜

まあ、当たり前っちゃあたりまえなんですが、リトルエンディアンなので注意しましょうねっていう。

HLレジスタに1234Hとセットされていたら(H=12H、L=34Hとなっていたら)
LD (nn), HL
をやると nn + 0 にはLレジスタの値、nn + 1にはHレジスタの値が入ります。
逆もまたしかり。
nn + 0に12H、nn + 1に34Hが入っていたとして
LD HL, (nn)
をやると、Hレジスタには34H、Lレジスタには12Hが格納されます。
これ、プログラムを書いてるとどっちがどっちやら、どっちだったっけ?ってなったり、思い込みでとんでもないコードになったりしたりします。
ので、いちおう当たり前だけどとても重要な罠としてあげときます。

以上、今回はマシン語のもろもろについて解説しました。

あんまり役に立たなかったかもしれませんが、私のサンプルでもコメントに「変数」とかって書いていたりするしSBC HL, BCを多用しているし、16ビット系命令を多用してたりするしで、変に誤った刷り込みがされてるとやばいなあと思って(汗)

では、また!ノシ


セーラー服が似合うおじさんです。猫好き、酒好き、ガジェット好き、楽しいことならなんでも好き。そんな「好き」をつらつらと書き留めていきます。