C言語のメモリ環境について

目次
1.メモリ領域の違い
1.1 各メモリの割り当て状況を確認する
2.スタックメモリの確認方法
3.ヒープメモリの確認
(1)動的メモリのデバッグツール:Valgrind(ヴァルグリンド、GNU General Public License)
(2)プロセスのメモリマッピングの確認
(3)/procファイルシステムの使用
(4)ランタイムライブラリの関数
4.Linuxのメモリマップ設定

1.メモリ領域の違い

メモリ領域          特徴
スタック領域・関数呼び出しやローカル変数の割り当てに使われる。・後入れ先出し(LIFO)のデータ構造をもつ。
ヒープ領域・プログラマが明示的にメモリの確保(C:malloc 関数、C++:new演算子)と解放(freeなど)を行う必要がある。・動的にメモリを確保・開放するのに適したデータ構造をもつ。
静的領域          ・グローバル変数などの静的変数が置かれる。
テキスト領域・機械語に翻訳されたプログラムが格納され、この機械語の命令が1行づつ実行されることでプログラムが動く。

(参考)
・メモリの4領域
https://brain.cc.kogakuin.ac.jp/~kanamaru/lecture/MP/final/part06/node8.html

1.1 各メモリの割り当て状況を確認する
(1)テキスト領域
 テキスト領域(プログラムコードが格納される領域)は、通常、実行ファイルの読み込みアドレスを確認することで確認できます。以下は、テキスト領域の開始アドレスを取得する方法です。

ここで、”(void)main”の “(void)”はvoidは、どんなデータ型にもキャスト可能なポインタ型です。これは、特定のデータ型を指さない汎用ポインタであり、さまざまな型のデータを指すことができます。voidは型情報を持たないため、キャストを間違えると意図しない動作を引き起こす可能性があり、ポインタ演算など行うときは正しいデータ型にキャストするよう注意が必要です。

(2)静的領域
 静的領域(グローバル変数や静的変数が格納される領域)のアドレスを取得するには、静的変数やグローバル変数のアドレスを確認します。

(3)スタック領域
 スタック領域のメモリ割り当ては、ローカル変数のアドレスを確認することで確認できます。

(4)ヒープ領域
 ヒープ領域のメモリ割り当ては、malloc関数で動的に割り当てたメモリのアドレスを確認することで確認できます。

各種類の領域をまとめて表示

2.スタックメモリの確認方法
(1)GDB (GNU Debugger)の使用

(参考)
・gdbのコマンド一覧
https://www.fos.kuis.kyoto-u.ac.jp/le2soft/siryo-html/node49.html

gdbの主なコマンド

コマンド名省略型動作
run [args]rプログラムを(引数argsで)実行する
break nb nブレークポイントの設定、n行目又は関数名
deleated nnのブレークポイントを削除
info breakpointsi bブレークポイントの一覧
continuecプログラムの実行を再開
stepsステップ・イン実行
backtrace btバックトレースの表示
quitqgdbの終了

(2)プロセスリミットの確認
 Unix系OSでは、ulimitコマンドでプロセスのスタックサイズリミットを確認できます。
 ulimit -s コマンドを実行して 8192 と表示される場合、それはスタックサイズの制限をキロバイト単位で表しています。具体的には、現在のシェルセッションにおけるスタックサイズの制限が 8192 キロバイト(8 メガバイト)であることを示します。
 スタックサイズが小さいと、深い再帰や大量のローカル変数を使用するプログラムはスタックオーバーフローを引き起こす可能性があります。

 スタックサイズは次のシェルを実行することで16メガバイトにできます。
 $ ulimit -s 16384
 システム上で永続的に設定するときは、システムの設定ファイル(例えば /etc/security/limits.conf)を変更する必要があります(この設定ファイルには該当箇所はありませんでした)。

3.ヒープメモリの確認
(1)動的メモリのデバッグツール:Valgrind(ヴァルグリンド、GNU General Public License)
 Valgrindは、メモリリークや不正なメモリアクセスを検出するための動的解析ツールの一つです。Valgrindを使用してプログラムを実行すると、ヒープメモリの使用状況が詳細に報告されます。

①Valgrindの概要
 Memcheck:Valgrindの一部で、メモリエラーの検出を行います。
 Copyright:Valgrindの著作権情報。
 Version:使用しているValgrindのバージョン。
 Command:実行されたコマンド(ここでは ./a.out )。
②プログラムの出力
 プログラム自体の出力でfunc2関数からの出力。
