va_arg
独自の可変関数を記述しますが、可変関数は内部で、つまりアセンブリ命令レベルでどのように機能しますか?
たとえば、printf
が可変数の引数を取ることはどのようにして可能ですか?
*例外なくルールはありません。言語C/C++はありませんが、この質問は両方に回答できます
*注:元々の回答は printf関数が変数パラメーターを数値で受け取ることができる方法 ですが、質問者には適用されなかったようです
CおよびC++標準では、CおよびC++標準がどのように機能するかについての要件はありません。準拠しているコンパイラーは、連鎖リスト、std::stack<boost::any>
、または(@Xeoのコメントのとおり)魔法のポニーダストを内部で放出することを決定する場合があります。
ただし、CPUレジスタでのインライン化や引数の引き渡しなどの変換で、説明したコードがまったく残らない場合でも、通常は次のように実装されます。
また、この回答は、下の図で下向きに成長するスタックを具体的に説明していることにも注意してください。また、この回答はスキームを実証するための単純化です( https://en.wikipedia.org/wiki/Stack_frame を参照してください)。
これが可能なのは、基盤となるマシンアーキテクチャがすべてのスレッドに対していわゆる「スタック」を持っているためです。スタックは、関数に引数を渡すために使用されます。たとえば、次の場合:
foobar("%d%d%d", 3,2,1);
次に、これは次のようなアセンブラコードにコンパイルされます(例示的および概略的に、実際のコードは異なる場合があります)。引数は右から左に渡されることに注意してください:
Push 1
Push 2
Push 3
Push "%d%d%d"
call foobar
これらのプッシュ操作でスタックがいっぱいになります。
[] // empty stack
-------------------------------
Push 1: [1]
-------------------------------
Push 2: [1]
[2]
-------------------------------
Push 3: [1]
[2]
[3] // there is now 1, 2, 3 in the stack
-------------------------------
Push "%d%d%d":[1]
[2]
[3]
["%d%d%d"]
-------------------------------
call foobar ... // foobar uses the same stack!
一番下のスタック要素は「スタックのトップ」と呼ばれ、しばしば「TOS」と省略されます。
foobar
関数は、TOSから始まるスタック、つまりフォーマット文字列にアクセスするようになりました。 stack
がスタックポインタ、stack[0]
がTOSでの値、stack[1]
がTOSの1つ上などを想像してください。
format_string <- stack[0]
...次に、フォーマット文字列を解析します。解析中に、%d
- tokensを認識し、それぞれについて、スタックからもう1つの値をロードします。
format_string <- stack[0]
offset <- 1
while (parsing):
token = tokenize_one_more(format_string)
if (needs_integer (token)):
value <- stack[offset]
offset = offset + 1
...
もちろん、これは非常に不完全な疑似コードであり、関数が渡された引数に依存して、スタックからロードおよび削除する必要がある量を見つける方法を示しています。
このユーザー提供の引数への依存は、存在する最大のセキュリティ問題の1つでもあります( https://cwe.mitre.org/top25/ を参照)。ユーザーは、ドキュメントを読んでいない、フォーマット文字列や引数リストを調整するのを忘れている、あるいは明らかに悪意があるなどの理由で、可変関数を簡単に誤って使用する可能性があります。 Format String Attack も参照してください。
CおよびC++では、可変個引数関数がva_list
インターフェイスと共に使用されます。スタックへのプッシュはこれらの言語に固有ですが( K + RCでは、引数を指定せずに関数を前方宣言することもできます ですが、任意の数と種類の引数を指定して呼び出します)、このような不明な引数リストは、基本的に低レベルのスタックフレームアクセスを抽象化するva_...
- macrosおよびva_list
- typeを介してインターフェイスされます。
可変個関数は標準で定義されており、明示的な制限はほとんどありません。以下は、cplusplus.comから抜粋した例です。
/* va_start example */
#include <stdio.h> /* printf */
#include <stdarg.h> /* va_list, va_start, va_arg, va_end */
void PrintFloats (int n, ...)
{
int i;
double val;
printf ("Printing floats:");
va_list vl;
va_start(vl,n);
for (i=0;i<n;i++)
{
val=va_arg(vl,double);
printf (" [%.2f]",val);
}
va_end(vl);
printf ("\n");
}
int main ()
{
PrintFloats (3,3.14159,2.71828,1.41421);
return 0;
}
想定はおおよそ次のとおりです。
...
は、コンパイラーに正しいことを行うように指示する以外は、実際には何もしません。va_start
マクロが引数を取得できるオブジェクトを返すことが可能です。タイプはva_list
です。va_list
オブジェクトから、va_arg
が各可変個引数を反復処理し、その値を互換性のある型に強制変換することが可能です。va_start
で奇妙なことが起こった可能性があるため、va_end
を使用すると問題が解決します。最も一般的なスタックベースの状況では、va_list
はスタックに置かれている引数へのポインタにすぎず、va_arg
はポインタをインクリメントしてキャストし、値に逆参照します。次に、va_start
は単純な算術(および内部知識)によってそのポインタを初期化し、va_end
は何もしません。奇妙なアセンブリ言語はなく、スタック上のどこにあるかについての内部知識がいくつかあります。標準ヘッダーのマクロを読んで、それが何かを確認してください。
一部のコンパイラ(MSVC)では特定の呼び出しシーケンスが必要になるため、呼び出し元は呼び出し先ではなくスタックを解放します。
printf
のような関数は、このように機能します。固定引数はフォーマット文字列で、引数の数を計算できます。
vsprintf
のような関数は、va_list
オブジェクトを通常の引数型として渡します。
レベルの詳細が必要な場合は、質問に追加してください。