LoginSignup
39
26

More than 5 years have passed since last update.

mallocの動作を追いかける(main_arenaとsbrk編)

Last updated at Posted at 2017-11-04

はじめに

mall0c動画見てますか?>挨拶

Twitterで「malloc動画」で検索かけると、眠れない夜に見ると良いとか、健康に良いとかいろいろ出てきて、健康に良い上にmallocのこともわかるなんて、もう見ない理由はないですね。

というわけで、今回はmallocにおけるmain_arenaとsbrkの振る舞いを追います。

main_arenaとsbrkの動作を追いかける

gdbで追いかける

mallocは、メモリをアリーナ(arena)という単位で管理している。その管理に使われるのがmalloc_state構造体。普通はアリーナは一つだけで、それにmain_arenaという名前がついており、グローバル変数として宣言されている。malloc_state構造体の定義は例えばこちらを参照。

さて、プログラム実行開始直後はmain_arenaは何も管理していない、まっさらな状態になっている。最初にmallocが呼ばれた時、sbrkを呼ぶことでプログラムのヒープを拡張し、それで得たメモリプールからチャンクを切り取って、ユーザに返す。まずはそのあたりの振る舞いを見てみよう。

こんなコードを書いてみる。

test1.cpp
#include <cstdio>
#include <cstdlib>
int
main(void){
  char *buf=(char*)malloc(1);
  free(buf);
}

なんの変哲もない、ただmallocで1バイトを要求して、それをfreeするだけのコードであるが、このコードを実行すると、

  1. 初めてのmalloc実行なのでsrbkによりヒープ拡張が起きて
  2. 得たアドレスの最初をチャンクとして切り出し、
  3. ヘッダ部を除いた部分をユーザに返し、
  4. freeする時に、チャンクをfastbinsに登録する

という、およそmallocに必要な種々のことが起きるので動作確認には都合が良い。これを-gつきでコンパイルして、gdbで動作を追いかけよう。

まずはmainにブレークポイントを置いて、そこまで実行する。

$ g++ -g test1.cpp
$ gdb -q ./a.out
(gdb) break main
Breakpoint 1 at 0x4006b8: file test1.cpp, line 5.
(gdb) r
Starting program: /path/to/a.out 
Breakpoint 1, main () at test1.cpp:5
5     char *buf=(char*)malloc(1);
(gdb) 

この状態で、main_arenaにアクセスできる。ただし、今は中身は空っぽである。中身を表示させてみよう。

(gdb) p/x &main_arena
$1 = 0x2aaaab813e80
(gdb) x/24xw &main_arena
0x2aaaab813e80 <main_arena>:    0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813e90 <main_arena+16>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ea0 <main_arena+32>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813eb0 <main_arena+48>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ec0 <main_arena+64>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ed0 <main_arena+80>: 0x00000000  0x00000000  0x00000000  0x00000000

こんな感じで、main_arena構造体のアドレスは0x2aaaab813e80であり、中身は全部ゼロであることがわかる。

さて、このあと最初のmallocが呼ばれるため、sbrkによりヒープ拡張が行われる。sbrkとは、program break、つまりプログラムの一番最後(データセグメントの最後尾)を拡張するためのシステムコールである1。brkが位置の変更、sbrkがサイズを増やす要求をするシステムコールで、同名の関数が用意されている。とりあえずsbrkにブレークポイントを置いて続行してみよう。

(gdb) break sbrk
Breakpoint 2 at 0x2aaaab5728e0
(gdb) c
Continuing.

Breakpoint 2, 0x00002aaaab5728e0 in sbrk () from /lib64/libc.so.6
(gdb) 

sbrkにひっかかった。バックトレースも表示してみる。

(gdb) bt
#0  0x00002aaaab5728e0 in sbrk () from /lib64/libc.so.6
#1  0x00002aaaab51c179 in __default_morecore () from /lib64/libc.so.6
#2  0x00002aaaab51896f in _int_malloc () from /lib64/libc.so.6
#3  0x00002aaaab51a1f7 in malloc () from /lib64/libc.so.6
#4  0x00000000004006c2 in main () at test1.cpp:5

malloc内部から呼ばれていることがわかる。

さて、sbrkは、引数として、ヒープを拡張するサイズを取る。したがって、sbrkにブレークポイントを置いて、ひっかかったら引数の値を表示させれば、mallocがどれくらいヒープ拡張を要求したかがわかる。第一引数は%rdiに入っている。

(gdb) p/x $rdi
$2 = 0x21000

0x21000(135168)バイト要求しているようだ。

さて、malloc実行直後まで続けよう。

(gdb) n
Single stepping until exit from function malloc,
which has no line number information.
main () at test1.cpp:6
6     free(buf);

mallocした直後まで来た。bufにどんなアドレスが入っているか調べる。

(gdb) p/x buf
$3 = 0x602010

bufには0x602010というアドレスが返された。したがってチャンクの先頭アドレスは0x602000である。

この状態で、再度main_arenaの中身を表示してみよう。

(gdb) x/24xw &main_arena
0x2aaaab813e80 <main_arena>:    0x00000000  0x00000001  0x00000000  0x00000000
0x2aaaab813e90 <main_arena+16>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ea0 <main_arena+32>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813eb0 <main_arena+48>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ec0 <main_arena+64>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ed0 <main_arena+80>: 0x00000000  0x00000000  0x00602020  0x00000000

いくつかゼロでない値が入ったことがわかる。上記の表示対応する形で、malloc_state構造体の最初の方を図示するとこんな感じ。

main_arena.png

