LLVMにおけるセグメント化されたスタック

導入

セグメント化されたスタックでは、スタック領域を、スレッドの初期化時に(最悪の場合のサイズである)モノリシックなチャンクとしてではなく、段階的に割り当てることができます。これは、スタックブロック(以下、スタックレットと呼ぶ)を割り当て、それらを二重リンクリストにリンクすることで行われます。関数のプロローグは、現在のスタックレットに関数を実行するのに十分なスペースがあるかどうかを確認し、そうでない場合は、libgccランタイムを呼び出してより多くのスタック領域を割り当てる責任があります。セグメント化されたスタックは、LLVM関数で"split-stack"属性を付けて有効になります。

ランタイム機能はlibgccにすでに存在しています

実装の詳細

スタックレットの割り当て

前述したように、関数のプロローグは、現在のスタックレットに十分なスペースがあるかどうかを確認します。現在のアプローチは、TCBのスロットを使用して、現在のスタック制限(新しいブロックを割り当てるために必要なスペース量を引いたもの)を格納することです。このスロットのオフセットも、libgccによって規定されています。生成されたアセンブリは、x86-64では次のようになります。

  leaq     -8(%rsp), %r10
  cmpq     %fs:112,  %r10
  jg       .LBB0_2

  # More stack space needs to be allocated
  movabsq  $8, %r10   # The amount of space needed
  movabsq  $0, %r11   # The total size of arguments passed on stack
  callq    __morestack
  ret                 # The reason for this extra return is explained below
.LBB0_2:
  # Usual prologue continues here

スタック上の関数引数のサイズは、__morestack(この関数はlibgccに実装されています)に渡す必要があります。なぜなら、その数のバイトは前のスタックレットから現在のスタックレットにコピーする必要があるからです。これは、関数引数のSP(およびFP)相対アドレス指定が期待どおりに機能するようにするためです。

異常なretは、__morestackを呼び出した関数が正しく戻るために必要です。__morestackは、戻る代わりに、.LBB0_2を呼び出します。これは、ret命令のサイズと__morestackへの呼び出しのPCの両方がわかっているため可能です。関数本体が戻ると、制御は__morestackに戻されます。__morestackは、新しいスタックレットを解放し、正しいSP値を復元し、2回目の戻りを行い、正しい呼び出し元に制御を返します。

可変サイズのalloca

スタックレットの割り当てのセクションでは、すべてのスタックフレームが固定サイズになることを自動的に前提としています。ただし、LLVMでは、llvm.alloca組み込み関数を使用して、スタック上に動的にサイズの変更が可能なメモリブロックを割り当てることができます。このような可変サイズのallocaに直面した場合、次のコードが生成されます。

  • 現在のスタックレットに十分なスペースがあるかどうかを確認します。ある場合は、通常の場合と同様に、SPを増やすだけです。

  • そうでない場合は、ヒープからメモリを割り当てるlibgccへの呼び出しを生成します。

ヒープから割り当てられたメモリは、現在のスタックレットのリストにリンクされ、同じものと一緒に解放されます。これにより、メモリリークを防ぎます。