« OSBDMで使うデバッグ・モード導入装置 (6) | メイン | XPortを使ったNetwork入門キット(14) »

HC08マイコンの使い方QY4A編 -
《72》スタック・ポインタとスタック領域(3)


 シミュレータを使ってスタック・ポインタ(SP) の動きを確認します。 C言語の変数の性質についていくつか確認し、それらがどこに配置されるかを観察します。
 

subroutine_image.gif

関数またはサブルーチン呼び出しのイメージ図 (再掲)

 

◆ C言語を使ったスタック確認用テスト・プログラム
 前回 に続いてスタック・ポインタ(SP) の動きを確認します。 C言語のサンプル・プログラムを下記に用意しました。 プロジェクトごと圧縮したものを hc08_stack02.zip として保存しましたのでご利用ください。
 

06    char var1 = 2;
07
08    void func01( void );    // 関数1 プロトタイプ宣言
09    void func02( void );    // 関数2 プロトタイプ宣言
10
11    void main( void ){      // メイン関数
12        var1 += 3;
13   
14        func01( );
15        func01( );
16        func02( );
17       
18        for( ; ; ){
19            __RESET_WATCHDOG( );
20        }
21    }
22
23    void func01( void ){    // テスト用の簡単な関数1
24        char var2 = 4;
25        static char var3 = 7;
26   
27        var2 += var1;
28        var3 += var2;
29    }
30
31    void func02( void ){    // テスト用の簡単な関数2
32        func01( );
33        var1 = 0;
34    }

 さてここで問題です。 このプログラムを for ループまで実行したとき、関数 func01 の中にある変数 var3 の値は何でしょうか? 引っ掛け問題、いじわる問題ではないので普通に答えてください。 「見えません」 とか 「不定です」 のような答えではありません。 何かメモを取りながらでもよいので、まじめに考えてみてくださいね。


・ 変数の管理について
 C言語の初心者の方は、記憶クラスとかスコープといった言葉を聞いたことがあるでしょうか? 正確な説明は C言語の解説書などに譲ることにして、ここでは上のサンプル・プログラムを読むのに必要な範囲の説明にとどめます。

グローバル変数 ・・・・・ 関数の外で宣言された変数を グローバル変数(大域変数) といいます。 グローバル変数は、プログラム・ソース・ファイル内のどこからでもアクセスできます。 このことを ファイル・スコープ といいます。 スコープというのは 可視性 のことです。
 変数の宣言をするときは、初期化も一緒に書くことができます。 グローバル変数の場合は初期化を省略するとプログラム開始時の値が 0  になります。

ローカル変数 ・・・・・ 関数の中で宣言された変数を ローカル変数(局所変数) といいます。 ローカル変数は、その関数の中でのみアクセスできます。 このことを 関数スコープ といいます。

static変数 ・・・・・ ローカル変数のうち、static という記憶クラス指定子を付けて宣言した変数は static変数(静的変数) になります。 static 変数の宣言と一緒に初期化を書いておくと、プログラム開始時に一度だけ初期化が行われます。 関数の呼び出しごとに初期化されるのではない点に注意してください。 そして、関数を呼び出してその関数が終了しても static変数はそのまま存在し続けて値を保持します。 つまり次回その関数を呼び出したときは、保持してあった値が継続して読み出されるのです。 なお、static 変数の場合も宣言時の初期化を省略するとプログラム開始時の値が 0  になります。

auto変数 ・・・・・ ローカル変数のうち、auto という記憶クラス指定子を付けて宣言した変数は auto変数(自動変数) になります。 また、記憶クラス指定子を省略 した場合も auto変数になります。 auto変数の場合は、その関数が呼び出された時点で一時的な記憶領域に変数が生成され(初期化子があれば初期化も行う)、関数が終了するときに消滅(記憶領域を開放) します。 宣言時の初期化を省略すると、変数の値が 不定 のまま生成されます。

 この分類で見ると、上のプログラム例では var1 がグローバル変数、var2 は auto変数、var3 は static変数 になっています。


・ スタック確認用テスト・プログラムを読んでみる
 変数の管理を理解したところで、上のプログラムを読んでみましょう。

06    char var1 = 2;
      グローバル変数 var1 を宣言し、値を var1 = 2 に初期化しています。
12        var1 += 3;
      var1 の値に 3 を足して、var1 に入れています。 var1 = 5
14        func01( );
      関数 func01 を呼び出しています。(1回目の呼び出し)

24        char var2 = 4;
      関数 func01 の中で auto変数 var2 を宣言し、値を var2 = 4 に初期化しています。
25        static char var3 = 7;
      関数 func01 の中で static変数 var3 を宣言しています。 この関数が呼び出されたのは初めてなので、値は var3 = 7 に初期化されています。
