まず第一に、C のガベージ コレクターは、広範なコンパイラーと OS のサポートがないため、保守的でなければなりません。正当なポインターと、たまたまポインターのように見える値を持つ整数とを区別できないからです。そして、保守的なガベージコレクターでさえ大変です実装する。のように、本当に難しい。また、許容できるものを得るために、言語を制約する必要がある場合がよくあります。たとえば、ポインターが隠されているか難読化されている場合、メモリを正しく収集することが不可能になる可能性があります。100 バイトを割り当てて、割り当ての 10 番目のバイトへのポインターのみを保持する場合、GC はブロックがまだ必要であることを認識できません。制御する必要があるもう 1 つの非常に重要な制約は、メモリのアライメントです。ポインタがアライメントされていないメモリ上にある可能性がある場合、コレクターは 10 倍以上遅くなる可能性があります。
ルートを見つけるには、スタックの開始位置とスタックの終了位置を知る必要があります。複数形に注意してください。各スレッドには独自のスタックがあり、目的によってはそれを考慮する必要がある場合があります。プラットフォーム固有の詳細を入力せずにスタックの開始場所を知るには (おそらくとにかく提供することはできません)、現在のスレッドのメイン関数内でアセンブリ コードを使用できます (スレッドmain
化されていない実行可能ファイルのみ)。スタック レジスタをクエリします ( esp
x86 ではrsp
、x86_64 ではこれら 2 つだけに名前を付けます)。Gcc と clang は、変数をレジスタに永続的に割り当てることができる言語拡張機能をサポートしているため、簡単に使用できます。
register void* stack asm("esp"); // replace esp with the name of your stack reg
(register
は標準言語のキーワードであり、今日のコンパイラではほとんどの場合無視されますが、 と組み合わせるとasm("register_name")
、厄介なことを実行できます。)
重要なルートを忘れないようにするために、main
関数の実際の作業を別のものに任せるべきです。(x86 プラットフォームでは、代わりにスタック フレームのベース ポインターであるebp
/rbp
をクエリしても、実際の作業はメイン関数で行うことができます。)
int main(int argc, const char** argv, const char** envp)
{
register void* stack asm("esp");
// put stack somewhere
return do_main(argc, argv, envp);
}
コレクションを行うために GC に入ったら、中断したスレッドの現在のスタック ポインターを照会する必要があります。そのためには、設計固有および/またはプラットフォーム固有の呼び出しが必要になります (ただし、同じスレッドで何かを実行する場合、上記の手法は引き続き機能します)。
根の実際の狩りは今から始まります。朗報: ほとんどの ABI では、ポインターのサイズよりも大きな境界にスタック フレームを配置する必要があります。つまり、すべてのポインターが配置されたメモリ上にあると信頼できる場合は、スタック全体intptr_t*
を内部は、マネージド ポインターのように見えます。
明らかに、他のルーツがあります。グローバル変数は (理論的には) ルートにすることができ、構造内のフィールドもルートにすることができます。レジスターは、オブジェクトへのポインターを持つこともできます。ルートになる可能性のあるグローバル変数を個別に説明する必要があります(または、私の意見ではそれを完全に禁止することは悪い考えではありません)。それらの自動検出は難しいためです(少なくとも、その方法はわかりません)任意のプラットフォームで)。
これらのルートはヒープ上の参照につながる可能性があり、注意しないとうまくいかない可能性があります。
すべてのプラットフォームがmalloc
イントロスペクションを提供しているわけではないため (私の知る限り)、スキャンされたメモリ、つまり GC が認識しているメモリの概念を実装する必要があります。少なくとも、そのような割り当てのそれぞれのアドレスとサイズを知る必要があります。これらのいずれかへの参照を取得すると、スタックに対して行ったのと同じように、ポインターをスキャンするだけです。(これは、ポインターが整列されていることに注意する必要があることを意味します。これは通常、コンパイラーにその仕事をさせる場合に当てはまりますが、サードパーティの API を使用する場合は注意が必要です)。
これは、GC が到達できない場所に収集可能なメモリへの参照を配置できないことも意味します。そして、これが最も痛い場所であり、特に注意する必要がある場所です. それ以外の場合、プラットフォームがmalloc
イントロスペクションをサポートしている場合は、ポインターを取得する各割り当てのサイズを簡単に確認し、それらをオーバーランしないようにすることができます。
これは、トピックの表面をなぞっただけです。ガベージ コレクターは、シングル スレッドであっても非常に複雑です。スレッドをミックスに追加すると、まったく新しい傷の世界に入ります。
Apple は、このような保守的な GC を Objective-C 言語に実装し、libauto と名付けました。彼らは、Mac OS X の低レベル技術のかなりの部分と共に、それをオープンソース化しており、ソースはここで見つけることができます。
ここではホット・リックスしか引用できません: 頑張ってください!
さて、さらに先に進む前に、非常に重要なことを忘れていました。コンパイラの最適化によって GC が壊れる可能性があるということです。コンパイラがGCを認識していない場合、特定のルートをスタックに置くことはできず(レジスタで処理するだけです)、それらを見逃すことになります。レジスタを調べることができれば、これはシングルスレッド プログラムではそれほど問題にはなりませんが、マルチスレッド プログラムでは大きな問題になります。
また、割り当ての中断可能性についても十分に注意してください。新しいポインターを返している間、GC が開始されないようにする必要があります。これは、ルートに割り当てられる直前にそれを収集する可能性があり、プログラムが再開すると割り当てられるためです。あなたのプログラムへのその新しいダングリングポインター。
そして、ここに編集に対処するための更新があります:
更新: GC を初期化するときに、すべてのポインター名と型を GC に送信するとどうなりますか? 同様に、さまざまなタイプの構造体を送信して、GC がツリーをトラバースできるようにすることもできます。これは正気な考えですか、それとも私はただ頭がおかしくなっているのでしょうか?
メモリを割り当ててから、それを GC に登録して、それがマネージド リソースであることを伝えることができると思います。これにより、中断可能性の問題が解決されます。ただし、サードパーティのライブラリに送信するものには注意してください。サードパーティのライブラリへの参照が保持されていると、データ構造が GC に登録されないため、GC がそれを検出できない可能性があるためです。
そして、スタック上のルートではおそらくそれを行うことはできません。