(実験)メモリのスタック領域を覗く

投稿者: | 2020年12月30日
皆さんこんにちは。

今回はgdbを使ってプロセスが持っているスタック領域のメモリを覗いてみようと思います。

これまでスタックという言葉は知っていて、「あぁ、あれだよね、あの質問サイトのやつ。」ぐらいのへっぽこ知識しかありませんでしたが、先日読了した「自作エミュレータで学ぶx86アーキテクチャ」の中で分かりやすく説明されてたので実際に自分の手でスタックが積まれていく様子などを確認してみようと思います。

●そもそもスタック領域って?
・私もうまく説明できませんが、プロセスが持つメモリ領域は「スタック、ヒープ、グローバル、定数、コード」などの領域が分かれているという前提がまずあります。そのうえで、malloc()などで動的にヒープ領域に確保されるメモリと違って、関数内のローカル変数などで一時的に利用されるメモリ領域ぐらいの認識でいます。(詳細はご自身でお調べください。。。)また、ヒープ領域と違ってプログラマがメモリの解放を行う必要がなくメモリリークなどの心配がない感じですかね。この辺のスタック破棄の動作回りも後ほど確認します。
(学校では教えてくれないこと)ヒープとスタック


●実験内容
1, C言語のサンプルソースを作成してgdbでスタックが積まれる様子を覗いてみる
    – サンプルソースではスタック領域にローカル変数として確保された変数の初期値を表示しています。
    – main()関数からtest_func()関数を3回実行して、実行するたびにスタックに確保された値を覗いていきます。
    – 予想:
        – 1回目: test_func()の初回呼び出し時に確保されたローカル変数は初期化されておらずゴミデータが見えるはず
        – 2回目: test_func()の2回目の呼び出し時は、初回呼び出し時にセットした値が残っているのが確認できるはず。関数が完了しスタック領域を破棄するときは、使用したメモリ領域を初期化せずにスタックポインタ(rsp)とベースポインタ(rbp)の移動のみで完結するため。
        – 3回目: 2回目と同じ

サンプルソースと実行結果

サンプルソースのアセンブリを出力

gdbでスタックの様子を確認する際に、サンプルソースのアセンブリがあったほうが動作を追いやすいので逆アセンブルした結果の一部を抜粋しておきます。

gdbを使ってスタックを覗く

それでは実際にgdbを使ってスタック領域を覗いていきたいと思います。
(ももいろテクノロジー)gdbの使い方のメモ
main()関数に入った直後のレジスタの値を確認しておこうと思います。 スタックポインタが「0x7fffffffe240」を指していることがわかります。
それではステップ実行していこうと思います。実行している間にスタックポインタが移動して、test_func()用のローカル変数が確保され行くはずです。 siを複数回実行すると、test_func()の中にやってきました。スタックポインタの値も「0x7fffffffe238」になって変化しています。
ここでちょっとした疑問ですが、「スタックが積まれるならメモリの番地は増えてるはずでは?」というものがあります。
実際はスタックは下方向に伸びていくので、スタック領域が増加すると、メモリ番地はマイナスされます。
なので前述のアセンブリにあるように、スタック領域を確保するときにはsub命令を使って、現在のスタックポインタから減算しています。
「sub rsp,byte +0x20」

何回かsiを実行していくとrspの値が-0x20されます。ここでtest_func()内でローカル変数に使う領域がスタックが積まれたので、初回実行時のゴミデータを覗いてみましょう。

実際に覗くメモリのアドレスは「rbp-0x14」になります。理由としては、前述したtest_func()アセンブリの中で「mov [rbp-0x14],edi」という処理で、ediに格納されているmain()関数からの引数を代入している処理がありからです。つまり、C言語の「i = val;」に対応する処理になり、代入先がローカル変数のアドレスということになります。 「0x7fffffffe21c: 0x00005555」という値が取得できました。これは10進数では
「21845」なので、初回実行時はこのゴミデータを表示して「before func:21845」という出力がされるはずです。

それではプログラムの実行を進めて出力を確認してみます。 想定通り、スタックに確保されたアドレスにあったゴミデータを初回実行時は表示しました。
続いて2回目以降のこのアドレスの値を確認していきます。2回目実行時も、main()関数から連続して同一関数の呼び出しを行っているので、初回と同じ位置にスタックが積まれていきます。

想定では、初回実行時に「i = val;」してるので、「4」が入っているはずです。
この動作は、関数終了後にスタックを破棄する際にrbpとrspの移動のみでメモリの値を初期化しているわけではないという動作確認にもなります。 想定通り「4」が入っていますね。
この後処理を進めると、2回目の引数の「8」で上書きされるはずです。 想定通り「8」が入っていますね。
3回目も同様の動作となるので動作確認は割愛します。


以上で今回の実験を終了としたいと思います。
私が勘違いしてるか箇所等あるかもなので疑問に思った点などはぜひご自身でも試してみると面白いかと思います。

今回はgdbを用いてプログラムのメモリ領域を確認してみました。ちゃんと使えると便利なツールなのでちょくちょく使う機会を増やしていこうかなーと思うところでした。(そうゆう機会はなかなかないけど。)

それではまた。