関数を呼び出すことほど簡単なことはありません。私自身もこれを繰り返しました。



C ++の例外に関する以前の記事では、多くの暗い場所が残っていましたが、

理解できないままである主なもの-それにもかかわらずそれはどのように実行されますか

例外が発生したときの制御の移動?

SJLJのすべては明らかですが、この技術は実際には

いくつかの費用のかからない(例外なし)テーブルエンジンに取って代わられました。

しかし、これはどのようなメカニズムであり、どのように配置されているかは、カットの下で理解できます。



この記事は、 C ++ Siberiaでのスピーチの準備の過程で登場し、退屈さで知られる著者を除いて、他の誰かに役立つ可能性のある詳細が明らかになりました。



はじめに



それはすべて、setjmp / longjmp関数が使用するバッファーのサイズを調べるという単純な要望から始まりました。

この構造では、プロセッサの状態が保持されると考えられています。

そして、これはレジスタの数( AMD x86-64 )とどのように一致していますか?



MMXとFP87レジスタが組み合わされています。

32ビットモードでは、32 + 128 + 80 + 12(eip、flags)= 252バイト

64ビットの場合-128 + 256 + 80 + 24(...)= 480バイト

何かが合わない。

ドキュメントを読む:

setjmpを呼び出すと、現在のスタック位置、不揮発性レジスタおよびフラグが保存されます。
不揮発性レジスタとは何ですか? ドキュメントをもう一度お読みください。

関数を呼び出すとき、レジスタの一部の内容を維持する責任は呼び出し元にあり、これらはいわゆる揮発性レジスタです。 着信側は残りのレジスタの内容を処理し、これらは不揮発性レジスタです。
それは何であり、なぜそれが必要なのでしょうか?



レジスタを揮発性と不揮発性に分割



関数呼び出しを最適化することです。 このような部門は長い間存在していました。

いずれにせよ、 68Kアーキテクチャ(1979年)では、8個の汎用レジスタのうち2個と7個のアドレスレジスタ揮発性と見なされ 、残りは着信側によって保護されていました。



88Kアーキテクチャ(1988)では、32個の汎用レジスタのうち12個が着信側+(スタックおよびフレームポインタ)によって保護されていました。



IBM S / 360 (1964)には、16個の汎用整数(32ビット)レジスタと4個の浮動小数点レジスタがありますが、ハードウェアスタックはありません。 関数を呼び出す前に、すべてのレジスタは特別な領域に保存されます。 再帰呼び出しの場合、OSからメモリを動的に要求する必要があります。 パラメーターは、パラメーター値へのポインターのリストへのポインターとして渡されます。 実際、11個のレジスタは不揮発性です。



レジスタの数が少ないアーキテクチャの場合、レジスタを保存しても問題はありません。 PDP-11 (1970)には、汎用レジスタが6つしかありません。 そして、パラメーターはスタックを介して渡されます。 実際、「C呼び出し規約」( cdecl )が登場し、C言語が結晶化したのはここです。



8086.彼なしではどこにいるのでしょうか。 プロセッサには8個の汎用レジスタがありますが、アーキテクチャ上の負担がないのはBXだけです。 関数の呼び出しにはいくつかの規則があり、それらはパラメーターの受け渡しに関連しており、すべてのレジスターは揮発性であると見なされていました。



IA-32留まらず、再びx86-64に戻ります。この場合、各呼び出しの前に値を保存するにはレジスタが多すぎます。 本格的な機能の場合、何らかの方法で実行する必要がありますが、小さな「機能レット」の場合は無駄です。 妥協が必要です。

特定のレジスタがどのクラスに属するかは誰が判断できますか? コンパイラ自体、これはアーキテクチャと間接的な関係があります。 GCC開発者の1人が2003年に書いたものを以下に示します。

特定のレジスタを割り当てるカテゴリを決定するのは簡単ではありませんでした。 AMD64には15個の汎用レジスタがあり(pr。Lane:%rspはカウントせず、とにかく保存します)、命令で8個を使用する(いわゆる拡張レジスタ)には、サイズを大きくするREXプレフィックスが必要です。 さらに、%rax、%rdx、%rcx、%rsi、および%rdiレジスタは、一部のIA-32命令で暗黙的に使用されます。 これらのレジスタを揮発性にして、命令の使用に関する制限を回避することにしました。

