2017-03-06 18:53 — asano
カテゴリー:
2日目の今日は80系(8080, Z80, 8085など)のちょっと懐かしいテクニックです。
今こんな書き方をすると、わかりにくいと怒られるか、パイプラインなどに悪影響が出たり技術的にもデメリットになったりしますので避けるべきですが、当時はそれなりにメリットもあってよく使われていました。
一つは命令の途中に飛び込むというものです。例えばこんな感じです。
1: 8000 ORG 8000H
2: 8000 entry1:
3: 8000 3EAF LD A,0AFH
4: 8002 3200FF LD (0FF00H),A
5: 8005 C9 RET
3行目でAレジスタに0AFHを入れ、4行目で0FF00H番地のメモリに書き込んで、リターンするだけのものです。
ここでは8000Hがエントリポイントですが、8001Hもエントリポイントと考えると以下のようになります。オブジェクトは上と一緒です。
1: 8000 ORG 8000H
2: 8000 entry1:
3: 8000 3E DEFB 3EH
4: 8001 entry2:
5: 8001 AF XOR A
6: 8002 3200FF LD (0FF00H),A
7: 8005 C9 RET
5行目でAレジスタ同士のXORを取り(結果としてAレジスタは00Hになります)、メモリに書き込むところ以降は上と同じになります。
8000HをCALLすれば0FF00H番地に0AFHが書き込まれ、8001HをCALLすれば00Hが書き込まれます。
これを素直に書けばこうなります。
1: 8000 ORG 8000H
2: 8000 entry1:
3: 8000 3EAF LD A,0AFH
4: 8002 1801 JR L1
5: 8004 entry2:
6: 8004 AF XOR A
7: 8005 L1:
8: 8005 3200FF LD (0FF00H),A
9: 8008 C9 RET
これだと9バイトになりますから、3バイトの節約になっていたわけです。
一方、デメリットはいくつもあります。
- アセンブラで書きにくい。
- 値が自由に選べない。
- 3つ以上にできない。
- わかりにくい。
それでもコードサイズが小さくなること、実行速度的にも有利なので使われました。これはNECのPC-8001, PC-8001mk2などのN-BASICのROMでも使われていたはずです。
2,3は他のレジスタを壊しても良いなら回避する方法もありますが、数が増えると速度的には不利になるのであまり使われなかったと思います。
4は見方を変えるとメリット(プロテクト等での解析の妨害)だったかもしれませんね。
お次はサブルーチンコール時のリターンスタックをいじるものです。
1: 9000 ORG 9000H
2: 9000 prtstr:
3: 9000 E1 POP HL
4: 9001 L1:
5: 9001 7E LD A,(HL)
6: 9002 23 INC HL
7: 9003 B7 OR A
8: 9004 2805 JR Z,L2
9: 9006 CD5702 CALL 0257H
10: 9009 18F6 JR L1
11: 900B L2:
12: 900B E5 PUSH HL
13: 900C C9 RET
これは文字列(00Hで終端)を出力するルーチンですが、文字列のアドレスを明示的に渡す必要が無いというものです。
3行目でスタックトップをHLレジスタに取り出しますが、スタックトップにはこのサブルーチンからの戻りアドレスが入っています。5行目ではメモリから1文字のコードを取り出し、6行目でポインタを進めます。7行目はAレジスタ同士のOR演算、同じもののORをとっても値は変化しませんが、結果がフラグに反映されます(常套手段、ORの代わりにANDでもよい)。8行目は文字列の終端チェック、直前のORでAレジスタが00HならZフラグが立っているのでL2へ抜けます。
9行目では1文字出力ルーチン(0257HはN-BASICのROMルーチン)を呼び出して1文字表示します。10行目はL1に戻って次の文字の取り出しに進みます。
11行目のL2:ではHLレジスタは文字列終端の00Hの次のアドレスを指しています。12行目でこれをスタックトップに戻します。13行目のRET命令ではスタックトップに戻りアドレスがあるとしてそこに分岐しますが、それは文字列終端の次のアドレスに書き換わっています。
JP (HL)
」に置き換えられます。
このテクニックはサブルーチンを効率化するためではなく、呼び出し側を簡単にするためにこそあります。呼び出し側はこんな感じになります。
1: 8000 ORG 8000H
2: 8000 CD0090 CALL 9000H
3: 8003 53545249 DEFB 'STRING',0
4E4700
4: 800A L0:
2行目でいきなり呼び出しを実行します。文字列の場所を指定する必要はありません。CALL
命令が自動的に戻りアドレスをスタックに積みますがそれが文字列の位置指定になっているからです。
3行目、CALL
命令の直後に文字列を配置します。スタックには8003Hが積まれているのでこの文字列が出力されます。
戻ってくるときは8003Hではなく800AHに戻ってくるのでプログラムの続きは文字列の直後から配置しておけば良いのです。
コンパイル言語(TL/1だったと思います)のランタイムで使われているのを見つけたとき、「うまいなぁ~」と感心したのを憶えています。
上の例だとA,HLレジスタの内容が壊れますが、次のようにすると壊れないようにすることもできます。
1: 9000 ORG 9000H
2: 9000 prtstr:
3: 9000 E3 EX (SP),HL
4: 9001 F5 PUSH AF
5: 9002 L1:
6: 9002 7E LD A,(HL)
7: 9003 23 INC HL
8: 9004 B7 OR A
9: 9005 2805 JR Z,L2
10: 9007 CD5702 CALL 0257H
11: 900A 18F6 JR L1
12: 900C L2:
13: 900C F1 POP AF
14: 900D E3 EX (SP),HL
15: 900E C9 RET
3行目でスタックトップを取り出す代わりに、HLレジスタと入れ替えます。また4行目でAレジスタとフラグもスタックに積んでおきます。これでレジスタを全く壊さない(CALL 0257H
がレジスタを壊さないな前提)ものができますが、EX (SP),HL
命令は重い(時間がかかる)命令なので普通はここまでしません。
80系の場合、A,HLレジスタは使用頻度が高い(何をするにも使う必要がある)ので自動的に保存するより必要に応じて保存するほうが良いのです。「使用頻度が高いならなおさら保存するべきでは」と思うかもしれませんが、逆に一つの用途に長く使えないのでかえって保存しなくて良い場合が多いのです。
まだまだ「リロケートの技」とかZ80限定ですが「IX,IYの使い方」とか書きたいことはあります。もう何回か80系アセンブラの話は書くと思います。
コメントを追加