C言語の復習その6メモリについて
メモリ管理について学びました。malloc,free,reallocなど
コンパイルするときに、知識不足によるコンパイルエラーがかなり減りました。*1 またコンパイルエラー時に、何が間違っているかすぐわかるようになり、なぜ動いているのか・動かないのかわからないというソースがかなり減りました。
mallocについて
ローカル変数との違い
- ローカル変数は確保できる合計サイズに限りがある
- mallocの方が柔軟。
- staticをつけた変数はデータセグメントに格納される
- ではローカル変数は?
- スタック領域に確保されます
- ある程度の領域を抑えてスタックのベースアドレスから、
- 下に伸びる
- 上に伸びる
- みたいな感じで空き容量を埋めていく感じです。
- ある程度の領域を抑えてスタックのベースアドレスから、
- スタック領域に確保されます
mallocではヒープ領域にメモリを確保する
- heapの上限はない?
この「ヒープ領域」には上限が決められていません。p223より
- スタックはコンパイル、リンク時に大きさが決まるらしい。
- heapの上限はない?
mallocで戻ってきたアドレスを紛失しないこと
ローカル変数でmallocの返値アドレスを確保して、スコープを抜けるなどで噴出すると、 プログラム実行終了までに確保したアドレスは回収されない。
javaのようなメソッドエリア(静的領域とも)からの参照がなくなった参照を自動的に解放対象にする(ガベージコレクション)のような仕組みはない。
対策としては、
- アドレスを管理している変数を関数の返値として呼び出し元に返す
- グローバル変数で管理する
などがある。
free()について
- mallocで確保した引数を与えるとそこを解放する
void free(void *ptr);
- 宣言を見る限り
void*
->char
のキャストはもちろんできるが、char
->void*
のキャストも可能なのか。
- mallocで確保していないアドレスや解放済みのアドレスをfree()に渡すと実行時エラーになる
- コンパイル時にわかりそうなもんだが。。。
p = (char *) malloc(strlen(str)+1); void* vp = (void*) p; free(p); //free(vp); // 間違えて、同じアドレスを指している変数を2回freeしてしまった。 // その時のエラーを記念に残しておく // a.out(20725,0x7fffad00f380) malloc: *** error for object 0x7f8ebd402710: pointer being freed was not allocated // *** set a breakpoint in malloc_error_break to debug // Abort trap: 6 char localv = 'c'; //free(&localv); //ローカル変数をfreeした結果。 // a.out(20787,0x7fffad00f380) malloc: *** error for object 0x7ffee5ed7ab7: pointer being freed was not allocated // *** set a breakpoint in malloc_error_break to debug // Abort trap: 6
realloc
void *realloc(void *ptr, size_t size);
char *str1 = "hello"; char *str2 = " world"; p = malloc(strlen(str1)+1); strcpy(p,str1); puts(p);//hello //なぜか入れても抜いても同じ結果になる。 //p = realloc(p,strlen(str1)+ strlen(str2) +1); strcat(p,str2); puts(p);//hello world; free(p);
- 確保した領域の後ろのアドレスが空で、たまたま使えていたということ?
保証されていない動作?ということだろうか。
mallocを2回続けて、1回目の領域に大きなデータを書いてみたが、バグらせることができなかった。
reallocやらないとどうなるか。
複数回連続でmallocをして確保したアドレスを
printf()
してみたが、アドレスは綺麗に順番で確保されているわけではないようだ以下は複数char型のポインタ変数のアドレスを確保して実験したソース
char *vp1 = malloc(1); char *vp2 = malloc(1); char *vp3 = malloc(1); char *vp4 = malloc(1); char *vp5 = malloc(1); char *vp6 = malloc(1); //1バイトしか確保していない領域に複数バイト分の文字を書き込み //文字列リテラルはそれ自体が静的データへのアドレスなので以下のコードは実験としてふさわしくない //vp1 = "test"; strcpy(vp1,"testtesttesttesttesttesttesttest"); strcpy(vp2,"computercomputercomputercomputer"); strcpy(vp3,"abcdeabcdeabcdeabcdeabcdeabcde"); strcpy(vp4,"englishenglishenglishenglishenglish"); strcpy(vp5,"boyboyboyboyboyboyboyboyboyboy"); strcpy(vp6,"girlgirlgirlgirlgirlgirlgirlgirlgirl"); puts(vp1); puts(vp2); puts(vp3); puts(vp4); puts(vp5); puts(vp6); //結果:データ破壊ができた // bcdeabcdeabcde // computercomputerabcdeabcdeabcdeabcdeabcdeabcde // abcdeabcdeabcdeabcdeabcdeabcde // englishenglishenboyboyboyboyboybgirlgirlgirlgirlgirlgirlgirlgirlgirl // boyboyboyboyboybgirlgirlgirlgirlgirlgirlgirlgirlgirl // girlgirlgirlgirlgirlgirlgirlgirlgirl printf("vp1 addr is %p\n",vp1); printf("vp1 value is %c\n",*vp1); printf("vp2 addr is %p\n",vp2); printf("vp2 value is %c\n",*vp2); printf("vp3 addr is %p\n",vp3); printf("vp3 value is %c\n",*vp3); printf("vp4 addr is %p\n",vp4); printf("vp4 value is %c\n",*vp4); printf("vp5 addr is %p\n",vp5); printf("vp5 value is %c\n",*vp5); printf("vp6 addr is %p\n",vp6); printf("vp6 value is %c\n",*vp6); //出力 // vp1 addr is 0x7fc072402730 // vp1 value is b // vp2 addr is 0x7fc072402710 // vp2 value is c // vp3 addr is 0x7fc072402720 // vp3 value is a // vp4 addr is 0x7fc072402750 // vp4 value is e // vp5 addr is 0x7fc072402760 // vp5 value is b // vp6 addr is 0x7fc072402770 // vp6 value is g
- 値の欠損、上書きなどが起こったが、想定したものと微妙に違う。
- mallocは10バイト間隔を開けてアドレスを確保している
- mallocが確保するアドレスはアドレスの並び順通りではない
- 上の例だと
vp1 addr is 0x7fc072402730
vp2 addr is 0x7fc072402710
vp3 addr is 0x7fc072402720
vp4 addr is 0x7fc072402750
- 最近のコンパイラはバッファオーバーフロー対策でこういう細工がしてあるということを少し聞きかじったことがあるが、もしかしてそれなのでしょうか。
- コンパイラのオプションでこの細工を無効化できたはずだが。
realloc実践
確保した領域より大きい文字列を書き込む前に、reallocすることで適切な動作を実現できた。
char *vp1 = malloc(1); char *vp2 = malloc(1); char *vp3 = malloc(1); char *vp4 = malloc(1); char *vp5 = malloc(1); char *vp6 = malloc(1); char* str1 = "testtesttesttesttesttesttesttest"; char* str2 = "computercomputercomputercomputer"; char* str3 = "abcdeabcdeabcdeabcdeabcdeabcde"; char* str4 = "englishenglishenglishenglishenglish"; char* str5 = "boyboyboyboyboyboyboyboyboyboy"; char* str6 = "girlgirlgirlgirlgirlgirlgirlgirlgirl"; vp1 = realloc(vp1,strlen(str1)+1); vp2 = realloc(vp2,strlen(str2)+1); vp3 = realloc(vp3,strlen(str3)+1); vp4 = realloc(vp4,strlen(str4)+1); vp5 = realloc(vp5,strlen(str5)+1); vp6 = realloc(vp6,strlen(str6)+1); strcpy(vp1,str1); strcpy(vp2,str2); strcpy(vp3,str3); strcpy(vp4,str4); strcpy(vp5,str5); strcpy(vp6,str6); puts(vp1); puts(vp2); puts(vp3); puts(vp4); puts(vp5); puts(vp6); printf("vp1 addr is %p\n",vp1); printf("vp1 value is %c\n",*vp1); printf("vp2 addr is %p\n",vp2); printf("vp2 value is %c\n",*vp2); printf("vp3 addr is %p\n",vp3); printf("vp3 value is %c\n",*vp3); printf("vp4 addr is %p\n",vp4); printf("vp4 value is %c\n",*vp4); printf("vp5 addr is %p\n",vp5); printf("vp5 value is %c\n",*vp5); printf("vp6 addr is %p\n",vp6); printf("vp6 value is %c\n",*vp6); //出力 // testtesttesttesttesttesttesttest // computercomputercomputercomputer // abcdeabcdeabcdeabcdeabcdeabcde // englishenglishenglishenglishenglish // boyboyboyboyboyboyboyboyboyboy // girlgirlgirlgirlgirlgirlgirlgirlgirl // vp1 addr is 0x7fa623402770 // vp1 value is t // vp2 addr is 0x7fa6234027a0 // vp2 value is c // vp3 addr is 0x7fa6234027d0 // vp3 value is a // vp4 addr is 0x7fa623402810 // vp4 value is e // vp5 addr is 0x7fa6234027f0 // vp5 value is b // vp6 addr is 0x7fa623402710 // vp6 value is g
void型のポインタ
- mallocの結果はvoid型のポインタとして帰ってくる
void *malloc(size_t size);
- これは任意のポインタ型に代入(暗黙の型変換を伴う)可能になっている
- もちろん明示的にキャストすることも可能
char *p; char *str = "Getting memories!!"; p = (char *) malloc(strlen(str)+1); //キャストしなくてもOK //p = malloc(strlen(str)+1); void* vp = (void*) p; printf("void pointer type size is %d\n",sizeof vp); strcpy(p,str); puts(p); //vp型のポインタ変数もputsに渡せるようだ。 puts(vp); //putsは引数にchar型のポインタをとるので、渡った先で暗黙の型変換がされているものと思われる //int puts(char * s);
Linuxのmallocとmmapについて
liunuxではmalloc()
は以下のような動作になっている
- mmap()関数で似たようなことができる
Linuxの仕組みより
[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識
- 作者: 武内覚
- 出版社/メーカー: 技術評論社
- 発売日: 2018/02/23
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (1件) を見る
hariboteOSでは?
memmann_alloc(カーネル用のmalloc)
こちらは MEMMAN_ADDRというmammann_allocで割り当てる領域の開始アドレスがdefineで(0x003c0000)に指定されています。
api_malloc(アプリ用のmalloc)
mallocで割り当てるアドレスがどこから始まるかをhariboteOS用の実行ファイル.hrbファイルに書きこまれています。 あまり自信がありませんが、これはデータセグメントのベースからのオフセットアドレスだと思います。
感想
メモリリークは大変だと思うが、 発生した際に時間をかければ、問題箇所の調査ぐらいはできるくらいの知識はついた気がする。
今まではメモリリークの調査など絶対できそうになかったが、最近は結構自信がついてきた。(まだやったことないけど)
ただし、調査に時間はすごくかかりそうだ。
プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み
- 作者: 倉薫
- 出版社/メーカー: 翔泳社
- 発売日: 2009/02/13
- メディア: 大型本
- 購入: 2人 クリック: 6回
- この商品を含むブログ (3件) を見る