したがって、%rbx、%rbp、および拡張レジスタのみを不揮発性にすることができます。 一連のテストにより、不揮発性レジスタが割り当てられたときに最小コードが得られることが示されました(%rbx、%rbp、%r12-%r15)。



最初は、揮発性の6つのSSEレジスタを作成する必要がありました。 ただし、これらのレジスタは128ビットであり、通常64ビットのみがデータの格納に使用されるため、呼び出し側用に保存するのは呼び出し側よりも高価です。

さまざまな実験が行われ、すべてのSSEレジスタがvolatileと宣言されたときに最もコンパクトで高速なコードが得られるという結論に達しました。


これは依然として当てはまります。21ページのAMD64 ABI仕様を参照してください。

同じABIがOS Xでサポートされています。



マイクロソフトはそれを異なるものと考え、その区分は次のとおりです。

少なくとも、これらすべてをjmp_bufのサイズに合わせることができます。



ケースが豊富なアーキテクチャについてはどうですか?

これは、32個の整数レジスタと浮動小数点レジスタがあるPowerPC用のOS X 64ビットコンパイラの現状です。

GPR11(*)-リーフ関数の不揮発性(他の呼び出しはありません)



合計:レジスタを2つのクラスに分離すると、関数呼び出しの汎用的な最適化が実装されます。



登録ウィンドウ



別のアプローチとして、この手法を使用するプロセッサは、 Berkeley RISCプロジェクト(1980..1984)から成長しています。



Intel i960 (1989)は、16個のローカルおよび16個の汎用汎用レジスタを備えた32ビットプロセッサです。 パラメータはグローバルレジスタを介して渡され、関数が呼び出されると、すべてのローカルレジスタが特別な命令によって保存されます。 実質的にすべてのローカルレジスタは不揮発性ですが、ハードウェアサポートが何らかの高速化をもたらすことを期待して、強制的に保存されます。 ただし、今では、これは1 キャッシュラインにすぎません。



AMD 29K (1988) -192 (sic!)レジスタを備えた32ビットプロセッサ



SPARC (1987)は、レジスタの数が異なる場合があります(名前のSはスケーラブルを意味します)



Itanium (2001)は、SPARCビジネスの後継者です。



間違いなく、Itaniumはオーディエンス賞を受賞しました。このプロセッサがうまくいかなかったことは残念です。



関数呼び出し



したがって、このアーキテクチャの素晴らしさをすべて考慮して、関数の呼び出しについて次の結論を導き出すことができます。



はい、オプティマイザーの後、関数本体の内容は主ブロスに似ている場合がありますが、この命令またはその命令が必要な理由と変数の値を見つける方法が常に明確ではありません。



ただし、関数の子呼び出しの時点で、この騒々しいアクティビティはすべてフリーズします。



プロセッサアーキテクチャに関係なく、レジスタからの現在のデータは何らかの形で損失から保護されています。 レジスタウィンドウアーキテクチャの場合、これは自然に発生します。 その他の場合、揮発性レジスタはメモリに保存されますが、これが一時的な値であり、メモリ内に場所がない場合は、再計算する必要があります。 不揮発性レジスタは変更されないか、値が復元されます。



基になる関数で例外が発生し、ある関数のcatchブロックの1つに制御を移したいとします。 実行コンテキストを復元するために必要なすべての情報は、すでにスタックまたはレジスタにあります。

tryブロックは努力を必要としない場合があります。スタックにスペースを割り当ててそこに何かを保存する必要はありません。 すべての情報はすでに保存されています。 しかし今、問題はその情報をどこに投稿したかに関する情報をどのように保存するかです。



幸いなことに、この情報は静的であり、コンパイル時に決定されます。 コンパイラはこれらすべてをテーブルに収集するため、コストのかからないテーブルエンジンになります。



これがMSVC(x64)およびGCC(x64)コンパイラでどのように実装されているかを見てみましょう。