まず、0x00000001はフラグ(main_arena->flags)であり、fastbinsにチャンクがあるかどうかを示す。その後fastbinYの配列が10個続くが、本稿では詳細を説明しない。0x00602020はmain_arena->topの値であり、次にメモリ要求があった場合に切り出す位置を示す。

さて、さらにプログラムを進め、free直後の状態を見てみる。

(gdb) n
7   }
(gdb) x/24xw &main_arena
0x2aaaab813e80 <main_arena>:    0x00000000  0x00000000  0x00602000  0x00000000
0x2aaaab813e90 <main_arena+16>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ea0 <main_arena+32>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813eb0 <main_arena+48>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ec0 <main_arena+64>: 0x00000000  0x00000000  0x00000000  0x00000000
0x2aaaab813ed0 <main_arena+80>: 0x00000000  0x00000000  0x00602020  0x00000000

先程1が入っていたflagsが0となり、fastbins対象のチャンクが存在することを示している。2次に、fastbinsの一番小さいメモリのビンに、先程のチャンクアドレス0x00602000が登録された。ヒープのトップは相変わらず0x00602020である。

プログラムから追いかける

先程みたいに全部gdbで追いかけても良いが、今後いちいちアドレス指定して値を読むものタルいので、malloc_state構造体を定義して使おう。あとで使いまわすためにヘッダにしてみよう。

malloc.h
struct malloc_state{
  int mutex;
  int flags;
  size_t *fastbinsY[10];
  size_t *top;
};
typedef malloc_state* mstate;
mstate main_arena = (mstate)0x2aaaab813e80;

いろいろ手抜きをしているが、とりあえずの目的にはこの程度で十分。最後にmain_arenaを先程gdbで見たアドレス決め打ちでゲットしている。もちろん、アドレス空間が毎回ランダマイズされるようなOSでこの手は使えない。

これを使ってこんなコードを書いてみる。

test2.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "malloc.h"
int
main(void){
  printf("---start program---\n");
  void *p = sbrk(0);
  printf("program break: 0x%lx\n",p);
  printf("---call malloc---\n");
  char *buf=(char*)malloc(1);
  p = sbrk(0);
  printf("program break: 0x%lx\n",p);
  printf("buf address: 0x%lx\n",buf);
  printf("chunk top: 0x%lx\n",buf-0x10);
  size_t csize = (size_t)(*(buf-8));
  csize ^= 1;
  printf("chunk size: 0x%lx\n",csize);
  printf("main_arena->fastbinsY[0]:0x%lx\n",main_arena->fastbinsY[0]);
  printf("main_arena->top:0x%lx\n",main_arena->top);
  printf("---call free---\n");
  free(buf);
  p = sbrk(0);
  printf("program break: 0x%lx\n",p);
  printf("main_arena->fastbinsY[0]:0x%lx\n",main_arena->fastbinsY[0]);
  printf("main_arena->top:0x%lx\n",main_arena->top);
}

実行するとこんな感じになる。

$ g++ test2.cpp   
$ ./a.out 
---start program---
program break: 0x602000  // 1.
---call malloc---
program break: 0x623000  // 2.
buf address: 0x602010    // 3.
chunk top: 0x602000      // 4.
chunk size: 0x20         // 5.
main_arena->fastbinsY[0]:0x0  // 6.
main_arena->top:0x602020 // 7.
---call free---
program break: 0x623000  // 8.
main_arena->fastbinsY[0]:0x602000  //9. 
main_arena->top:0x602020 // 10.

sbrkは、成功すると前回のプログラムブレーク、つまりヒープの最後尾を返す。したがって、要求サイズ0で実行すると、現在のヒープの最後尾のアドレスを返してくれる。それを使ってヒープの最後尾をモニタできる。

実際のプログラムの動作は以下の通り。

  1. プログラム開始直後、ヒープの最後尾は0x602000になっている。
  2. mallocを実行すると、ヒープが拡張されて、位置が0x623000になった(先程gdbで確認したとおり、0x21000バイトだけ拡張されている)。
  3. ユーザに返されたのは、その0x10バイト後の0x602010である
  4. その0x10バイト前がチャンクの先頭であり、拡張されたヒープの先頭アドレス0x602000に一致する。
  5. チャンクサイズは0x20(32)バイトである。
  6. この時点ではfastbinsY[0]にチャンクは登録されていない
  7. main_arena->topは、確保したチャンクの直後、つまり先頭アドレス0x602000にチャンクサイズ0x20を足した0x602020を指している
  8. freeした直後も、ヒープ位置は変わらない
  9. fastbinsY[0]に先程解放したチャンクが登録された
  10. main_arena->topの指す位置は(確保したメモリがfreeされたにもかかわらず)変わらない。

図解するとこんな感じ。

sbrk.png

(1) 最初はアリーナがなかったのが、
(2) sbrkでヒープが拡張され、
(3) 拡張された領域の先頭からチャンクが切り出された

という感じ。

まとめ

mallocが初めて呼ばれたときにsbrkでヒープ領域が拡張され、チャンクが切り出される様子を確認した。また、main_arenaの内部の変化も観察してみた。全部「こうなるだろう」と思ったことが確認できただけなのだが、実際にプログラムを実行して確認したり、gdbで中身を見たりすると、なんとなくmallocがより身近に感じたり感じなかったりするかもしれない。

続く

参考


  1. より正確には、Linuxではsbrkは内部でbrkシステムコールを使っている。 

  2. 古いmallocでは、fastbinsの最大サイズmaxfastというメンバ変数を持ち、その値の最下位bitをこのフラグに使っていた模様。下位bitをフラグに使うの好きね・・・ 

39
26
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
39
26