呼び出し元と呼び出し先の保存されたレジスタの違いと、いつ使用するかを理解するのに苦労しています。
私はMSP430を使用しています:
手順:
mov.w #0,R7
mov.w #0,R6
add.w R6,R7
inc.w R6
cmp.w R12,R6
jl l$loop
mov.w R7,R12
ret
上記のコードは呼び出し先であり、教科書の例で使用されているため、規則に従います。 R6とR7は呼び出し先が保存され、R12は呼び出し元が保存されます。私の理解では、プロシージャで値を変更してもプロシージャ外の値に影響しないという意味で、呼び出し先が保存したregは「グローバル」ではありません。このため、最初に新しい値を呼び出し先regに保存する必要があります。
R12、保存された呼び出し元は、より良い言葉がないため「グローバル」です。プロシージャが行うことは、呼び出し後のR12に永続的な影響を与えます。
私の理解は正しいですか?私は他のものが欠けていますか?
呼び出し元保存レジスタ(別名volatileレジスタ、またはcall-clobbered)は、呼び出し間で保持する必要のない一時的な量を保持するために使用されます。
そのため、これらのレジスタをスタックにプッシュするか、他の場所にコピーするのは呼び出し側の責任ですifプロシージャコール後にこの値を復元したい。
ただし、call
を使用してこれらのレジスタの一時値を破棄するのは正常です。
呼び出し先保存レジスタ(別名不揮発性レジスタ、または呼び出し保存)は、保存する必要がある長期保存値を保持するために使用されます呼び出します。
呼び出し元がプロシージャ呼び出しを行うとき、呼び出し先が戻った後にこれらのレジスタが同じ値を保持することを期待でき、呼び出し元に戻る前にそれらを保存して復元するのは呼び出し先の責任になります。またはそれらに触れないようにします。
呼び出し先と呼び出し元の保存は、コール全体でレジスタの値を保存および復元する責任者の慣習です。すべてのレジスタは「グローバル」であり、どのコードでもどこでもレジスタを参照(または変更)でき、それらの変更は後のコードでどこでも参照できます。レジスタ保存規則のポイントは、コードが特定のレジスタを変更することを想定していないことです。他のコードは値が変更されていないことを前提としています。
サンプルコードでは、レジスタ値を保存または復元しようとしないため、レジスタのどれも呼び出し先の保存ではありません。ただし、未定義のラベルへの分岐が含まれているため、プロシージャ全体ではないようです(l$loop
)。そのため、一部のレジスタを呼び出し先保存として扱うプロシージャの途中からのコードの断片である可能性があります。保存/復元の指示が欠けているだけです。
呼び出し元保存/呼び出し先保存の用語は、かなり長い間非効率的なプログラミングのモデルに基づいており、呼び出し元は実際にすべての呼び出しで上書きされたレジスタを保存/復元し(長期的に有用な値を他の場所に保持するのではなく)、呼び出し先は実際に保存します/すべてのコール保存レジスタを(それらの一部または一部を使用しないだけでなく)復元します。
または、「caller-saved」は「何らかの方法でif後で保存したい」という意味であることを理解する必要があります。
実際には、効率的なコードにより、値が不要になったときに破棄されます。コンパイラは通常、関数の開始時にいくつかの呼び出し保存レジスタを保存する関数を作成します(そして、それらを最後に復元します)。関数内では、関数呼び出し間で生き残るために必要な値にこれらのregを使用します。
基本的な概念を聞いた後は、「コールプリザーブド」と「コールクローバード」の方が好きです。発信者の視点または着信者の視点から考えるために、深刻な精神体操を必要としません。 (両方の用語はsameの観点からのものです)。
さらに、これらの用語は複数の文字で異なります。
用語volatile/non-volatileは、電力損失で値を失うか、または失わないストレージとの類推により、かなり良いです(DRAM対Flashなど) )。ただし、C volatile
キーワードの技術的意味はまったく異なるため、Cの呼び出し規約を記述するときの「(non)-volatile」のマイナス面です。
呼び出し先の観点から、関数は保存/復元せずにこれらのレジスタを自由に上書きできます(別名clobber)。
発信者の観点から、call foo
は、すべての呼び出しで破壊されたレジスタを破壊(別名、破壊者)します。または、少なくともそれを破壊すると仮定する必要があります。
カスタム呼び出し規約を持つプライベートヘルパー関数を作成できます。あなたは彼らが特定のレジスタを変更しないことを知っています。ただし、ターゲット関数が通常の呼び出し規則に従うことだけを知っている(または仮定または依存したい)場合は、関数呼び出しをすべての呼び出しで破壊されたレジスタを破壊するかのように扱う必要があります。それは文字通り名前の由来です:呼び出しはこれらのレジスタを破壊します。
プロシージャー間の最適化を行う一部のコンパイラーは、カスタム呼び出し規約を使用して、ABIに従わない関数の内部使用のみの定義を作成することもできます。
呼び出し先の観点からは、元の値をどこかに保存しない限り、これらのレジスタを変更することはできません。そのため、戻る前に元の値を復元できます。または、スタックポインターのようなレジスタ(ほとんどの場合、コールが保存されます)の場合、実際のsavingの代わりに、既知のオフセットを減算し、返す前に再度追加することができますどこでも。つまり、ランタイム変数量のスタックスペースを割り当てない限り、推測航法によってそれを復元できます。次に、通常は別のレジスタからスタックポインターを復元します。
多くのレジスタを使用することでメリットが得られる関数は、関数呼び出しを行わなくても、より多くの一時レジスタとして使用できるように、いくつかの呼び出し保存レジスタを保存/復元できます。通常、保存/復元は通常、関数の開始/終了時にプッシュ/ポップのコストがかかるため、これを使用するには、コールクローバーレジスタを使い果たした後にのみこれを行います。 (または、関数に複数の終了パスがある場合は、それぞれにpop
があります。)
「caller-saved」という名前は誤解を招くものです。特別に保存/復元する必要はありませんhave。通常、コードは、呼び出しが保持されるレジスター、スタック上のどこか、またはリロード可能な他の場所で関数呼び出しを生き残るために必要な値を持つように調整します。 call
に一時的な値を破棄させるのは正常です。
たとえば、x86-64 System V ABIについては、 Linux x86-64関数呼び出しで保持されるレジスタ を参照してください。
また、私が知っているすべての関数呼び出し規約では、引数渡しレジスタは常に呼び出しで上書きされます。 rdiとrsiの呼び出し元は保存されていますか、呼び出し先はレジスターに保存されていますか? を参照してください
ただし、システムコールの呼び出し規則では、通常、戻り値以外のすべてのレジスタが呼び出し保存されます。 (通常、偶数の条件コード/フラグを含みます。) i386およびx86-64でのUNIXおよびLinuxシステムコールの呼び出し規則は何ですか を参照してください。
上記の回答に加えて、呼び出された関数のレジスタ値をバックアップする必要がない場合があるため、関数呼び出し後に値を保存する必要のある変数に呼び出し先保存レジスタを使用することをお勧めします。それ以外の場合、関数呼び出し後に変数の内容を保存する必要がない場合は、呼び出し元関数または呼び出し先関数でバックアップが不要なため、呼び出し元が保存したレジスタを使用します。