MSVC(x64)



MSVCは各機能のプロローグとエピローグを作成しますが、RSP値はそれらの間で同じままです。 誰かがallocaを使用するまで、RBPは通常の登録と見なされます。 実験のためにいくつかの重要な関数を取り、そのプロローグの重要な部分をデバッガーで見てみましょう。

000000013F440850 mov rax、rsp

000000013F440853 push rbp

000000013F440854プッシュRDI

000000013F440855プッシュr12

000000013F440857プッシュr14

000000013F440859プッシュr15

000000013F44085B lea rbp、[rax-0B8h]#初期化

000000013F440862 sub rsp、190h

000000013F440869 mov qword ptr [rbp + 20h]、0FFFFFFFFFFFFFFFEFE#初期化

000000013F440871 mov qword ptr [rax + 10h]、rbx

000000013F440875 mov qword ptr [rax + 18h]、rsi

000000013F440879 mov rax、qword ptr [__security_cookie(013F4C5020h)]#これから、関数本体

可能であれば、オプティマイザーは初期化命令でプロローグコードを希釈します。



そして、不揮発性レジスタが元の状態に復元されるエピローグ。

000000013F4410C2 lea r11、[rsp + 190h]

000000013F4410CA mov rbx、qword ptr [r11 + 38h]

000000013F4410CE mov rsi、qword ptr [r11 + 40h]

000000013F4410D2 mov rsp、r11

000000013F4410D5ポップr15

000000013F4410D7ポップr14

000000013F4410D9ポップr12

000000013F4410DB pop rdi

000000013F4410DC pop rbp

000000013F4410DD ret

コンパイラは、.pdataセクションでスタックプロモーションの情報を収集します。 各関数にはRUNTIME_FUNCTION構造があり、そこからunwindテーブルへのリンクがあります 。 その内容は、-dump -unwindinfoオプションを指定したリンクユーティリティを使用して取り出すことができます。 同じ関数に対して、以下を見つけます。

00001D70 00020880 0002110E 000946C0?Write_table_header ...

巻き戻しバージョン:1

フラグをほどく:EHANDLER UHANDLER

プロローグのサイズ:0x3A

コード数:11

コードをほどく:

29:SAVE_NONVOL、レジスタ= rsiオフセット= 0x1D0

25:SAVE_NONVOL、レジスタ= rbxオフセット= 0x1C8

19:ALLOC_LARGE、サイズ= 0x190

0B:PUSH_NONVOL、レジスタ= r15

09:PUSH_NONVOL、レジスタ= r14

07:PUSH_NONVOL、レジスタ= r12

05:PUSH_NONVOL、レジスタ= rdi

04:PUSH_NONVOL、レジスタ= rbp

ハンドラー:0006BFD0 __GSHandlerCheck_EH

EHハンドラーデータ:00087578

GS Unwindフラグ:UHandler

Cookieオフセット:00000188

Unwindコードに興味があります -例外が発生したときに実行する必要があるアクションが含まれています。





GCC(x64)



同様に、GCCによって作成された同じ関数のプロローグとエピローグを分析します。

プロローグ

.cfi_startproc

.cfi_personality 0x9b、DW.ref .__ gxx_personality_v0

.cfi_lsda 0x1b、.LLSDA11339

pushq%r15

.cfi_def_cfa_offset 16

.cfi_offset 15、-16

pushq%r14

.cfi_def_cfa_offset 24

.cfi_offset 14、-24

pushq%r13

.cfi_def_cfa_offset 32

.cfi_offset 13、-32

movq%rdi、%r13

pushq%r12

.cfi_def_cfa_offset 40

.cfi_offset 12、-40

pushq%rbp

.cfi_def_cfa_offset 48

.cfi_offset 6、-48

pushq%rbx

.cfi_def_cfa_offset 56

.cfi_offset 3、-56

subq $ 456、%rsp

.cfi_def_cfa_offset 512

エピローグ:

addq $ 456、%rsp

.cfi_remember_state

.cfi_def_cfa_offset 56

popq%rbx

.cfi_def_cfa_offset 48

popq%rbp