27        var2 += var1;
      var2 の値に var1 の値を足して、var2 に入れています。 var2 = 9
28        var3 += var2;
      var3 の値に var2 の値を足して、var3 に入れています。 var3 = 16
15        func01( );
      関数 func01 を呼び出しています。(2回目の呼び出し)
24        char var2 = 4;
      関数 func01 の中で auto変数 var2 を宣言し、値を var2 = 4 に初期化しています。
25        static char var3 = 7;
      関数 func01 の中で static変数 var3 を宣言しています。 この関数が呼び出されたのは初めてではないので、値は前回から保持されて var3 = 16 になっています。
27        var2 += var1;
      var2 の値に var1 の値を足して、var2 に入れています。 var2 = 9
28        var3 += var2;
      var3 の値に var2 の値を足して、var3 に入れています。 var3 = 25
16        func02( );
      関数 func02 を呼び出しています。
32        func01( );
      関数 func01 を呼び出しています。(3回目の呼び出し)
24        char var2 = 4;
      関数 func01 の中で auto変数 var2 を宣言し、値を var2 = 4 に初期化しています。
25        static char var3 = 7;
      関数 func01 の中で static変数 var3 を宣言しています。 この関数が呼び出されたのは初めてではないので、値は前回から保持されて var3 = 25 になっています。
27        var2 += var1;
      var2 の値に var1 の値を足して、var2 に入れています。 var2 = 9
28        var3 += var2;
      var3 の値に var2 の値を足して、var3 に入れています。 var3 = 34
33        var1 = 0;
      グローバル変数 var1 を 0 にしています。 var1 = 0
18~20  for ループ
      for ループで実質プログラム終了です。

 いかがでしょう。 結局 var3 の値は 34 (16進数で $22 ) でした。 正解しましたか?


・ テスト・プログラムを Assembly Step 実行で動かしてみる
 それでは前回と同様にして、このプログラムをステップ実行で動かしてレジスタや RAMの変化を確認してみましょう。

 CodeWarrior で hc08_stack02 を開き、Debug ボタンをクリックするとシミュレータ画面が開きます (下図クリックで拡大)。 Memory ペインの中で右クリック、【Address...】 を選択してください。 今回は SP の値が B1 になっているので、その少し前のあたりということで 80 と入力します。 【Hex Format】 にチェックが入っているのを確認して、【OK】 をクリックします。 すると Memory ペインに $0080 ~$00B7 あたりが表示されます。 デバッガを開くとすぐにこのプログラムのスタートアップ・ルーチン(*1) が実行されたため、スタック領域 ($00B1 番地の前あたり) を見ると RAM を利用した痕跡が見られます (uu 以外の何らかの数値が入っている) 。
 次に、SP 以外の CPUレジスタの値を確認します。 Assembly ペインの PC の値が EE9F(16進数) になっているはずです。 これは最初の命令が $EE9F 番地から始まっていることを表しています。 そこで、Assembly ペインの中で右クリック、【Address...】 を選択してから、EE9F と入力します。 【Hex Format】 にチェックが入っているのを確認して、【OK】 をクリックします。 すると Assembly ペインに $EE9F 番地からの命令が表示されます。
 

hc08_stack03.gif

 Assembly Step ボタンをクリックして、1命令だけ実行してください(Single Stepボタンは C言語の 1行分になるのでNG)。
 

hc08_stack05.gif

$EE9F LDHX  #0x0080  を実行しました。(H:X に即値 $0080 を格納の意味)
H:X レジスタに $0080 が入りました。
PC は次の命令のある $EEA2 を示しています。

 同様にして、Assembly Step 実行で1命令だけ進めてください。
$EEA2 LDA  ,X  を実行しました。 (H:X レジスタが指し示すアドレスの内容をアキュムレータに格納する、の意味)
H:X レジスタが指す $0080 番地の値は 2 なので、アキュムレータには 2が入ります。

 以下、「Assembly Step 実行で1命令だけ進めてください」 は省略します。
$EEA3 ADD  #0x03  を実行しました。
即値 3 を足して、アキュムレータが 5 になりました。

$EEA5 STA  ,X  を実行しました。 (アキュムレータの値を H:X レジスタが指し示すアドレスに格納する、の意味)
アキュムレータの値が 5 なので、H:X レジスタが指す $0080 番地に 5 が入りました。

