フレームをアンワインドするとは、関数の状態から呼び出し元の状態を求めることです。つまり、関数が戻る先のアドレス、呼び出し元のスタックポインター、関数が変更した呼び出し先保存レジスターの値です。戻りアドレスはスタック上にありますが、その位置は関数がそれまでにどれだけプッシュし確保したかによって決まり、それはプロローグとエピローグの中で命令ごとに変わります。バックトレースを表示するデバッガー、時間の使われ方をサンプリングするプロファイラー、ハンドラーを探す例外は、いずれもこの手順をフレームからフレームへと繰り返します。

これを知る方法は 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 は、このためにレジスターに番号を付けています。

psABI による x86-64 の DWARF レジスター番号
レジスターDWARF 番号
RAX、RDX、RCX、RBX0、1、2、3
RSI、RDI、RBP、RSP4、5、6、7
R8 から R158 から 15
戻りアドレス16
XMM0 から XMM1517 から 32
各プッシュと RSP の各変更の後で、.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_endproc

Intel 構文 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_endproc

Clang 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_endproc

Intel 構文 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_endproc

GCC のマニュアルは、このようなテーブルを非同期 (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_endproc

Intel 構文 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 がエピローグをいくつかの形式に限っているのはそのためです。プロローグの中では、すでに実行された命令のコードを元に戻します。

Windows x64 向けの同じ関数。.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_endproc

Intel 構文 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_endproc

MinGW-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_endproc

Intel 構文 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_endproc

UNWIND_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_endproc

Intel 構文 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_endproc

32 ビット Windows: SEH フレームチェーン

32 ビット Windows にはアンワインドテーブルがありません。FS:0 にあるスレッド情報ブロックの最初のフィールドは、スタック上の例外登録レコードのチェーンを指し、各レコードは次のレコードへのポインターとハンドラーのアドレスを持ちます。ハンドラーを持つ関数は、入口でフレーム内に自分のレコードを作り、それをチェーンの先頭につなぎ、戻る前に外します。例外が発生すると、システムはこのチェーンをたどってハンドラーを見つけます。

Clang が作る _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_handler3

Intel 構文 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 も使います。

まとめ

各 ABI のアンワインド
System V x86-64 と i386Windows x64Windows x86
場所.eh_frame、DWARF コールフレーム情報.pdata と .xdataスタック上のレコード、FS:0 からたどるチェーン
コンパイラーが書く形.cfi_ ディレクティブ.seh_ ディレクティブハンドラーを持つ各関数のコード
記述対象すべての命令プロローグ、エピローグは認識されるハンドラーを持つ関数のみ
例外がないときのコスト実行時にはなし実行時にはなしレコードの連結と切り離し
例外パーソナリティルーチンとそのデータ言語固有のハンドラーとそのデータレコードのハンドラー

出典

  1. System V Application Binary Interface, AMD64 Architecture Processor Supplement, version 1.0: コミット e1ce098331da、2025-03-12
  2. DWARF Debugging Information Format, Version 5: 2026-09-25 の公開版
  3. GCC 14.2 manual: Options for Code Generation Conventions: 2026-09-25 の公開版
  4. Linux 7.3: tools/perf/Documentation/perf-record.txt: コミット c03c1a14507d、2026-10-18
  5. Microsoft Learn: x64 exception handling: コミット f70d88cd5da7、2026-09-24
  6. Microsoft Learn: x64 prolog and epilog: コミット f70d88cd5da7、2026-09-24
  7. Microsoft Learn: Structured Exception Handling (C/C++): コミット f70d88cd5da7、2026-09-24
  8. Microsoft Learn: /SAFESEH (Image has Safe Exception Handlers): コミット f70d88cd5da7、2026-09-24
  9. LLVM 23.1.2: llvm/lib/Target/X86/X86WinEHState.cpp: コミット 85ac56026243、2026-09-20
  10. Wine 11.0: include/winnt.h: コミット db11d0fe6a16、2026-01-13
  11. Linux 7.3: Documentation/arch/x86/shstk.rst: コミット c03c1a14507d、2026-10-18
  12. glibc 2.44: sysdeps/x86_64/__longjmp.S: コミット c3a3a9808ad3、2026-07-24
  13. GCC 14.2.0: libgcc/config/i386/shadow-stack-unwind.h: コミット 04696df09633、2024-08-01
  14. Microsoft Learn: /CETCOMPAT (CET Shadow Stack compatible): コミット f70d88cd5da7、2026-09-24