Yabu.log

ITなどの雑記

C言語の復習その6メモリについて

メモリ管理について学びました。malloc,free,reallocなど

コンパイルするときに、知識不足によるコンパイルエラーがかなり減りました。*1 またコンパイルエラー時に、何が間違っているかすぐわかるようになり、なぜ動いているのか・動かないのかわからないというソースがかなり減りました。

mallocについて

ローカル変数との違い

  • ローカル変数は確保できる合計サイズに限りがある
  • mallocの方が柔軟。
  • staticをつけた変数はデータセグメントに格納される
  • ではローカル変数は?
    • スタック領域に確保されます
      • ある程度の領域を抑えてスタックのベースアドレスから、
        • 下に伸びる
        • 上に伸びる
      • みたいな感じで空き容量を埋めていく感じです。
  • mallocではヒープ領域にメモリを確保する

    • heapの上限はない?

      この「ヒープ領域」には上限が決められていません。p223より

      • スタックはコンパイル、リンク時に大きさが決まるらしい。
  • mallocは失敗するとNULL=(((void*)0))が帰ってくる*2

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
  • freeをしないとヒープの領域を食いつぶしてメモリリークが発生する
    • 厄介なバグの一つ
    • malloc()で確保した領域は必ずfreeせよ!!!

realloc

void *realloc(void *ptr, size_t size);

  • 一度mallocした領域の拡張を行うことができる
  • ただしmallocで確保したサイズより大きいデータを書き込んでも普通に実行できてしまった。
  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);

Linuxmallocmmapについて

liunuxではmalloc()は以下のような動作になっている

  • mmap()関数で似たようなことができる
    • mmap関数ではページ単位でメモリを獲得する
    • malloc関数ではバイト単位で管理する
      • 内部ではmmapシステムコールを使ってページ単位で確保し、それをプールしている
      • malloc呼び出し時にプールから指定バイト数分切り出して渡している

Linuxの仕組みより

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

[試して理解]Linuxのしくみ ~実験と図解で学ぶOSとハードウェアの基礎知識

hariboteOSでは?

memmann_alloc(カーネル用のmalloc)

こちらは MEMMAN_ADDRというmammann_allocで割り当てる領域の開始アドレスがdefineで(0x003c0000)に指定されています。

api_malloc(アプリ用のmalloc)

mallocで割り当てるアドレスがどこから始まるかをhariboteOS用の実行ファイル.hrbファイルに書きこまれています。 あまり自信がありませんが、これはデータセグメントのベースからのオフセットアドレスだと思います。

感想

メモリリークは大変だと思うが、 発生した際に時間をかければ、問題箇所の調査ぐらいはできるくらいの知識はついた気がする。

今まではメモリリークの調査など絶対できそうになかったが、最近は結構自信がついてきた。(まだやったことないけど)

ただし、調査に時間はすごくかかりそうだ。

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み

プログラミング学習シリーズ C言語改訂版 2 はじめて学ぶCの仕組み