Edited at
C++Day 21

C++のメモリーの話とストレージの有効期間の話をしようか


C++ Advent Calendar 2017

この記事はC++ Advent Calendar 2017 21日目の記事です

<< 20日目|C++テンプレートはコードジェネレータ || 22日目|C++のAllocatorとアロケータを使うcontainer >>

C++17の話であふれるかと思いきや意外とそうでもなかったですね。話題が広い。

遅刻してすみません。


はじめに

動的とか静的とかわかんない、という話から変数のスコープとかの話に前になった。

C++のメモリーの気持ちを考えたくなるまとめ - Togetterまとめ

というわけでその辺の話をしたい。

規格的に厳密な議論はこの記事では行わない。ボケボケとした脳みそでも40%くらい内容が理解できる記事を目指す。

ちなみにC言語には当てはまらない話もあるのでC言語について知りたい場合は自分で規格書を読むなり他のサイトに行ってほしい。

また特定のアーキテクチャに強く依存した話になるので、その辺を差し引いて読み進めてほしい。


C++規格:ストレージの種類


C++11: Syntax and Feature 3.7 ストレージ(storage duration)

規格上、ストレージという用語は正しくなく、ストレージの有効期間(storage duration)という用語が正しいのだが、本書では多くの箇所で、簡単にするため、単にストレージという言葉を使っている。


という言葉に従いストレージという言葉をここでも使う。

なおC言語にはストレージの有効期間という定義はなく、単にstorage-class-specifier(typedef,extern, static, _Thread_local(C11), auto, register)の定義のみある。

ちなみにこの項で述べるdynamic storageはSTLヘッダの<new>が必要だが、これはfreestanding環境でも存在しているので、このあと述べる4つの区分は、処理系がOS上か否かにかかわらず存在すると思われる。


static storage

struct Foo {

int x;
int y;
};
/*
* staticキーワードをつけて宣言したローカル変数
*/

void f() {
static int n;//これ
static Foo foo;//これ。クラス型でもいい
}
namespace detail {
template<typename T> T& put_on_static_storage() {
static T storage;//これ。template関数内でも同じ。
return storage;
}
}

/*
* staticキーワードをつけて宣言したデータメンバー
*/

struct Hoge {
static bool value;//これ
};

/*
* thread_localをつけずに宣言した名前空間スコープの変数
*/

int g_num1;//これ
static int g_num2;//これ。ここのstaticはリンゲージの指定だから関係ない
namespace {
int g_num3;//これ。無名名前空間にあっても同じ。
}
namespace hoge {
int num1;//これ。名前空間にあっても同じ。
static int num2;//これ。名前空間にあっても同じ。
}

static storageに該当する変数は上記に上げたものなどが該当する。

main関数から離脱した場合std::exitにその戻り値が渡されるが(return文省略時は0が戻り値)、std::exitの呼び出しのときに、static storageである変数の寿命が尽きる。この時static storageである変数がクラス型で破棄可能な場合デストラクタが呼び出される。なおstd::quick_exitが呼び出された場合はやはりstatic storageである変数の寿命が尽きるが、デストラクタは呼ばれない。

そのへんは

15秒で理解するmain関数からのreturnとexitとquick_exitとか

を参照してほしい。


thread storage

こいつはC11/C++11で追加された。だから知らない人もいるかもしれない。

あたりを参照してほしい。

#include <random>

thread_local std::mt19937 mt;
struct S
{
static thread_local int x ;
};

どこでもいいからthread_localがくっついているやつ全部。こいつらは断じてstatic storageではないから


staticローカル変数でthread_localを使っている。


とかいう謎な文章を作り出さないように。

スレッドの生成時に作成され、スレッドの終了時に破棄される。


automatic storage

まあふつう変数って言ったらこれだろ。

#include <iostream>

using inferior_find_if_cond_f = bool (*)(int);
std::size_t inferior_find_if(
int* arr,//これもautomatic storage
std::size_t n,//これもautomatic storage
inferior_find_if_cond_f cond//これもautomatic storage
){
std::size_t i;//これ
for(i = 0; i < n; ++i) if(cond(arr[i])) return i;
return i;
}
int main(){
int arr[5] = { 3, 4, 1, 2, 5 };//これ
std::cout << inferior_find_if(arr, 5, [](int n){ return n == 2; }) << std::endl;
}