③HEAP SUMMARY(ヒープの概要)
・in use at exit:プログラム終了時に使用中のメモリの量。ここでは、0バイトが0ブロックに使用されていることを示しています。つまり、全てのメモリが解放されています。
・total heap usage:プログラムの実行中に行われたメモリ割り当て(allocs)と解放(frees)の総数、および割り当てられたメモリの総量。
・1 allocs:1回のメモリ割り当てが行われた。
・1 frees:1回のメモリ解放が行われた。
・1,024 bytes allocated:合計で1,024バイトのメモリが割り当てられた。
④メモリリークの確認
・All heap blocks were freed:すべてのヒープメモリブロックが解放されたことを示します。
・no leaks are possible:メモリリークがないことを意味します。
⑤エラーの概要
・0 errors from 0 contexts:検出されたエラーはゼロであることを示します。
・suppressed: 0 from 0:抑制されたエラーはないことを示します。

 このValgrindの出力から、次のことがわかります:
・プログラムは正常に実行され、出力も予期されたものでした。
・メモリ使用に関しては、メモリリークは発生していません。
・割り当てられたメモリはすべて適切に解放されています。
・検出されたメモリエラーはありません。
 この結果から、プログラムはメモリ管理に関して健全であると言えます。Valgrindを使用することで、メモリリークやその他のメモリエラーを検出し、修正することができます。

(参考)
・Valgrind による動的解析
https://qiita.com/QGv/items/d7857cb0fff80ecbe41d#valgrind%E3%81%A8%E3%81%AF

(2)プロセスのメモリマッピングの確認:
 Linuxでは、pmapコマンドを使ってプロセスのメモリマッピングを確認できます。ヒープ領域のサイズと位置を特定することができます。
メモリを動的に割り当てる簡単なCプログラムを用意します。

ヒープ領域の詳細な確認
詳細なメモリ使用情報を表示するために、pmapコマンドに-xオプションを使用

出力の中に、[ heap ] というラベルは表示されない場合、[ anon ] として表示される行がヒープ領域を示すことが多いとのこと。
 上記の例では、
 00007fe5066f0000 10244 10244 10244 rw— [ anon ]
アドレスにある 10MB(10244KB)の匿名メモリがヒープ領域です。

(3)/procファイルシステムの使用:
 /proc/[pid]/maps:
 Linuxでは、/procファイルシステムを使ってプロセスのメモリマップを確認できます。ヒープ領域は通常、heapとして表示されます

(4)ランタイムライブラリの関数:mallinfo:
 mallinfo関数は、ヒープメモリの統計情報を提供します。例えば、以下のコードでヒープメモリの使用状況を表示できます。

実行結果

c_heaptestを実行し、入力待ちになっている状態で、mallinfo()を実行

4.Linuxのメモリマップ設定
 Linuxでは、スタックのサイズはulimitコマンドで確認および設定できます

・標準のリンカスクリプト
 GCCがデフォルトで使用するリンカスクリプトは、インストールされたツールチェーンのディレクトリにあります。標準スクリプトの場所を見つけるには、以下のコマンドを使用して標準のリンカスクリプトを表示することができます

実際のプログラムをコンパイルしたとき、どのようにメモリに配置されるか確認します。

ここで、
・VMA(Virtual Memory Address)
 VMAは、セクションが実行時に仮想メモリ空間内で占めるアドレスを示します。プログラムが実行されるときに、CPUがこのアドレスを使用してメモリにアクセスします。
例えば、コードセクション(.text)やデータセクション(.data)の実行時のアドレスはVMAで決まります。VMAは、プログラムが実行されるときに、オペレーティングシステムが各セクションをマッピングするアドレスです。
・LMA(Load Memory Address)
 LMAは、セクションが実際にメモリにロードされるアドレスを示します。特に、初期化データセクション(.data)やBSSセクション(.bss)のようなセクションで重要です。
LMAは、プログラムがディスクからメモリにロードされるときに、セクションが配置される物理メモリのアドレスです。
・VMAとLMAの違い
 VMAは実行時のアドレス、LMAはロード時のアドレスです。
 通常、コードセクション(.text)や読み取り専用データセクション(.rodata)では、VMAとLMAが同じです。しかし、初期化データセクション(.data)や未初期化データセクション(.bss)では異なることがあります。例えば、.dataセクションはROMからRAMにコピーされる場合、ROM上のアドレスがLMAであり、RAM上のアドレスがVMAです。

(参考)
・objdump – オブジェクトファイルの情報を表示する
https://linuxcommand.net/objdump/#_-h

(2)組み込みシステムやカスタムOSでのメモリマップ設定
 組み込みシステムやカスタムOSの場合、メモリマップはリンクスクリプト(例えば、GNU linkerの.ldファイル)によって設定されます。
・MEMORYセクション:使用可能なメモリ領域を定義します。
 各メモリ領域の開始アドレス(ORIGIN)とサイズ(LENGTH)を指定します。
・SECTIONSセクション:セクションの配置を定義します。
 各セクションがどのメモリ領域に配置されるかを指定します。

(参考)
・【 ulimit 】コマンド――ユーザーが使用できるリソースを制限する
https://atmarkit.itmedia.co.jp/ait/articles/1908/01/news031.html

・GNU Cを使いこなそう2017/06/30
リンカスクリプトを理解しよう
https://www.computex.co.jp/article/use_gcc_1.htm