C言語のメモリ構成についての勉強

2025/02/09

C言語 プログラミング

最近時自動車整備の記事しか書いてなかったので、久しぶりプログラミング系の記事です。

Static変数の挙動について勉強する必要があったのですが、その際にそもそもメモリについて理解をしなければいけなかったため、今回はメモリについて勉強したことを備忘録として残しておきます。(間違っている内容があればご指摘お願いします。)


プログラムのメモリ構成について

何個かサイトを読んでみましたが、一般的なメモリの構成は下記のようになってそうです。(ただし、こちらの構成はプログラム実行中のメモリマップです。プログラムのメモリマップの場合はデバッグ情報が含まれてたりなど構成が異なります。詳細は後ほど確認します。)
参考文献[1][2]より

ここでそれぞれの領域の説明は下記のとおりです。
テキスト領域プログラムの機械語命令が置かれる領域.この領域は読み出し専用になっており,同じプログラムを実行しているプロセスの間で共有可能になっている.
データ領域
(初期値あり)
0以外の初期値を持つ大域変数や,static と指定された局所変数が置かれる領域.
データ領域
(初期値なし)
プログラムの BSS (Block Started by Symbol) と呼ばれる部分に対応する領域. 初期値を持たないまたは初期値が0の大域変数や,static と指定された局所変数が置かれる領域.プロセス生成時に確保され,0に初期化される.
ヒープ領域malloc 関数などにより,プロセスの実行中に確保されるデータ領域.
共有ライブラリ共有ライブラリを配置するための領域. ヒープとスタックの間にとられる. テキスト領域と同じく読み出し専用で,他のプロセスと共有される.
スタック領域static と指定されていない局所変数,関数引数,関数呼び出し時の戻り番地などが置かれる領域.
引数、環境変数プログラムに渡される引数と環境変数は,スタック領域の最上位部分に格納されている.
参考文献[1]より引用

また、上記文献に各変数が格納される領域についての説明があったため、紹介します。

グローバル変数が使用する領域

グローバル変数が使用する領域は、プログラム開始から終了まで確保されます。実行中に状態が変化しないことから静的な領域確保ともいうみたいです。
先ほども述べた通り、初期値を持つものはデータセグメントに置かれ、初期値を持たない(初期値が0)のものはBSSセグメントに置かれます。この時データセグメントの情報はプログラムファイルに残ります。(プログラムファイルについては参考として別で記載してます。)

これらの領域はプログラムの実行開始時に必要な大きさの領域が確保され、データセグメントの情報はプログラムファイルから読み出し、BSSセグメントの領域は全て0に初期化されます。

データセグメント及びBSSセグメントはプログラム開始から終了するまでメモリが開放されることが無く、開放することもできません。(あくまでメモリの開放が出来ないだけで値の変更はできることに注意)

開放できないという事で、使用されているか否かに関わらずメモリを確保してしまうので、設計する際に注意をした方がいい領域です。

また、これらの領域はプログラム開始時に確保されるため、逆説的にメモリが確保できない場合はプログラムを実行することが出来ません。

また、今回私が知りたかったStaticで確保される領域もこちらの領域で確保されます。

ローカル変数が使用する領域

ローカル変数が使用する領域は、宣言された関数が呼び出されてから呼び出し元に戻るまで有効です。なので、その関数が別の関数を呼び出した場合もその関数がreturnされるまでローカル変数は有効となります。
例えばfunc1で宣言されたローカル変数var1はfunc1から呼び出されるfunc2を実行中も有効なので、func2の引数にvar1へのポインタを渡しても問題ないです。逆にfunc1のreturnに&var1を返すことはfunc1のreturn後にその領域を解放してしまうため、出来ません。
緑の区間がvar1の生存区間

ローカル変数が使用する領域は先ほども紹介した通り、スタック領域になります。スタックは関数を呼び出す際に伸びていき、関数から戻る際に縮みます。(図でも紹介しましたが、上位アドレスから下位アドレスに向けて伸びていきます。)

別の関数を呼ばれる際にまたこの領域が伸び縮みするので、この領域はプログラム実行中にいろいろな関数が再利用する領域となります。

このスタック領域の確保のされ方はシステムによって異なるみたいで、必要な時に伸びるシステムが多いですが、サイズが固定されているシステムもあります。

ヒープ領域

ヒープ領域はプログラム実行開始後malloc()などで必要な時に確保され、不要になったら開放される領域となります。実行中にメモリが確保、開放されるため動的な領域確保という言い方もされます。(メモリの確保、開放は明示的に行う必要があります。)

実行時に確保されるため、確保された領域を指し示す方法はポインタしかないです。
※スタック領域も関数を呼び出されるたびに確保と開放をしているため、動的に確保しているように感じてしまうかもしれないですが、スタック領域はコンパイル時に関数開始位置からの相対的なオフセットでアドレスが決まるため、変数名を使って直接参照することが出来ます。

サンプルプログラムによる動作確認

参考文献にプログラム実行中の各領域を取得するサンプルプログラムがありました。そのサンプルプログラムを少し改造してメモリ構造がどうなっているか確認してみます。サンプルプログラムは下記の通りです。(ファイル名はmemory_map.cとしてます。)
#include <stdio.h>
#include <stdlib.h>

extern char **environ;

int data0;
int data1;
int data2;
int data3 = 10;
int data4 = 20;
int data5 = 30;

