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
への呼び出しを生成します。
ヒープから割り当てられたメモリは、現在のスタックレットのリストにリンクされ、同じものと一緒に解放されます。これにより、メモリリークを防ぎます。