次は Source ペインを一緒に見るとわかるように、func01 関数を呼び出すという意味です。
$EEA6 JSR  0xEEB4  を実行しました。(下図クリックで拡大)
スタックに $EEA9 という 2バイトの値が格納されました。 これはサブルーチン呼び出し命令(*2) の次に置かれている命令のアドレスです。 サブルーチンから復帰してくるときに、ここへ帰ってきます。 SP の値は 2減って $00AF です。
サブルーチンの先頭アドレスにジャンプしたので、PC の値が $EEB4 になりました。
 

hc08_stack04.gif

$EEB4 PSHH  を実行しました。 (H レジスタをプッシュする、の意味)
H レジスタ(H:X レジスタの上位バイト) が $00 なので、スタックに $00 という値が積まれます。 実はこの値には意味はなくて、SP を -1 してそこに 1バイト分の変数領域を確保するのが目的です。 SP の値は一つ減って $00AE になりました。 つまり $00AF 番地が一時的な変数の領域として確保されたということになります。

$EEB5 LDA  #0x04  を実行しました。
アキュムレータに 4 が入りました。

$EEB7 TSX  を実行しました。 (SP の値+1 を H:X レジスタに格納する、の意味)
SP の値が $00AE なので、+1 して H:X には $00AF が入ります。

$EEB8 STA  ,X  を実行しました。 (アキュムレータの値を H:X レジスタが指し示すアドレスに格納する、の意味)
アキュムレータの値が 4 なので、H:X レジスタが指す $00AF 番地に 4 が入ります。
これが char var2 = 4; の部分になります。

ソース・プログラムの static char var3 = 7; に相当する処理はありません。 この初期化はスタートアップ・ルーチンで処理済みなのです。

$EEB9 LDA  0x0080  を実行しました。
($0080 番地の内容をアキュムレータに格納する、の意味)
アキュムレータに 5 が入りました。

$EEBC ADD  ,X  を実行しました。
(H:X レジスタが指し示すアドレスの内容をアキュムレータに加算する、の意味)
$00AF 番地の内容 4 を足して、アキュムレータが 9 になりました。

$EEBD PSHA  を実行しました。
加算の結果 9 を一時データとしてスタックに追加格納しました($00AE 番地)。 ここはちょっと興味深いところで、ソース・プログラムでは結果を var2 に保存するよう書かれていますから加算の結果を素直に $00AF 番地 (var2) に入れればよいと思うのですが、実際には var2 が最適化されて 「var2 に保存する必要なし」 となっています。 つまり一時データとして得られた加算結果の値を var2 と var3 に入れる必要はないので、var3 だけに入れて済ませているのですね。
SP は一つ減って $00AD になっています。

$EEBE LDA  0x0081  を実行しました。
$0081 番地の値が 7 だったので、アキュムレータに 7 が入りました。 ところでこの番地は何でしょう? Data:2 ペインの var3 をクリックすると Address 0x81 と表示されます。 つまり、これは var3 のことだったのです。

$EEC1 TSX  を実行しました。 (SP の値+1 を H:X レジスタに格納する、の意味)
SP の値が $00AD なので、+1 して H:X には $00AE が入ります。

$EEC2 ADD  ,X  を実行しました。
(H:X レジスタが指し示すアドレスの内容をアキュムレータに加算する、の意味)
$00AE 番地の内容 9 を足して、アキュムレータが 16 ($10) になりました。

$EEC3 STA  0x0081  を実行しました。
アキュムレータの値が 16 ($10) なので、$0081 (var3) に 16 ($10) が入りました。

ここまでで関数 func01 の中身の処理が終わりました。 2回プッシュ(データを押し込む)操作を行いましたから、この後で呼び出し元へリターンする前に 2回プル(データを引っ張り出す)しておく必要があります。 そうしないと、スタックへデータを入れた量と取り出した量が合わなくなって、繰り返すうちにスタックがあふれるという大変な事態を引き起こします。 普通に PULA などを 2回行ってもよいのですが、ここはSP の値を直接操作しています。

$EEC6 AIS  #2  を実行しました。 (SP の値に即値 2 を符号付き加算して、結果を SP に格納する、の意味)
SP の値が 2 増えて $00AF になりました。
これで関数内で使った一時変数の領域を開放したことになります。

$EEC8 RTS  を実行しました。
SP が $00AF のときに RTS を実行したので $00B0 から 2バイトを読み出して、その値 $EEA9 を PC に格納しました。 つまりサブルーチン・コール命令の次の命令に戻ったわけです。 (戻ったところは 2回目の func01 呼び出しです)
SP は 2増えて $00B1 になりました。

 さてこのようにして func01 の呼び出しから一時変数の準備、そして処理の実行を終えて復帰まで、CPUレジスタの動きを細かく見てきました。 スタックの使われ方がだいぶ理解できたのではないでしょうか。

 ここで、続きを実行するのに 三つの選択肢があります。 好きな方法で進めてください。
