PC(プログラムカウンタ)とSP(スタックポインタ)
Z80プログラミング初心者の壁、PCとSP
今回の記事ではZ80でのマシン語プログラムの壁(と筆者は思っている)
PC(プログラムカウンタ)とSP(スタックポインタ)の説明です。
MSXに限定したことではないですし、あまり面白くない記事かもしれませんが、筆者自身が腑に落ちていないこともあり「スタックポインタ関連は暴走するきっかけの大半だから怖い」という食わず嫌いもあって、今までは記事にしていませんででした。
当記事の最初の頃にも「SPはこわいよ」って書いてました。
ですが、ゲーム制作を進めるにあたって、ようやく腑におちてきたことと
Z80のマシン語プログラミングを行ううえで重要な知識のため
あえてそれ専用の記事にしてみました。
賢者のみなさん>
間違ってる点などありましたらご指摘ください。
そもそものZ80での動作原理
いまさらといえば、いまさらの話題なのですが
そもそものZ80での動作原理としてPC(プログラムカウンタ)というのがあります。
例えばMSXのROMの場合、ROMヘッダに4010Hが始まりだよー。と書くと
4010HがPCにセットされてそのアドレスから処理が動き出します。
PCというのはCPUが内部的に持っている情報で「いままさに動いているアドレス」が保持されています。命令が実行されるたびにこのPCの値が次の命令のアドレスに進んでいきます。LD命令とかだとアドレスが次に進む。みたいな感じです。このPCの値を書き換えることでいろんなアドレスに強制的に処理をうつすことができます。
JP命令などはこのPCの値を書き換えることで、特定のアドレスにジャンプしたりしているわけです。
SP(スタックポインタ)
これがおそらく初心者には難解だと思っています。(個人的感想です)
Z80に限らずですがスタックポインタという仕組みがあります。
「とりあえず値を退避するよーん」
「退避していた値を戻すよーん」
といった感じで最初は使いがちです。
たとえば、こんな感じ。
LD HL, $1800 ; HLレジスタに値をセット
PUSH HL ; とりあえず値を退避
LD BC, 768 ; BCレジスタに値をセット
LD A, $20
CALL FILVRM ; これを呼び出すとHLの値が変わってしまうので
POP HL ; 退避していた値を戻す
ですが、SPを深く知るといろんなことができるようになってきます。
例えば
LD BC, HL
という命令は存在しませんが、それと同等のことができるようになります。
存在しない命令を作る
ちなみにZ80では存在しない命令の
LD BC, HL
を実現させようとすると、だいたいこう書きます。
LD B, H
LD C, L
; ここでBCの値はHLの値に書き変わってる
上記コードは、SPを使うとこう書けます。
PUSH HL ;
POP BC ;
; ここでBCの値はHLの値に書き変わってる
SPの仕組みを知らないと「うんーーー??」ってなります。
筆者も他人のコードを見てこういうのをみるたびに「うんーーー??」ってなってました。
SPの基礎知識
それではSPについて基礎知識からはじめてみましょう。
SPはスタックポインタの略ですが、PC同様、CPUが内部的に保持している情報になっています。
SPは常に2バイトの情報が保持されるようになっていて、PUSHなどでスタックに上積みしていくようになっています。
SPの初期アドレス
MSX1のFDDなしの場合、HIMEMと呼ばれるワークエリアの開始アドレスはF380Hになっていますがそのアドレスの直前のアドレス(F37FH)がスタックポインタの初期値になります。FDD付きの機種だとこのHIMEMの値がF380Hよりももっと小さいアドレスになったりするので注意が必要です。
HIMEMの値はワークエリアのFC4AHにセットされています。
この内容を見て判断しましょう。試しにwebmsxのDisk Basic起動状態でHIMEMの値を見てみるとこんな感じです。
当記事ではFDDなし機種のROMを前提にしていますので、常にプログラム上ではSPはF380Hである!と宣言してからプログラムをスタートさせるようにサンプルを作っています。initialize.asmで次のように宣言してます。
スタックポインタが積み上がりすぎて自分のプログラムのワークエリアを壊したりすることもありますが、よほどHIMEMに近い場所にプログラムで使うワークエリアを配置しない限りはそういうことは発生しません。
それでは実際のSPの挙動について説明します。
SPを扱うメジャーなやつはPUSH、POPだと思いますのでそれをメインで説明しますね。
PUSHとPOPとSPの動き
PUSH命令を実行するとSPのアドレスに値が格納されます。
値を格納したあとはSPそのものの値が-2されます。
次の図は、PUSH HL, PUSH DEを連続して実施したイメージ図です。
レジスタの上位8ビットがSP-1のアドレスに
レジスタの下位8ビットがSP-2のアドレスに
それぞれセットされていきます。これ重要。
LD (SP-1), 上位レジスタ
LD (SP-2), 下位レジスタ
という感じですね。
PUSHするとSPが-2されて次のPUSHではそのアドレスを基準にして値が格納されます。
さて次はPOPです。POPの挙動を見てみましょう。
次の図は上記PUSHした結果のSPを対象としてPOP命令を実行したイメージ図になります。
この図ではPOP DEをしたあとのスタックはからっぽになります。
筆者は
PUSH HLと書いたら必ずPOP HLで元に戻さなければいけないんだ!
と、ずっと思っていましたがまったくそんなことはありませんでした。
SPに入っている内容がなんなのかさえ気をつけていればそれでいい。
それだけのシロモノでした。
ちなみに、PUSHした内容が不要であればPOPするだけして使わなければそれでいい。というものでした。
さて、そういうシロモノであるということを前提にした応用編は後述するとしてCALLとRETもSPを使うのでそれについて先に説明しましょう。
CALLとRETとSP
CALLではRETでの戻り先アドレス(CALLの次の命令のアドレス)をSPにつみあげてから(PUSHしてから)
PCを呼び出し先のアドレスに更新します。
そのことで次の処理は呼び出し先のアドレスからになります。
その後、RETでそのアドレスをPOPして、そのアドレスの値でPCを書き換えることでジャンプします。
これがおおよそ普通の(一般的な)使い方ですね。
さて、最後にこれらのシロモノを応用した使い方です。
RETで元いた場所とは違う場所に戻ってこれる
「CALLじゃないけどRETで、なおかつ元の場所じゃないところに戻ってこれるよ」というやつです。BASICのRETURN 行番号みたいなやつですね。
この例では、HLレジスタにRET時のPCの戻り先アドレスをセットして最初にPUSHしています。このことでSPにはD100Hが格納されます。
その後、JPでD500Hにジャンプします。
D500HではRETを実行しますが、RETを実行するとSPからアドレスを取り出してPCの値を書き換えます。そのことでD100HにPCが移ります。
まとめ
今回の記事で説明したかった重点は以下のポイントです。
・PCで処理が動いていく
・PCを書き換えるとジャンプできる(JPはPCを書き換えてるだけ)
・SPにはPUSHでスタックに値が積み上がっていく
・POPでSPから値を取り出すとスタックが減っていく
・PUSHとPOPは対ではない(PUSH HLのあとに必ずPOP HLをしなければいけないというルールではない)
・CALLはPUSHしてからJPを実施する
・RETはPOPしてからJPを実施する
・CALLとRETが対でなければならないというルールではない
おまけ:わざとRETで暴走させるサンプル
CALLとRETの間でSPにPUSHしてPOPせずにRETが行われるとプログラムが暴走します。と、いうサンプルをMSXPenで作りました。
ASMコード内の
; POP 忘れた
の箇所を
POP HL
とかに修正すると正常に動作します。
(POP BCでもPOP DEでもなんでもOK)
CALLやRETでのスタックの意味が理解できると思います。
筆者近況
さて、筆者近況です。
とりあえず生きてます(当たり前か)
先日、MSX0が届きましてMSX-DOSについても勉強中だったりしています。
MSX0はFDD搭載機のエミュレータが搭載されているのですが、中学生当時はFDDが付いてるMSXは持っていなかったので新鮮に感じています。
MSX0で現在制作中のゲームを動かしてみたりなんたりして充実したMSXライフを満喫中です。
MSX0はROMがそのまま使えないのでNSLOADというやつを使ってROMを動かしています。
こんなにMSXに夢中になるのは中学生以来のことです。
夢中になれるものがあるって幸せなことだなあと思いますね。はい。
制作中のゲームもアイテム選択画面ができたりなんたりと牛歩なかんじですが進んでいます。そろそろゲーム制作も大詰めになってきてます。
ということで、次回あたりはMSX-DOSまわりについても解説できたらいいなあーなんて思ったりしている今日このごろです。
秋も深まってきそうで
みなさまもくれぐれも体調にはご注意ください。
では、また!ノシ