malloc
読み:マロック
外語:malloc: Memory ALLOCate
C/C++の標準ライブラリ関数の一つで、メモリー領域を確保するもの。なお、ユーザー空間用とカーネル空間用では仕様が全く違うので注意。カーネル空間用の仕様は最後に説明する。
書式
#include <stdlib.h>
void *malloc(size_t size);
特徴
機能
ユーザー空間用のmallocは、引数に与えられたsizeバイトを主としてヒープ(サイズが大きい場合はmmapの場合もある)から取得し割り当てて、そのメモリーに対するポインターを返す。あらゆる変数の型に対応できるよう、その環境に応じたアラインメントが行なわれていることが保証されている。なお、確保されたメモリーはクリアはされておらず、内容は不定である。
使用後はfree()関数で必ず解放する。さもないとメモリーリークの原因となる。
ヒープの容量不足などの理由によりメモリー確保に失敗した場合はNULLポインターを返す。関数の引数sizeが0の場合は、malloc()はNULL、あるいは後でfree()に渡しても問題が生じないポインターを返す。
C++
C++でも、Cとの互換性のため利用できる。
但し、C++ではnewとdeleteが追加され、さらにSTLにはstd::vectorが追加された。malloc/freeはCと同等で、new/deleteとは異なりコンストラクターとデストラクターが呼び出されないため、使用すべきではない関数となっている。
なお、mallocで確保した領域をdeleteしたり、あるいはnewで確保した領域をfree()したりすることは想定されておらず、動作は未定義である。
Androidにも、AOSP由来ではframeworks/av/services/audiopolicy/AudioPolicyEffects.cpp の AudioPolicyEffects::loadEffectParameter()などに、Code Aurora Forum由来でもframeworks/base/media/libstagefright/ExtendedWriter.cpp の ExtendedWriter::start()などに、このようなバグが確認できる。
malloc/freeとnew/deleteが混在するとこのようなバグを意図せずに混入させてしまい危険なため、C++ではmalloc/freeは忌避される傾向にある。
確保と解放
malloc関数は、size_t型のサイズを受けて、確保したメモリーのポインターをvoid *型で返す。
void *malloc(size_t size);
ちなみに、古いC、例えばK&Rの頃にはvoid *が存在しなかったため、mallocはchar *型を返していた。このため、int *などとして使うためには明示的にキャストする必要があった。なお、C++の場合はvoid *から自動的に任意の型に変換できないので、やはり明示的にキャストする必要がある。
mallocで確保されたメモリーは、プログラマーが自発的に解放するか、プログラムが終了するまで維持される。
解放はfreeでおこなう。
void free(void *ptr);
引数はvoid *であるため、どのような型にキャストされていても、問題なく受け取ることができる。これも、K&Rの頃は当然char *だった。
実装
進化
K&Rのmallocとfreeは20行程度である。性能は必要十分だが、構造が単純なため、使い続けるとメモリーの断片化が発生し速度低下が生じるなどの欠点があった。
そこで、その後の実装は、K&Rを超えるべく様々な努力を繰り広げることになる。しかし単純なK&Rのmallocに、速度や効率で勝とうとすると簡単にはいかなかった。
後の天才達が集まって改良し、何とかK&Rの性能にほぼ勝ったglibcのmallocとfreeは、なんと合わせて5000行を超えていた。ここまで巨大化してもなお、条件によってはK&Rに負けてしまうこともある。
戦略
mallocは頻繁に利用されるものであるため、この実装の良し悪しがシステム全体のパフォーマンスにも直結する。
プロセッサーの仕様ごとに実装時に考慮すべき点が異なり、またオペレーティングシステムによっても差異がある。K&Rからの伝統的なヒープ方式は今も現役であるが、使い続けるとフラグメンテーション(断片化)が生じ、効率が大幅に落ちるという弱点がある。
また、オペレーティングシステムはマルチスレッド対応、CPUもキャッシュが搭載されたり、マルチコア化が進むなどしており、このような環境に合わせた最適化も必要である。
そこで現在の実装は、実装ごとに、様々な戦略で効率的な手法を実現しようとしている。
空間による違い
メモリーアロケーターが必要なのは、カーネル空間でもユーザー空間でも同様である。
カーネル空間の場合はオペレーティングシステムの機能ということになり、関数名もmallocではなく異なるものが使われることが多い。その実装もカーネルごとに様々である。
以下は、原則としてユーザー空間について説明する。
glibc
gccで使われているglibcのユーザー空間用mallocは、Doug Leaにより書かれたdlmallocを元に改良を加えたものである。
小さな領域、中程度の領域、大型の領域と分けて各々で処理を変えており、中程度の領域まではヒープを利用しながら管理方法を工夫し、大型の領域は直接mmap()でメモリーを確保して返す。
FreeBSD/NetBSD
FreeBSDとNetBSDのユーザー空間用実装は、古くはPoul-Henning Kampにより書かれたphkmallocを用いており、後にJason Evansにより書きなおされたjemallocへと置き換えられた。
新たな実装は2005(平成17)年頃から徐々に使われ始めた。FreeBSDでは、FreeBSD 7.0から正式に採用されている。
古いphkmallocはマルチスレッドを考慮しないロック機構を用いていたためマルチスレッドでの動作に難があったが、jemallocはCPUごとに動作を分離し、arenaと名付けられた管理領域を用いる実装となっている。
OpenBSD
OpenBSDも、ユーザー空間用実装を2005(平成17)年頃から新たな実装へと移行させたが、FreeBSD/NetBSDとは異なる戦略を取った。
malloc(3)の実装に brk(2) ではなく mmap(2) を使うようにした点が大きな特徴である。
小さな領域はメモリープールより割り当てるが、それ以外ではmmapを使用する。つまり、mallocするとmmapでメモリーが確保され、freeするとmunmapでメモリーが開放される動作となる。
この戦略はキャッシュヒット率が落ちるため速度低下が懸念されるが、主たる目的はOpenBSDが標榜するセキュリティのためであり、ASLR(アドレス空間配置のランダム化)の実現に加えて、解放後の使用の検出が容易(munmapした領域に再度アクセスすると多くの場合セグメンテーションフォルトとなる)、二重開放の検出が容易(munmapした領域を再度munmapしたらエラーになる)、など様々なセキュリティ面での利点が期待される。
関連する関数
calloc
mallocは、確保したメモリー領域を初期化しない。したがって別途memset等を使って初期化する必要がある。
あまり使われていないが、あらかじめゼロクリアする関数もある。
void *calloc(size_t nmemb, size_t size);
sizeバイトの要素nmemb個からなる領域を確保し、そのメモリー領域をゼロ(全ビットが0のバイト)にして、そのポインターを返す。例えば、calloc(10, sizeof(int));といった具合である。
但し単なるゼロクリアなので、整数型変数では動作するだろうが、floatやdoubleなどでは0にならないだろう(これはmemsetでの初期化でも同様である)。
realloc
既に確保されたメモリー領域の変更も可能である。
void *realloc(void *ptr, size_t size);
malloc等で確保された領域ptrを、sizeバイトに変更する。メモリー領域は初期化されないが、元の領域は極力コピーされる。
つまり、小さくした場合は減った部分は削られる。大きくした場合は元のデータはすべて複写されるが、増えた部分の領域は不定な状態となる。
関連する不具合
メモリーに関するバグは、枚挙に暇が無い。
領域確保失敗
ヒープが不足しているなどの場合、malloc関数は失敗する。この時、malloc関数はNULLポインターを返す。
多くのプログラムはmallocが失敗しない前提で書かれており、mallocの戻り値をいちいちチェックすることなくそのまま用いているケースが多い。
この場合、NULLポインターに対して読み書きを実行することになり、結果、プログラムは不正終了することになる。
このようなコーディングをする理由には様々ある。現実にメモリー確保に失敗することは皆無で、仮にあったとしてもその時はシステムに致命的な問題があることが多い。結果として、その時点でプログラムを終了する以外に対処がないので、プログラマーも、あまり真面目にチェックしないようになってしまったのではないかとされる。
メモリーリーク
メモリーリークとは、一度mallocした領域のfreeを忘れることである。
Cはまったくモダンではない言語なので、ガベージコレクションなどは(特殊な実装を除いては)存在しない。freeを忘れれば、プログラムが終了するまで領域は居残り続け、メモリー残容量を圧迫する。
解放後の使用
一度freeしてしまったポインターに再度アクセスしてしまうバグである。
char *ptr = malloc(sizeof *ptr);
free(ptr);
*ptr = '\0'; /* <= OH MY GOD! */
きちんとメモリー保護しているオペレーティングシステムであっても、同じプロセスのヒープなので不正な領域へのアクセスにはならず、発見しにくいという難点がある。
しかも、一度freeした領域は次にmallocされるまでライブラリが管理領域に使用することがあり(特にglibcのmalloc)、このため解放後に書き込んでしまうとヒープ管理を破壊し、致命的な動作を引き起こす。
二重解放
一度freeしたポインターを使って、再度freeしてしまうバグである。
二重解放の脆弱性とも呼ばれており、ヒープ管理を破壊するため致命的な動作を引き起こす。
応用例
malloc関数自体は引数を一つしか持たないが、型キャストやsizeofなどが関数周辺に散在し、一般にこの関数周辺では視認性が著しく悪くなる。
そこで、次のようなマクロを用意するとキャストなどのゴチャゴチャを解消し、引数ひとつでメモリー確保が可能となる。
#define MALLOC(var) (var (*))malloc(sizeof(var))
例えば、次のように用いる。
struct tagHoge *pstHoge;
pstHoge = MALLOC(struct tagHoge);
if文も一緒にすると、次のようになる。
struct tagHoge *pstHoge;
if ((pstHoge = MALLOC(struct tagHoge))) {
...
}
カーネル空間
もちろん、OSによって全く違う。
FreeBSD
関数
FreeBSDの場合、malloc()する前に、構造体malloc_typeを定義する必要がある。
FreeBSDでは、malloc/freeの他に、realloc、reallocf、という関数がある。また、MALLOC、FREE、というマクロが用意されている。
使用方法
まずプロトタイプ宣言。
#include <sys/types.h>
#include <sys/malloc.h>
MALLOC_DECLARE(type);
次に定義。
#include <sys/param.h>
#include <sys/malloc.h>
#include <sys/kernel.h>
MALLOC_DEFINE(type, shortdesc, longdesc);
shortdescとlongdescは文字列で、短い説明と長い説明を記述する。`vmstat -m' などで確認できる。
そしてメモリーをもらったり返したりする。
char *buf;
buf = (char *)malloc(sizeof *buf, type, M_NOWAIT);
free(buf, type);
M_NOWAITの部分はint型のフラグである。カーネル空間では、mallocでもらったポインターの他に、malloc_typeを使い終わるまで持っておく必要がある。
mallocまわりが美しくないが、標準で用意されるマクロMALLOC/FREEを使うと、こうなる。
char *buf;
MALLOC(buf, char *, sizeof *buf, type, M_NOWAIT);
FREE(buf, type);
Linux
関数
Linuxの場合、カーネル空間ではmallocは利用せず、次のどちらかが利用される。つまりmallocではないが、ここで説明する。
- kmalloc(), kfree()
- vmalloc(), vfree()
それぞれに利点と欠点がある。
- kmalloc()
- カーネルで、連続した物理メモリー空間を確保する場合に使う。
物理メモリー空間が連続していることからTLBが最大限に利用でき、高速である。ただし得られる容量には制限がある。古くは128Kiバイトまでしか確保できなかったが、2.6のいつ頃か以降は32Miバイトまで可能になった。
- vmalloc()
- 仮想メモリー領域から、仮想的に連続したメモリー領域を確保する。
物理メモリー空間で連続している保証は無い。このため速度面では若干劣るが、大きな領域を確保することができる。
使用方法
#include <linux/slab.h>
void *kmalloc(size_t size, int flags);
void kfree(const void *ptr);
kmallocの第一引数に確保したいサイズを与える。第二引数はフラグである。開放はkfreeを使う。
kmallocの第二引数のフラグは、確保の方法などを選択する。カーネル用メモリーの確保は一般的にGFP_KERNELを指定するが、他にも様々なものがある。
Linux 2.6では、内部的にはスラブ・アロケーターからメモリーを確保するkmem_cache_allocなどを呼び出して処理している。
再検索