x86-64 と x86 のスタックのアンワインドと例外処理のデータ
デバッガー、プロファイラー、クラッシュレポーター、そして例外は、いずれも関数からその呼び出し元へ、さらにその先へとスタックをさかのぼる必要があります。各フレームがどうなっているかを知っているのはコンパイラーだけなので、ABI はコンパイラーにその答えを、テーブルかフレーム自体に書き残させます。
フレームをアンワインドするとは、関数の状態から呼び出し元の状態を求めることです。つまり、関数が戻る先のアドレス、呼び出し元のスタックポインター、関数が変更した呼び出し先保存レジスターの値です。戻りアドレスはスタック上にありますが、その位置は関数がそれまでにどれだけプッシュし確保したかによって決まり、それはプロローグとエピローグの中で命令ごとに変わります。バックトレースを表示するデバッガー、時間の使われ方をサンプリングするプロファイラー、ハンドラーを探す例外は、いずれもこの手順をフレームからフレームへと繰り返します。
これを知る方法は 2 つあります。フレームポインターは、フレームをほかのデータなしでたどれる連結リストにします。アンワインドテーブルは各関数を記述し、フレームポインターがなくてもその関数のフレームをアンワインドできるようにします。System V と Windows x64 はアンワインドテーブルを使います。32 ビット Windows は別の方法を取り、例外ハンドラーを持つ関数がスタック上のレコードを連結します。
フレームポインター
フレームポインターを保持する関数は、呼び出し元の RBP をプッシュし、RBP がそのスロットを指すようにします。そのため、各フレームの RBP は呼び出し元の保存された RBP につながり、そのすぐ上に戻りアドレスがあります。このチェーンをたどるにはスタックだけがあれば十分です。スタックフレームのページにそのようなフレームを示しています。x86-64 Linux の GCC と Clang は、-fno-omit-frame-pointer で指示された場合にだけこれを作ります。
Linux の perf record は、ユーザー空間のコードのコールグラフを記録するために、デフォルトでこのチェーンをたどります。そのドキュメントは、フレームポインターなしでビルドされたコードではこの方法で誤ったコールグラフが得られると警告し、ほかに 2 つの方法を用意しています。サンプルごとにスタックの一部をコピーし、後でアンワインドテーブルを使ってアンワインドする dwarf と、プロセッサーが記録した直近の分岐を読む lbr です。
DWARF コールフレーム情報
x86-64 psABI は、手書きのアセンブリも含め、すべての関数に DWARF のコールフレーム情報 (call frame information) の形式のアンワインド情報を求めています。コンパイラーはそれを命令の間に .cfi_ ディレクティブとして書き、アセンブラーがそれを .eh_frame セクション内の関数のエントリに変換します。x86-64 Linux の GCC と Clang はデフォルトでこれを行います。このサイトのほかのリストは -fno-asynchronous-unwind-tables でこれを省いており、後に -fasynchronous-unwind-tables を指定するとそれが上書きされます。
このテーブルは、正規フレームアドレス (canonical frame address、CFA) を中心に構成されます。CFA は呼び出し時点の呼び出し元のフレームでのスタックポインターの値で、フレームが存在する間ずっと変わりません。x86-64 では戻りアドレスは CFA-8 にあります。テーブルは命令ごとに CFA の規則 (レジスターとオフセット) を与え、関数が各呼び出し先保存レジスターを CFA からどのオフセットに保存したかを示します。psABI は、このためにレジスターに番号を付けています。
| レジスター | DWARF 番号 |
|---|---|
| RAX、RDX、RCX、RBX | 0、1、2、3 |
| RSI、RDI、RBP、RSP | 4、5、6、7 |
| R8 から R15 | 8 から 15 |
| 戻りアドレス | 16 |
| XMM0 から XMM15 | 17 から 32 |
.cfi_def_cfa_offset は CFA を RSP に新しいオフセットを加えたものとして与え、フレームが完成した時点ではそのオフセットは 64 です。.cfi_offset はレジスターが保存された場所を示し、RBP の場合は CFA より 16 下です。GCC はレジスターを DWARF 番号 (RBP は 6、RBX は 3) で、Clang は名前で示します。エピローグも記述されるため、テーブルはすべての命令で正確です。unwind-frame.c
/* A function that saves two registers, keeps an array on the stack
and calls another: its unwind information tells where each is. */
extern long use(long *p);
long work(long a, long b)
{
long buf[4] = { a, b };
long r = use(buf);
return r + a * b;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -S unwind-frame.c
work:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsi, %rbp
pxor %xmm0, %xmm0
pushq %rbx
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
movq %rdi, %rbx
imulq %rbp, %rbx
subq $40, %rsp
.cfi_def_cfa_offset 64
movq %rdi, (%rsp)
movq %rsp, %rdi
movq %rsi, 8(%rsp)
movaps %xmm0, 16(%rsp)
call use@PLT
addq $40, %rsp
.cfi_def_cfa_offset 24
addq %rbx, %rax
popq %rbx
.cfi_def_cfa_offset 16
popq %rbp
.cfi_def_cfa_offset 8
ret
.cfi_endprocIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -masm=intel -S unwind-frame.c
work:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
mov rbp, rsi
pxor xmm0, xmm0
push rbx
.cfi_def_cfa_offset 24
.cfi_offset 3, -24
mov rbx, rdi
imul rbx, rbp
sub rsp, 40
.cfi_def_cfa_offset 64
mov QWORD PTR [rsp], rdi
mov rdi, rsp
mov QWORD PTR 8[rsp], rsi
movaps XMMWORD PTR 16[rsp], xmm0
call use@PLT
add rsp, 40
.cfi_def_cfa_offset 24
add rax, rbx
pop rbx
.cfi_def_cfa_offset 16
pop rbp
.cfi_def_cfa_offset 8
ret
.cfi_endprocClang 23.1.2 x86_64-linux-gnu
AT&T 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -S unwind-frame.c
work:
.cfi_startproc
pushq %r14
.cfi_def_cfa_offset 16
pushq %rbx
.cfi_def_cfa_offset 24
subq $40, %rsp
.cfi_def_cfa_offset 64
.cfi_offset %rbx, -24
.cfi_offset %r14, -16
movq %rsi, %rbx
movq %rdi, %r14
movq %rdi, (%rsp)
movq %rsi, 8(%rsp)
xorps %xmm0, %xmm0
movaps %xmm0, 16(%rsp)
movq %rsp, %rdi
callq use@PLT
imulq %r14, %rbx
addq %rbx, %rax
addq $40, %rsp
.cfi_def_cfa_offset 24
popq %rbx
.cfi_def_cfa_offset 16
popq %r14
.cfi_def_cfa_offset 8
retq
.cfi_endprocIntel 構文 clang --target=x86_64-linux-gnu -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -masm=intel -S unwind-frame.c
work:
.cfi_startproc
push r14
.cfi_def_cfa_offset 16
push rbx
.cfi_def_cfa_offset 24
sub rsp, 40
.cfi_def_cfa_offset 64
.cfi_offset rbx, -24
.cfi_offset r14, -16
mov rbx, rsi
mov r14, rdi
mov qword ptr [rsp], rdi
mov qword ptr [rsp + 8], rsi
xorps xmm0, xmm0
movaps xmmword ptr [rsp + 16], xmm0
mov rdi, rsp
call use@PLT
imul rbx, r14
add rax, rbx
add rsp, 40
.cfi_def_cfa_offset 24
pop rbx
.cfi_def_cfa_offset 16
pop r14
.cfi_def_cfa_offset 8
ret
.cfi_endprocGCC のマニュアルは、このようなテーブルを非同期 (asynchronous) と呼んでいます。命令の境界ごとに正確なので、デバッガーやガベージコレクターはコードをどこで止めてもアンワインドできます。フレームポインターがあると規則は単純になります。RBP が設定されると、RSP がどうなっても CFA は RBP+16 であり、追加のディレクティブが必要なのは保存されたレジスターだけです。
.cfi_def_cfa_register 6 は RBP を CFA の基準にし、関数が配列を確保しても何も変わりません。popq %rbp の後、.cfi_def_cfa 7, 8 で RSP に戻ります。unwind-frame.c
/* A function that saves two registers, keeps an array on the stack
and calls another: its unwind information tells where each is. */
extern long use(long *p);
long work(long a, long b)
{
long buf[4] = { a, b };
long r = use(buf);
return r + a * b;
}
GCC 14.2.0 (Debian 14.2.0-19) x86_64-linux-gnu
AT&T 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -fno-omit-frame-pointer -S unwind-frame.c
work:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pxor %xmm0, %xmm0
movq %rsp, %rbp
.cfi_def_cfa_register 6
pushq %r12
.cfi_offset 12, -24
movq %rsi, %r12
pushq %rbx
.cfi_offset 3, -32
movq %rdi, %rbx
imulq %r12, %rbx
subq $32, %rsp
movq %rdi, -48(%rbp)
leaq -48(%rbp), %rdi
movq %rsi, -40(%rbp)
movaps %xmm0, -32(%rbp)
call use@PLT
addq $32, %rsp
addq %rbx, %rax
popq %rbx
popq %r12
popq %rbp
.cfi_def_cfa 7, 8
ret
.cfi_endprocIntel 構文 gcc-14 -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -fno-omit-frame-pointer -masm=intel -S unwind-frame.c
work:
.cfi_startproc
push rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
pxor xmm0, xmm0
mov rbp, rsp
.cfi_def_cfa_register 6
push r12
.cfi_offset 12, -24
mov r12, rsi
push rbx
.cfi_offset 3, -32
mov rbx, rdi
imul rbx, r12
sub rsp, 32
mov QWORD PTR -48[rbp], rdi
lea rdi, -48[rbp]
mov QWORD PTR -40[rbp], rsi
movaps XMMWORD PTR -32[rbp], xmm0
call use@PLT
add rsp, 32
add rax, rbx
pop rbx
pop r12
pop rbp
.cfi_def_cfa 7, 8
ret
.cfi_endproc例外はこれらのテーブルの上に成り立っています。psABI は _Unwind_RaiseException などの関数を持つアンワインドライブラリを定めており、このライブラリはフレームを 2 つのフェーズでたどります。まず何も変更せずにハンドラーを探し、見つかったときにだけ実際にフレームをアンワインドして、各フレームのクリーンアップを実行します。各フレームで、言語が提供するその関数のパーソナリティルーチン (personality routine) を、アンワインド情報が指す独自のデータとともに呼び出します。C のコードは -fexceptions で例外用のアンワインドテーブルを得ます。GCC はこれを C++ のような言語でだけデフォルトで有効にします。try と catch のデータは C++ ABI のページに示しています。
Windows x64: .pdata と .xdata
Windows x64 は、スタック領域を確保するか、ほかの関数を呼び出すすべての関数にアンワインドデータを求めます。.pdata セクションのテーブルには、関数ごとに 1 つのエントリ RUNTIME_FUNCTION があり、関数の開始と終了、そして .xdata 内のその関数の UNWIND_INFO のアドレスを持ちます。UNWIND_INFO は、プロローグのサイズ、関数がフレームレジスターを使う場合はそのレジスター、アンワインドコードの配列、そして必要に応じて例外ハンドラーを保持します。エントリのない関数はリーフ関数で、RSP はその戻りアドレスを指しています。
アンワインドコードはプロローグだけを記述し、それぞれがプロローグの命令の 1 つが行ったこと、つまりレジスターのプッシュ、何バイトかの確保、フレームポインターの設定を示します。エピローグにはアンワインドコードがありません。例外がエピローグに割り込んだ場合、アンワインダーは後続のコードを読んでそれを認識します。Microsoft がエピローグをいくつかの形式に限っているのはそのためです。プロローグの中では、すでに実行された命令のコードを元に戻します。
.seh_pushreg はアンワインドコード UWOP_PUSH_NONVOL に、.seh_stackalloc は確保になり、.seh_endprologue はプロローグの終わりを示します。Clang はエピローグにも印を付けます。MinGW-w64 GCC は同じプロローグのディレクティブを書きます。unwind-frame.c
/* A function that saves two registers, keeps an array on the stack
and calls another: its unwind information tells where each is. */
extern long use(long *p);
long work(long a, long b)
{
long buf[4] = { a, b };
long r = use(buf);
return r + a * b;
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -S unwind-frame.c
work:
.seh_proc work
pushq %rsi
.seh_pushreg %rsi
pushq %rdi
.seh_pushreg %rdi
subq $56, %rsp
.seh_stackalloc 56
.seh_endprologue
movl %edx, %esi
movl %ecx, %edi
movl %ecx, 40(%rsp)
movl %edx, 44(%rsp)
movq $0, 48(%rsp)
leaq 40(%rsp), %rcx
callq use
imull %edi, %esi
addl %esi, %eax
.seh_startepilogue
addq $56, %rsp
popq %rdi
popq %rsi
.seh_endepilogue
retq
.seh_endprocIntel 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -masm=intel -S unwind-frame.c
work:
.seh_proc work
push rsi
.seh_pushreg rsi
push rdi
.seh_pushreg rdi
sub rsp, 56
.seh_stackalloc 56
.seh_endprologue
mov esi, edx
mov edi, ecx
mov dword ptr [rsp + 40], ecx
mov dword ptr [rsp + 44], edx
mov qword ptr [rsp + 48], 0
lea rcx, [rsp + 40]
call use
imul esi, edi
add eax, esi
.seh_startepilogue
add rsp, 56
pop rdi
pop rsi
.seh_endepilogue
ret
.seh_endprocMinGW-w64 GCC 14.2.0 (Debian 14.2.0-19+27+b1) x86_64-w64-mingw32
AT&T 構文 x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -S unwind-frame.c
.seh_proc work
work:
pushq %rsi
.seh_pushreg %rsi
pushq %rbx
.seh_pushreg %rbx
subq $56, %rsp
.seh_stackalloc 56
.seh_endprologue
movl %edx, %esi
movl %ecx, %ebx
movl %ecx, 32(%rsp)
leaq 32(%rsp), %rcx
imull %esi, %ebx
movl %edx, 36(%rsp)
movq $0, 40(%rsp)
call use
addl %ebx, %eax
addq $56, %rsp
popq %rbx
popq %rsi
ret
.seh_endprocIntel 構文 x86_64-w64-mingw32-gcc -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -masm=intel -S unwind-frame.c
.seh_proc work
work:
push rsi
.seh_pushreg rsi
push rbx
.seh_pushreg rbx
sub rsp, 56
.seh_stackalloc 56
.seh_endprologue
mov esi, edx
mov ebx, ecx
mov DWORD PTR 32[rsp], ecx
lea rcx, 32[rsp]
imul ebx, esi
mov DWORD PTR 36[rsp], edx
mov QWORD PTR 40[rsp], 0
call use
add eax, ebx
add rsp, 56
pop rbx
pop rsi
ret
.seh_endprocUNWIND_INFO がハンドラーを指定している関数に例外が到達すると、アンワインダーはそのハンドラーを呼び出し、ハンドラーはその後に続く独自の形式のデータを読みます。Microsoft が C と C++ に加えた構造化例外処理である __try と __except について、Clang は C ランタイムのハンドラーと保護された範囲の表を指定します。プログラムが __try ブロックに入るときには何も実行されず、コストがかかるのは例外が発生したときだけです。
.seh_handler は __C_specific_handler を指定し、.seh_handlerdata の後にその表が続きます。.Ltmp0 から .Ltmp1 までの 1 つの範囲で、そのフィルターは定数 1 であり、__except ブロックのアドレスがあります。unwind-seh.c
/* Structured exception handling, a C extension of Microsoft's
compilers that Clang implements for Windows targets. */
extern int risky(int *p);
int guarded(int *p)
{
__try {
return risky(p);
} __except (1) {
return -1;
}
}
Clang 23.1.2 x86_64-pc-windows-msvc
AT&T 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -S unwind-seh.c
guarded:
.seh_proc guarded
.seh_handler __C_specific_handler, @unwind, @except
pushq %rbp
.seh_pushreg %rbp
subq $32, %rsp
.seh_stackalloc 32
leaq 32(%rsp), %rbp
.seh_setframe %rbp, 32
.seh_endprologue
.Ltmp0:
callq risky
nop
.Ltmp1:
.LBB0_2:
.seh_startepilogue
addq $32, %rsp
popq %rbp
.seh_endepilogue
retq
.LBB0_1:
movl $-1, %eax
jmp .LBB0_2
.seh_handlerdata
.long (.Llsda_end0-.Llsda_begin0)/16
.Llsda_begin0:
.long .Ltmp0@IMGREL
.long .Ltmp1@IMGREL
.long 1
.long .LBB0_1@IMGREL
.Llsda_end0:
.seh_endprocIntel 構文 clang --target=x86_64-pc-windows-msvc -O2 -fno-asynchronous-unwind-tables -fasynchronous-unwind-tables -masm=intel -S unwind-seh.c
guarded:
.seh_proc guarded
.seh_handler __C_specific_handler, @unwind, @except
push rbp
.seh_pushreg rbp
sub rsp, 32
.seh_stackalloc 32
lea rbp, [rsp + 32]
.seh_setframe rbp, 32
.seh_endprologue
.Ltmp0:
call risky
nop
.Ltmp1:
.LBB0_2:
.seh_startepilogue
add rsp, 32
pop rbp
.seh_endepilogue
ret
.LBB0_1:
mov eax, -1
jmp .LBB0_2
.seh_handlerdata
.long (.Llsda_end0-.Llsda_begin0)/16
.Llsda_begin0:
.long .Ltmp0@IMGREL
.long .Ltmp1@IMGREL
.long 1
.long .LBB0_1@IMGREL
.Llsda_end0:
.seh_endproc32 ビット Windows: SEH フレームチェーン
32 ビット Windows にはアンワインドテーブルがありません。FS:0 にあるスレッド情報ブロックの最初のフィールドは、スタック上の例外登録レコードのチェーンを指し、各レコードは次のレコードへのポインターとハンドラーのアドレスを持ちます。ハンドラーを持つ関数は、入口でフレーム内に自分のレコードを作り、それをチェーンの先頭につなぎ、戻る前に外します。例外が発生すると、システムはこのチェーンをたどってハンドラーを見つけます。
_except_handler3 のレコード。-36(%ebp) から上に向かって、保存された ESP、例外ポインター、前のレコードへのリンク、ハンドラー、スコープテーブル、try レベル (__try ブロックの中では 0、外では -1) が並びます。関数は %fs:0 から古い先頭を読み込んで自分のレコードに格納し、自分のレコードを先頭にし、戻る前に古い先頭を元に戻します。unwind-seh.c
/* Structured exception handling, a C extension of Microsoft's
compilers that Clang implements for Windows targets. */
extern int risky(int *p);
int guarded(int *p)
{
__try {
return risky(p);
} __except (1) {
return -1;
}
}
Clang 23.1.2 i686-pc-windows-msvc
AT&T 構文 clang --target=i686-pc-windows-msvc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -S unwind-seh.c
_guarded:
pushl %ebp
movl %esp, %ebp
pushl %ebx
pushl %edi
pushl %esi
subl $28, %esp
Lguarded$frame_escape_0 = -40
movl 8(%ebp), %eax
movl %esp, -36(%ebp)
movl $-1, -16(%ebp)
movl $L__ehtable$guarded, -20(%ebp)
leal -28(%ebp), %ecx
movl $__except_handler3, -24(%ebp)
movl %fs:0, %edx
movl %edx, -28(%ebp)
movl %ecx, %fs:0
movl $0, -16(%ebp)
pushl %eax
calll _risky
addl $4, %esp
LBB0_2:
movl -28(%ebp), %ecx
movl %ecx, %fs:0
addl $28, %esp
popl %esi
popl %edi
popl %ebx
popl %ebp
retl
LBB0_1:
movl -24(%ebp), %esp
addl $12, %ebp
movl $-1, %eax
jmp LBB0_2
Lguarded$parent_frame_offset = -36
L__ehtable$guarded:
.long -1
.long "?filt$0@0@guarded@@"
.long LBB0_1
"?filt$0@0@guarded@@":
pushl %ebp
movl %esp, %ebp
pushl %esi
movl (%ebp), %eax
movl $Lguarded$frame_escape_0, %ecx
movl -20(%eax), %edx
movl (%edx), %edx
movl (%edx), %edx
movl $Lguarded$parent_frame_offset, %esi
subl %esi, %eax
movl %edx, -24(%ecx,%eax)
movl $1, %eax
popl %esi
popl %ebp
retl
.safeseh __except_handler3Intel 構文 clang --target=i686-pc-windows-msvc -march=pentium4 -mtune=generic -O2 -fno-asynchronous-unwind-tables -masm=intel -S unwind-seh.c
_guarded:
push ebp
mov ebp, esp
push ebx
push edi
push esi
sub esp, 28
Lguarded$frame_escape_0 = -40
mov eax, dword ptr [ebp + 8]
mov dword ptr [ebp - 36], esp
mov dword ptr [ebp - 16], -1
mov dword ptr [ebp - 20], offset L__ehtable$guarded
lea ecx, [ebp - 28]
mov dword ptr [ebp - 24], offset __except_handler3
mov edx, dword ptr fs:[0]
mov dword ptr [ebp - 28], edx
mov dword ptr fs:[0], ecx
mov dword ptr [ebp - 16], 0
push eax
call _risky
add esp, 4
LBB0_2:
mov ecx, dword ptr [ebp - 28]
mov dword ptr fs:[0], ecx
add esp, 28
pop esi
pop edi
pop ebx
pop ebp
ret
LBB0_1:
mov esp, dword ptr [ebp - 24]
add ebp, 12
mov eax, -1
jmp LBB0_2
Lguarded$parent_frame_offset = -36
L__ehtable$guarded:
.long -1
.long "?filt$0@0@guarded@@"
.long LBB0_1
"?filt$0@0@guarded@@":
push ebp
mov ebp, esp
push esi
mov eax, dword ptr [ebp]
mov ecx, offset Lguarded$frame_escape_0
mov edx, dword ptr [eax - 20]
mov edx, dword ptr [edx]
mov edx, dword ptr [edx]
mov esi, offset Lguarded$parent_frame_offset
sub eax, esi
mov dword ptr [ecx + eax - 24], edx
mov eax, 1
pop esi
pop ebp
ret
.safeseh __except_handler3このチェーンは、例外が起きるかどうかにかかわらず、そのような関数を呼び出すたびに時間がかかり、ハンドラーのアドレスを書き込み可能なメモリに置くことになります。リンカーは /SAFESEH でイメージの有効なハンドラーを列挙することもでき、それによってどのハンドラーがそのイメージに属するかをシステムに伝えます。リストの .safeseh は _except_handler3 をその一覧に加えます。32 ビット Windows では、同じチェーンが __CxxFrameHandler3 をハンドラーとして C++ の例外も運びます。
シャドウスタック
Intel の CET のシャドウスタックは、プログラムが直接書き込めない 2 つ目のスタックです。CALL は戻りアドレスを両方のスタックにプッシュし、RET は 2 つのコピーを比較して、異なれば制御保護フォルト (control-protection fault) を発生させます。オブジェクトファイルは、シャドウスタック向けにビルドされたかどうかを ELF ノートで示します。Linux では、プログラムと起動時に読み込むすべてのライブラリにその印がある場合に、動的ローダーがスレッドごとにシャドウスタックを有効にします。これは 64 ビットのプログラムだけが対象です。Windows では、リンカーオプション /CETCOMPAT が x64 イメージに互換性があることを示します。
アンワインドでは 2 つのスタックをそろえておかなければなりません。longjmp や例外が一度に複数のフレームを抜けると、それらの戻りアドレスはシャドウスタックに残り、次の RET でフォルトが起きてしまいます。そのため、glibc の longjmp と libgcc のアンワインダーは、飛ばしたフレームを数え、INCSSP でシャドウスタックからポップします。1 命令あたり最大 255 個で、ループで繰り返します。longjmp は、別のシャドウスタックに戻る必要がある場合には RSTORSSP も使います。
まとめ
| System V x86-64 と i386 | Windows x64 | Windows x86 | |
|---|---|---|---|
| 場所 | .eh_frame、DWARF コールフレーム情報 | .pdata と .xdata | スタック上のレコード、FS:0 からたどるチェーン |
| コンパイラーが書く形 | .cfi_ ディレクティブ | .seh_ ディレクティブ | ハンドラーを持つ各関数のコード |
| 記述対象 | すべての命令 | プロローグ、エピローグは認識される | ハンドラーを持つ関数のみ |
| 例外がないときのコスト | 実行時にはなし | 実行時にはなし | レコードの連結と切り離し |
| 例外 | パーソナリティルーチンとそのデータ | 言語固有のハンドラーとそのデータ | レコードのハンドラー |
出典
- System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット
e1ce098331da、2025-03-12 - DWARF Debugging Information Format, Version 5: 2026-09-25 の公開版
- GCC 14.2 manual: Options for Code Generation Conventions: 2026-09-25 の公開版
- Linux 7.3: tools/perf/Documentation/perf-record.txt: コミット
c03c1a14507d、2026-10-18 - Microsoft Learn: x64 exception handling: コミット
f70d88cd5da7、2026-09-24 - Microsoft Learn: x64 prolog and epilog: コミット
f70d88cd5da7、2026-09-24 - Microsoft Learn: Structured Exception Handling (C/C++): コミット
f70d88cd5da7、2026-09-24 - Microsoft Learn: /SAFESEH (Image has Safe Exception Handlers): コミット
f70d88cd5da7、2026-09-24 - LLVM 23.1.2: llvm/lib/Target/X86/X86WinEHState.cpp: コミット
85ac56026243、2026-09-20 - Wine 11.0: include/winnt.h: コミット
db11d0fe6a16、2026-01-13 - Linux 7.3: Documentation/arch/x86/shstk.rst: コミット
c03c1a14507d、2026-10-18 - glibc 2.44: sysdeps/x86_64/__longjmp.S: コミット
c3a3a9808ad3、2026-07-24 - GCC 14.2.0: libgcc/config/i386/shadow-stack-unwind.h: コミット
04696df09633、2024-08-01 - Microsoft Learn: /CETCOMPAT (CET Shadow Stack compatible): コミット
f70d88cd5da7、2026-09-24