dynamic storage

void* operator new(std::size_t);

void* operator new(std::size_t, std::align_val_t);//since C++17
void operator delete(void*);//until C++11
void operator delete(void*) noexcept;//since C++14
void operator delete(void*, std::size_t) noexcept;//since C++14
void operator delete(void*, std::align_val_t) noexcept;//since C++17
void operator delete(void*, std::size_t, std::align_val_t) noexcept;//since C++17
void* operator new[](std::size_t);
void* operator new[](std::size_t, std::align_val_t);//since C++17
void operator delete[](void*);//until C++11
void operator delete[](void*) noexcept;//since C++14
void operator delete[](void*, std::size_t) noexcept;//since C++14
void operator delete[](void*, std::align_val_t) noexcept;//since C++17
void operator delete[](void*, std::size_t, std::align_val_t) noexcept;//since C++17

C++17までで眺めると、C++には上記のnew/delete演算子がある。サイズ付き、align、noexcept周りでごちゃごちゃしているが。

dynamic storageとはこのnew/delete演算子によって作成/破棄される領域をさす。

驚く人もいるかもしれないが、C++がCから受け継いだ動的メモリ確保手段であるstd::aligned_alloc(C11/C++17),std::calloc,std::mallocおよびstd::realloc関数によって作成、std::freeによって破棄される領域はdynamic storageとは言わない。これは単にC規格書で定義されているものについてC++規格書で改めて言及するのを避けただけではないかと私は思っている(C++規格書はC規格書を参照しているので)。


処理系について

ここまでC++規格上の話をしてきたが、一旦離れて具体的な話をする。つまりはいくつかの処理系に偏った話になる。

規格の要件さえ満たせばどんな実装でもいいわけだが、もうすこし現実的な想定として、近代的なOSの上で動く、となると話が変わってくる。

近代的なOSはメモリーの管理を担当していて、ハードウェアと協調して仮想アドレス空間管理を行っており、

スタックオーバーフロー攻撃対策などのセキュリティ上の問題からDEP(データ実行防止)のような機能を実装していたり、

return-to-libc攻撃対策などのセキュリティ上の問題からASLR(アドレス空間配置ランダム化)を行ったりしている。

なにが言いたいかというとメモリーの割り当てや動的確保はOSの仕事だ、ということである。

多重仮想記憶の概念図

License: GFDL


実行ファイル:セクション

このあと述べるセグメントと混同しがちなのでここで。

実行ファイルのセクションとは、OSなどが実行可能なファイルのフォーマット(Windows:PE, Unix:elf)に含まれるもので、OSがそれを読み取って実行する際の必要なデータ(プログラム命令列、変数など)が区分されているものである[要出典]

Linuxであれば、

readelf -SW <実行ファイル名>

としてやると

There are 32 section headers, starting at offset 0x46f8:

