●スタックの概念
さて、ここでまずはスタックというものについて説明する必要があります。まずはスタックの概念から説明します。
スタック(Stack)という言葉は直訳で積み重ねを意味します。わかりやすくイメージするためすごく重いものを積み重ねるとします。その積み重ねたものをとろうしたら、重いので、一番上からしかとれないですよね。この場合だと、最初に取り出すことができるのは最後に積み上げた物(一番上の物)になります。これは「後入れ先出し」となるわけです。
この後入れ先出しの方式をデータ格納で利用したのが、スタックです。
実際のスタックを見てみましょう。
|
|
|
上の表のFFFEh番地を最後に積んだデータとします。その上にデータ03hを積んだとしたら
アドレス | データ |
---|---|
: : |
: : |
FFFDh | 03h |
FFFEh | 02h |
FFFFh | 01h |
このように積まれるわけですよね。今度、取り出すのは最後に積まれたものになりますから
|
|
|
このように取り出される、これがスタックです。
上の表のアドレスに注目してください。スタックはアドレスの底(大きい値の方)から積まれていってます。
結局、スタックは積み上げるごとに、アドレスが−1となり、取り出すごとアドレスが+1になるように動作します。
まず、一般的にコンピュータでのメモリの使い方を見てください。
エリア | 内容 |
---|---|
メモリ先頭番地 〜 コード最終番地 |
コード部 |
コード最終番地 〜 データ最終番地 |
データ部 |
データ最終番地 〜 スタック最終番地 |
フリーエリア |
スタック最終番地 〜 メモリ最終番地 |
スタックエリア |
すべてこれだというわけではないのですが、大まかに書くとこのような使い方をしていることが多いです。
コード部(プログラム)は先頭の方から最後の方に向かって実行されるのではじめに来ますね。その後ろに通常データ部が配置されます。これは保存するデータ、たとえば、テキストエディタのプログラムならば、入力されたテキストデータはこのエリアに保存されます。当然入力されるたびにそのデータは増えてゆくので、そのあとの部分はフリーエリア(未使用のメモリ)が必要です。そしてスタックはフリーエリアの後から最後の方に割り当てられています。
エリア | 内容 |
---|---|
メモリ先頭番地 〜 コード最終番地 |
コード部 |
コード最終番地 〜 データ最終番地 |
データ部 |
データ最終番地 〜 スタック開始番地 |
データフリーエリア |
スタック開始番地 〜 スタック最終番地 |
スタックエリア |
スタック最終番地 〜 メモリ最終番地 |
スタックフリーエリア |
もし、仮にこのような配置をしたらどうでしょう。
データエリアのみ増えていった場合、スタックのフリーエリアが残っていても、それ以上のデータの保存ができません。逆に、スタックエリアが、スタックのフリーエリアを使い切ったら、データエリアが残っていてもスタックは積めなくなりますよね。このような使い方だと、メモリの利用が非常に悪くなります。
このような理由から、データ部は最終番地方向に向かって、フリーエリアを使っていき、スタックエリアは先頭番地に向かって使っていくことになります。これにより、データ部、スタックエリアがそれぞれが反対側からフリーエリアを使っていくようなメモリ利用をします。結局、この方法だと、データ部、スタック部がお互い、フリーエリアを有効利用する事ができるわけなんです。なんて効率いいんでしょう、スタックを考えた人ってすごい!(笑)
このことから、スタックメモリを逆の方から使っていくこと、そして、先ほど述べたスタックのアドレスがスタックが積み上げるごとにアドレスが−1、取り出すごとアドレスが+1になってゆくのはこういった理由からです。
さて、このスタックは実際のところどんな風に利用するためにあるのでしょう。
スタックは普通、何かのデータを一時保管用するために使用されます。たとえば、一時的にレジスタの中にあるデータをスタックへ積み(データの回避)、レジスタを別の目的に使用した後、スタックからデータを戻す(データの復元)ように使用されます。
データの保管としては、直接メモリを指定したり、間接指定で保存を開始する場所をしていして、そこから、必要なエリア分データを保存する方法がありました。この方法の場合は常にどこにどんなデータをどれだけのエリアに保存するか、プログラマーが常に把握する必要がありますし、一時保管するたびに、毎回、アドレスを割り当てるのは、何かと手間がかかるでしょう。
そこで、そのような保管を行うには、このスタックがベストなのです。
●スタックとスタックポインタ
さてこれまでの例はスタックの出し入れを見たわけですが、当然、最後に積んだ場所であり、取り出す場所となるアドレスは記憶しておかないとスタックとして意味がありません。そのため、CPUにはこのスタックのアドレスを記憶する専用のレジスタがあります。
A | F |
B | C |
D | E |
H | L |
SP |
これまで説明してきたレジスタ以外に新しいレジスタが出ています。上の表のSPがそのレジスタになり、SPをスタックポインタと呼びます。当然、SPレジスタはスタック専用のレジスタですから、スタックを積むごとにこのレジスタのアドレスは−1、取り出すごとアドレスが+1になります。
これもまた、「ポインタ」であり、メモリ上の場所(アドレス)を示すためのものであるわけです。
また、Aレジスタの隣にFレジスタを書いています。これはフラグレジスタと呼ばれ、値の保存には使用しないもので、たとえば、計算結果が0になったとか、計算結果がマイナスになったなど、演算結果の目印(フラグ=旗)が入る専用のレジスタです。書き込むことができないわけではないのですが、通常はプログラマ側が操作することはないレジスタです。
ここまで説明したAからHLまでのレジスタはまとめて汎用レジスタとよばれ、それ以外にCPU自体が動作するために必要なレジスタがいくつかあります。それらを機能レジスタと呼んでいます。上の表で出てきたSPはその機能レジスタの一つにあたります。
ニーモニック | オペレーション |
---|---|
PUSH rr | (SP) ← rr |
POP rr | rr ← (SP) |
上の表が実際のスタックへデータを出し入れする命令です。スタック命令と書いていますが、実際は保存する場所がスタックポインタの示す場所というだけで、LDと同じ移動命令の仲間です。
PUSH命令はスタックへ指定したレジスタの値を積み上げるもの、そしてPOPが取り出す命令です。PUSHの名前の由来はスタックへデータの押し込みを、そしてPOPはデータが飛び出すとか、弾け出すイメージからつけられたものでしょう。
スタック命令のレジスタはAF、BC、DE、HLのように2つのレジスタを繋げて16ビットとしてスタッキングするようになっています。
ではこのスタック、実際にどんなふうに使えば有効かを説明しましょう。
演算処理中にキーボードが押されるとします。そうすると、当然そのキー入力に対しての処理をしなければいけませんが、計算途中の状態です。もし、そのままキー入力を「しかと」しちゃったら、使う側は寂しい限りですよね。当然それを無視するわけには行かない場合もあるでしょう。
そこでまず、キーが押された場合に
PUSH AF
PUSH BC
PUSH DE
PUSH HL
のような処理を行うようにしておきます。こうしておけばすべての汎用レジスタの値をスタックへ積むことになります。
レジスタの値はスタックへ回避されたので、レジスタは別の用途に使っても問題ないわけですから、キー入力し対しての処理を行います。
そして、キー入力の処理が終了したら、元の処理を途中から続行するために
POP HL
POP DE
POP BC
POP AF
のように、レジスタへスタックのデータを戻すわけです。
このように汎用レジスタの値をすべて元に戻したわけですから、Fレジスタも元に戻りますので、計算途中のフラグの状態も元に戻り、問題なく途中から処理が続行できるわけです。
上を見ても分かると思いますが、はじめにPUSHしたレジスタはAFで最後がHLです。当然、POPする順番はスタックの概念である後入れ先出しのきまりから、はじめがHL、最後がAFにならないとレジスタのデータが入れ替わってしまいます。
また、この入れ替わることを逆に利用し、よく使われるテクニックで、
PUSH BC
PUSH DE
POP BC
POP DE
といったのがあります。レジスタ同士の値を入れ替える技です。
当然、これをLD命令でやることもできますけど、その場合、空いているレジスタが必要ですし、メモリを直接指定して、保存し、入れ替える方法もあるでしょう。この方法であれば、レジスタが空いてなくても、またメモリを決めなくても、簡単に入れ替えることができます。
こういったように、スタックはその特徴から一時保管として非常に有効なデータ保存の方式なのです。