.cfi_def_cfa_offset 40

popq%r12

.cfi_def_cfa_offset 32

popq%r13

.cfi_def_cfa_offset 24

popq%r14

.cfi_def_cfa_offset 16

popq%r15

.cfi_def_cfa_offset 8

ret

CFIプレフィックスはコールフレーム情報を意味し、アセンブラにスタックプロモーションの追加情報を記録する方法を指示します。 この情報は.eh_frameセクションに収集されます。dwarfdumpユーティリティと-Fスイッチを使用すると、読み取り可能な形式で表示できます。

#プロローグ

〈0〉 〈0x00000e08:0x00000f4a〉 〈〉 〈fdeオフセット0x00000e00長さ:0x00000060〉 〈eh aug data len 0x0〉

0x00000e08:〈off cfa = 08(r7)〉 〈off r16 = -8(cfa)〉

0x00000e0a:〈off cfa = 16(r7)〉 〈off r15 = -16(cfa)〉 〈off r16 = -8(cfa)〉

0x00000e0c:〈off cfa = 24(r7)〉 〈off r14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e0e:〈off cfa = 32(r7)〉 〈off r13 = -32(cfa)〉 〈off r14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e10:〈off cfa = 40(r7)〉 〈off r12 = -40(cfa)〉 〈off r13 = -32(cfa)〉

〈off r14 = -24(cfa)〉 〈off r15 = -16(cfa)〉 〈off r16 = -8(cfa)〉

0x00000e11:〈off cfa = 48(r7)〉 〈off r6 = -48(cfa)〉

〈オフr12 = -40(cfa)〉 〈オフr13 = -32(cfa)〉

〈オフr14 = -24(cfa)〉 〈オフr15 = -16(cfa)〉

〈オフr16 = -8(cfa)〉

0x00000e12:〈off cfa = 56(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉



0x00000e19:〈off cfa = 64(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e51:〈off cfa = 56(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e52:〈off cfa = 48(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e53:〈off cfa = 40(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e55:〈off cfa = 32(r7)〉 〈off r3 = -56(cfa)〉 〈off r6 = -48(cfa)

〉 〈オフr12 = -40(cfa)〉 〈オフr13 = -32(cfa)〉

〈オフr14 = -24(cfa)〉 〈オフr15 = -16(cfa)〉

〈オフr16 = -8(cfa)〉

0x00000e57:〈off cfa = 24(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e59:〈off cfa = 16(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e5b:〈off cfa = 08(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000e60:〈off cfa = 64(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈off r13 = -32(cfa)〉 〈off r14 = -24(cfa)〉 〈off r15 = -16(cfa)〉

〈オフr16 = -8(cfa)〉

0x00000f08:〈off cfa = 56(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000f09:〈off cfa = 48(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000f0a:〈off cfa = 40(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000f0c:〈off cfa = 32(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈off r13 = -32(cfa)〉 〈off r14 = -24(cfa)〉 〈off r15 = -16(cfa)〉

〈オフr16 = -8(cfa)〉

0x00000f0e:〈off cfa = 24(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000f10:〈off cfa = 16(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000f12:〈off cfa = 08(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

0x00000f18:〈off cfa = 64(r7)〉 〈off r3 = -56(cfa)〉

〈オフr6 = -48(cfa)〉 〈オフr12 = -40(cfa)〉

〈オフr13 = -32(cfa)〉 〈オフr14 = -24(cfa)〉

〈オフr15 = -16(cfa)〉 〈オフr16 = -8(cfa)〉

ここに見えるもの:





合計



それで終わりました。 その過程で、魔法がなかったことが判明しました。 例外をキャッチした後に状態を復元できるようにするために、アクションは必要ありません;コードの自然な実行中にすべてが自動的に保存されます。

ここでは、状態を復元するために、コンパイラーからの少しの助けが必要ですが、すべてが非常に控えめで、飾り気がありません。



一般に、現代のコンパイラーによる例外処理は、最も難しい問題を大騒ぎせずに完全に「労働者と農民」の方法を使って冷静に解決できる方法の良い例です。 開発者の尊重。



All Articles