Section Headers:
[Nr] Name Type Address Off Size ES Flg Lk Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 0000000000400270 000270 00001c 00 A 0 0 1
[ 2] .note.ABI-tag NOTE 000000000040028c 00028c 000020 00 A 0 0 4
[ 3] .gnu.hash GNU_HASH 00000000004002b0 0002b0 000044 00 A 4 0 8
[ 4] .dynsym DYNSYM 00000000004002f8 0002f8 000348 18 A 5 1 8
[ 5] .dynstr STRTAB 0000000000400640 000640 00045e 00 A 0 0 1
[ 6] .gnu.version VERSYM 0000000000400a9e 000a9e 000046 02 A 4 0 2
[ 7] .gnu.version_r VERNEED 0000000000400ae8 000ae8 000040 00 A 5 2 8
[ 8] .rela.dyn RELA 0000000000400b28 000b28 000048 18 A 4 0 8
[ 9] .rela.plt RELA 0000000000400b70 000b70 000228 18 AI 4 25 8
[10] .init PROGBITS 0000000000400d98 000d98 00001a 00 AX 0 0 4
[11] .plt PROGBITS 0000000000400dc0 000dc0 000180 10 AX 0 0 16
[12] .plt.got PROGBITS 0000000000400f40 000f40 000008 00 AX 0 0 8
[13] .text PROGBITS 0000000000400f50 000f50 001442 00 AX 0 0 16
[14] .fini PROGBITS 0000000000402394 002394 000009 00 AX 0 0 4
[15] .rodata PROGBITS 00000000004023a0 0023a0 00006b 00 A 0 0 4
[16] .eh_frame_hdr PROGBITS 000000000040240c 00240c 00009c 00 A 0 0 4
[17] .eh_frame PROGBITS 00000000004024a8 0024a8 0002b4 00 A 0 0 8
[18] .gcc_except_table PROGBITS 000000000040275c 00275c 0000e0 00 A 0 0 4
[19] .tbss NOBITS 0000000000602db0 002db0 000004 00 WAT 0 0 4
[20] .init_array INIT_ARRAY 0000000000602db0 002db0 000008 00 WA 0 0 8
[21] .fini_array FINI_ARRAY 0000000000602db8 002db8 000008 00 WA 0 0 8
[22] .jcr PROGBITS 0000000000602dc0 002dc0 000008 00 WA 0 0 8
[23] .dynamic DYNAMIC 0000000000602dc8 002dc8 000230 10 WA 5 0 8
[24] .got PROGBITS 0000000000602ff8 002ff8 000008 08 WA 0 0 8
[25] .got.plt PROGBITS 0000000000603000 003000 0000d0 08 WA 0 0 8
[26] .data PROGBITS 00000000006030d0 0030d0 00006c 00 WA 0 0 16
[27] .bss NOBITS 0000000000603140 00313c 0000c8 00 WA 0 0 16
[28] .comment PROGBITS 0000000000000000 00313c 000061 01 MS 0 0 1
[29] .shstrtab STRTAB 0000000000000000 0045e0 000111 00 0 0 1
[30] .symtab SYMTAB 0000000000000000 0031a0 000ae0 18 31 58 8
[31] .strtab STRTAB 0000000000000000 003c80 000960 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), l (large)
I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
O (extra OS processing required) o (OS specific), p (processor specific)

のような情報が得られる。このreadelfの読み方については

を見てもらうとして、とにかくこの例では32個のセクションが存在している。

ものすごく雑な説明をすると、.textにはプログラム命令列、.rdataには文字列リテラルと定数、.bssにはstatic storageの変数のうち初期値が明示的に0指定されている/無指定なために0になるものが含まれ、.dataにはそれ以外のstatic storageの変数、.tbssにはthread storageの変数のうち前述の通り0になるもの、.tdataにはそれ以外のthread storageの変数が入る。


実行時メモリー空間:セグメント

セグメントというとFarポインタとNearポインタとかのセグメントを思い出す人もいるようだが、ここではメモリー空間の用途を仮想的に区分したものを指す。

多重仮想記憶の概念図

License: GFDL

セグメントは物理メモリー上の位置とその長さ、そして権限を持つ。権限というのは、例えば


  • 実行可能か

  • 書き込み可能か

  • 読み出し可能か

と言った物がある。segmentation faultに代表されるようなハードウェア例外は、これを犯した時に発生する。

C/C++のプログラムのデータのうち何がどこに配置されるかは処理系に大きく依存するが、プログラム命令列は実行可能でかつ書き込み不可能なセグメントに置いてプログラム命令列を実行中に書き換えられないように保護している可能性は十分にある、というかそうなっていると思うべきだ。

また定数の類でかつメモリー上に実体を持つものは書き込み不可能なセグメントに置かれている可能性が高い。つまり例えば文字列リテラルがそういうセグメントに配置された場合、それを書き換えようとすると、segmentation faultのようなハードウェア例外が飛ぶ可能性が高い。

そういえばARMは書き込み専用のセグメントを持っているみたいな話を聞いたことがあるんだが、本当だろうか?何に使うんだろうか。

こういうセグメントの管理をメモリー管理装置(MMU)と呼び、普通ハードウェア実装される。


まとめ

C/C++でのstorageと実行ファイルのセクションやらメモリーセグメントは一対一対応するわけではないものの、なんとなく何がどこに配置されそうなのかイメージが湧いてきた。


License

CC BY-SA 3.0

CC BY-SA 3.0