int main(int argc, char *argv[])
{
char c;
char c2;

printf("environ:\t%p\n", environ);//環境変数のアドレス
printf("argv:\t\t%p\n", argv);//(コマンドライン)引数のアドレス
printf("argv(value):\t\t%s\n", argv[1]);//(コマンドライン)引数の値
printf("stack0:\t\t%p\n", &c);//局所変数(ローカル変数)のアドレス(1つめ)
printf("stack1:\t\t%p\n", &c2);//局所変数(ローカル変数)のアドレス(2つめ)

printf("bss0:\t\t%p\n", &data0);//大域変数(グローバル変数)/初期値なしのアドレス(1つめ)
printf("bss1:\t\t%p\n", &data1);//大域変数(グローバル変数)/初期値なしのアドレス(2つめ)
printf("bss2:\t\t%p\n", &data2);//大域変数(グローバル変数)/初期値なしのアドレス(3つめ)
printf("data0:\t\t%p\n", &data3);//大域変数(グローバル変数)/初期値ありのアドレス(1つめ)
printf("data1:\t\t%p\n", &data4);//大域変数(グローバル変数)/初期値ありのアドレス(2つめ)
printf("data2:\t\t%p\n", &data5);//大域変数(グローバル変数)/初期値ありのアドレス(3つめ)

return EXIT_SUCCESS;
}
私があまりC言語に慣れていないため、コマンドライン引数の渡し方含めてあっているかを確認するためにコマンドライン引数の値を表示する箇所を追加してます。

上記プログラムのコンパイルが完了したら下記のようにコマンドライン引数を与えたうえで実行してみます。
.\memory_map.exe 1
実行結果は下記の通りになります。(実行環境によって異なると思うので、人によってはこの値になりません。)
environ:        00701BD8 
argv:           00701500 
argv(value):            1
stack0:         0061FF1F 
stack1:         0061FF1E                                         
bss0:           00407074
bss1:           00407070
bss2:           00407078
data0:          00404004
data1:          00404008
data2:          0040400C
これを先ほど書いた絵に追記すると以下のようになっていることが分かります。
上記図より以下のことが分かります。
  1. 参考文献に記載があった通り、環境変数のアドレスが一番大きく、初期値ありのデータ領域のアドレスが一番小さい
  2. 参考文献に記載があった通り、データ領域初期値ありとデータ領域初期値なしで確保されるメモリ領域が分かれて確保されている。
  3. スタック領域はアドレスが大きい順に確保していき、データ領域初期値ありはアドレスが小さい方から確保していく
  4. 初期値なしのデータ領域については確保されるメモリ領域が宣言順ではない。
1,2,3,4については調査情報と一致している気がするのですが、4について理由が分からなかったです。BSS領域はプログラムの実行時にカーネルが割り当てるという記載があったため、カーネルの挙動について理解が出来ればわかるのだと思いますが、今の自分にはまだレベルが高い話のため、一旦後回しにします。

続いて、先ほどのサンプルプログラムに下記記述を追加してみてStatic変数の動作についてもダメ確していきます。
         static char c3;
         static char c4=4;
         printf("static:\t\t%p\n", &c3);
         printf("static2:\t\t%p\n", &c4);
         

実行結果は下記のようになりました。
static:         00407020
static2:        00404010

ローカル変数であってもstaticを付けることで、グローバル変数と同じ領域に配置されることが分かります。(初期値のあり、なしによって配置される場所が変わることもグローバル変数と同じ挙動)

(参考)プログラムファイルの構成

参考文献[1]によるとプログラムファイルの構成は下記のようになっております。


今回は上記のようになっているか先ほどのプログラムをobjdumpを使ってみてみます。
私はwindows環境ですが、objdumpはWSLを使って実行しました。
WSLを開いて下記コマンドを入力します。
objdump -x memory_map.exe

ヘッダ情報

出力結果の最初の方にstartアドレスやコードのサイズなどの情報が記載されている箇所があると思います。こちらがヘッダ情報です。0x004012d0からヘッダ情報が開始していることが分かります。


テキスト領域、データ領域、デバッグ領域

まとめて記載してしまいますが、次はテキスト領域、データ領域、デバッグ領域です。出力結果の中でSectionと記載されている下記のようなものが出てくると思います。
text領域は分かりやすく0.textとなっているところだという事が分かります。データ領域は難しく.data, .rdata, .bss, .idataがデータ領域だと思われます。(.eh_frameは[3]によるとスタックを戻すために使用する呼び出しフレーム情報となっており、データ領域とは違う気がします。。。)

ちなみにデータ領域はたくさんありますが.dataは初期値を持つ変数を配置するためのセクションです。.rdataは読み取り専用のデータが配置されるセクションです。.bssは初期値を持たない(初期値0)の変数が配置されるセクションです。idataはインポートデータセクションでWindowのPortable Executableファイルフォーマットであるそうです。[4]

デバッグ領域は.debug_**となっているため分かりやすいと思います。デバッグ領域にはシンボル情報(変数名や関数名などの情報)などが含まれてそうです。

各領域の詳細は別途確認しますが、プログラムのファイル構成について概要を理解することが出来ました。

まとめ

今回は各宣言した変数たちがメモリ上どのように配置されるかを確認しました。

もう少し詳細を知りたいですが、調べ方すらわからない状態になってしまったので、この記事としては一旦ここまでとします。。。
調べ方が分かり次第追加で書きたいと思います。。。

参考文献

その他勉強のために目を通したサイトたち


自己紹介

はじめまして 社会人になってからバイクやプログラミングなどを始めました。 プログラミングや整備の記事を書いていますが、独学なので間違った情報が多いかもしれません。 間違っている情報や改善点がありましたらコメントしていただけると幸いです。

X(旧Twitter)

フォローお願いします!

ラベル

QooQ