(a) 次の命令は 2回目の func01 関数実行になっていますので、先ほどと同じようにして Assembly Step ボタンをクリックして丁寧に確認することができます。 ただし先ほどと比べると var3 の値が 1回目と違います(増えている)。 なぜそうなるかというと、もうおわかりかと思いますが var3 が static 変数 だからですね。
(b) 「ちょっと、もう面倒くさいな」 という場合は Assembly Step ではなく、 Single Step ボタンを使ってもかまいません。 C言語プログラム・ソースの1行分を実行しますので、Assembly Step よりも早く進めることができます。 その場合は、計算の途中で var2 への格納が削除されていることを忘れないでください。 うっかり忘れていると、もしかしてバグ?と不安になるかもしれません。
(c) もうわかった、Single Step 実行も面倒くさい、という場合は Step Over 実行ボタンを使っても良いです。 これは Single Step に似ていますが、関数の場合は関数を丸ごと実行するのでサクサク進みます。

 仮に (c) を選んだとすると 2回目のfunc01 が一気に実行され、 $0081 番地(var3) の値が 9 増えて 25 ($19) になっているはずです。 SP は減って増えて元に戻って $00B1 のはずです。

 次は func02 関数です。 もう大体わかってきましたね。 でも気を抜かないで、Single Step 実行を 1回クリックしてみてください。 関数呼び出しを行ったので、サブルーチン・コール命令が実行されて SP が 2減って $00AF になっています。

 さて、次の命令は func02 関数の中にある func01 関数呼び出しです。 これは 3回目の func01 呼び出しになります。 先ほどの 三つの方法のうち、好きな方法で実行させてみてください。 3回目も 1回目、2回目と同じことをするわけですが、実際の動きは違うところがあります。 それは、この func01 関数の実行開始時の SP の値がこれまでとは違うのです。 そのことでどういう影響が出るかというと、一時的な変数を確保する位置が変わります。 具体的には先ほどまでとは 2バイトずれた位置に確保されます。 ですから、auto 変数である var2 の配置も、スタック内のアドレスが先ほどとは異なります。 このように関数内の一時変数には決まったアドレスがない、ということを理解しておいてください。

 func02 関数の中の func01 を、 (c) の方法で実行したとしましょう。 つまり Step Over ボタンをクリックします。 すると、$0081 番地(var3) の値が 9 増えて 34 ($22) になっているはずです。 SP は減って増えて元に戻って $00AF のはずです。

 次は var1 = 0; ですが、あまり本筋には関係ないので func02 関数を一気に抜け出しましょう。 Step Out ボタンを 1回クリックしてください。 これは今ある関数を最後まで実行して、その関数の呼び出し元へ戻るところまで進めて止まります(Single Step 実行と組み合わせて使うと非常に便利)。

 呼び出し元へ戻ると、もう最後の for ループまで来ました。 このプログラムでいうと、実質的なプログラム終了です。 SP の値は最初と同じ $00B1 になっているはずです。 そして $0081 番地(var3) の値は、34 ($22) が正解でした。

 以上、読むだけだとかなり面倒くさい説明でしたが、CodeWarrior のデバッガ・シミュレータを動かしながらひとつひとつ確認していくと、とてもよく理解できたのではないかな・・・と思っています。
 

 次回は新しい話題で、「割り込み」 の予定です。


(*1) C言語の場合、main 関数を実行する前に C言語のプログラムが動けるような準備を行う処理が必要です。 その部分のプログラムをスタートアップ・ルーチンと呼びます。

(*2) HC08マイコンを含む多くのマイコンの場合、C言語プログラムをコンパイルしてできあがるアセンブリ言語のプログラム中では、関数の呼び出し処理がサブルーチン・コールとして展開されます。

 

 『参考文献』
「試しながら学ぶHC08マイコン入門」 (CQ出版)
  第1章  マイコン電子工作を始めよう
  第10章 統合開発環境CodeWarriorを使ってみる
筆者のホームページ 『マイコン工作の実験室』

組み込みエンジニア KAWANO

カテゴリ:

トラックバック

このエントリーのトラックバックURL:
http://www.eleki-jack.com/mt/mt-tb.cgi/3881

コメントを投稿

(いままで、ここでコメントしたことがないときは、コメントを表示する前にこのブログのオーナーの承認が必要になることがあります。承認されるまではコメントは表示されません。そのときはしばらく待ってください。)

カテゴリ

会社案内
情報セキュリティおよび個人情報の取り扱いについて

コメントとトラックバックは、spamを予防するために、編集担当が公開の作業をするまで非公開になっています。
コメントはそれぞれ投稿した人のものです。

Powered by
Movable Type 4.1