こんにちは、めのんです!
1週間に1回程度の更新なので間が空いてしまいますね。
忘れてしまった方はぜひ前の投稿も読んでみてください!
さて、今回は「スタックポインタ」について考えてみたいと思います。
その前に「スタック」についても簡単に説明することにします。
スタックとは?
スタックポインタの話をするためにはスタックがわかっていないと始まりません。
詳しくはウィキペディアの解説なんかを見ていただくとして、ここでは最低限の説明にとどめたいと思います。
スタックというのは後入れ先出しのデータ構造のことで、LIFO(Last In First Out)という言い方をすることもあります。
上の写真はプラスチックのトレイを積み重ねていますね。
このように積み重ねると、そこから取り出そうと思うと普通は一番上のものから取り出すことになります。
一番上に積まれたトレイは最後に積まれたものです。
最初に積まれた(置かれた?)トレイは、上に積まれているトレイを全部取り除いてからでないと取ることができません。
こういうデータ構造が「スタック」です。
スタックをプログラムで実現する
CPUはスタック構造をハードウェアで実現しています。
いきなりハードウェアの話に入っても難しいので、まずはスタック構造を実現するCのプログラムを見ていくことにしましょう。
#include <stdio.h>
#include <stdint.h>
uint8_t stack[1024]; // スタック領域
uint8_t pos = 0; // 次にスタックに積む位置
// スタックに数値を積む
void push(uint8_t value)
{
stack[pos++] = value;
}
// スタックから値を取り出す
uint8_t pop(void)
{
return stack[--pos];
}
int main(void)
{
push(12);
push(34);
printf("%d\n", pop());
printf("%d\n", pop());
}
上のプログラムでは、stackという配列を値を実際に積んでいくメモリ(スタック領域)に見立てています。
スタックを積むときはstack[pos]に値を書き込みます。
そして次に書き込む位置を移動するためにposをインクリメントしています。
スタックから値を取り出すときは、先にposをデクリメントしてからstack[pos]の値を読んでいます。
伝統的にスタックに値を積む操作を「プッシュ」、スタックから値を取り出す操作を「ポップ」と呼びますので、上のプログラムでも関数名をそのようにしています。
CPUに備わったスタックの仕組み
多くのCPUでは、先ほどのプログラムと同じような方法でハードウェア的にスタックを実現しています。
CPUによってはハードウェア的なスタックの仕組みが備わっていないので、ソフトウェア的にスタックを実現していることもあります。
RISCにそういうのが多いのですが、AVRはRISCなのにハードウェア的なスタックが備わっています。
先ほどのプログラムとちょっと違うのは、CPUに備わったスタックの仕組みでは伝統的にRAMの最後(最上位アドレス)から先頭(最下位アドレス)に向かってデータを書き込んでいきます。
つまり、posを1023に初期化した上で、プッシュするたびにデクリメントして、ポップするたびにインクリメントすることになります。
そしてこの変数posに相当するものが「スタックポインタ」で、これも一種のレジスタになります。
AVRのスタックポインタ
AVRというかATmega328のスタックポインタ(「SP」と略します)は16ビットのレジスタです。
一応16ビットあるんですが、実際に使われるのは下位12ビットだけです。
これはATmega328のデータメモリが0x0000から0x08ffまでなので12ビットあれば十分だからです。
データメモリの最後のアドレスが0x8ffなので、SPの初期値は0x08ffになります。
これはCPUがリセットされるとSPに0x08ffが格納されるということです。
AVRでは汎用レジスタにもアドレスが割り当てられていましたが、実はスタックポインタにもアドレスが割り当てられています。
スタックポインタの下位8ビットが0x005dに、上位8ビットが0x005eに割り付けられています。
16ビットのレジスタなのに奇数番地に割り付けられているのがちょっと嫌ですけど、こういう仕様なので仕方がありません。
PUSH命令とPOP命令
PUSH命令を実行すると、SPが指すアドレスに指定したレジスタの値が書き込まれてSPがデクリメントされます。
逆にPOP命令を実行すると、SPをインクリメントしてからSPが指すアドレスの値がレジスタに読み込まれます。
サブルーチンとスタック
実はPUSHやPOP以外でもスタックが使われることがあります。
それはサブルーチンを呼び出したり、呼び出し元に戻ったりする場合です。
「サブルーチン」というともしかしたら聞いたことがない方もいらっしゃるかもしれませんね。
ちょっと乱暴な説明ですが、関数やメソッドのようなものだと思っていただいてかまいません。
サブルーチンを呼び出す際には、サブルーチンから戻ったときに実行される命令のアドレスがスタックに積まれます。
サブルーチンから戻る際には、スタックから戻り先アドレスが取り出されて、そのアドレスに分岐します。
プログラムメモリのアドレスは0x0000から0x3fffですので戻り先アドレスをスタックに積む際には16ビット(=2バイト)分をRAMに書き込むことになります。
もちろんスタックポインタを増減する際も2ずつ動かします。
このときスタックに積む戻り先アドレスは、AVRでは下位8ビットを先に上位8ビットをあとに積みますので、格納された結果を見るとビッグエンディアンになっています。
命令セットシミュレータでの実装
今回制作している命令セットシミュレータでのスタックポインタの実装ですが、とくに何もすることなくデータメモリの0x005d番地と0x005e番地を直接使おうかなと考えています。
uint8_t data_memory[0x900];
データメモリは上のような単純な配列を考えていますので、data_memory[0x005d]がスタックポインタの下位8ビット、data_memory[0x005e]が上位8ビットにあたります。
必要ならスタックポインタの値を取り出しやすくするためのマクロを作るかもしれませんが、今のところは未定です。
という感じで今回はスタックポインタについてお話しました。
ちょっと長くなってしまいましたので、プログラムカウンタについては次回に回したいと思います。
どうぞご期待ください!