web-dev-qa-db-ja.com

技術的には、可変関数はどのように機能しますか? printfはどのように機能しますか?

va_arg独自の可変関数を記述しますが、可変関数は内部で、つまりアセンブリ命令レベルでどのように機能しますか?

たとえば、printfが可変数の引数を取ることはどのようにして可能ですか?


*例外なくルールはありません。言語C/C++はありませんが、この質問は両方に回答できます

*注:元々の回答は printf関数が変数パラメーターを数値で受け取ることができる方法 ですが、質問者には適用されなかったようです

57
Sebastian Mach

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およびC++では、可変個引数関数がva_listインターフェイスと共に使用されます。スタックへのプッシュはこれらの言語に固有ですが( K + RCでは、引数を指定せずに関数を前方宣言することもできます ですが、任意の数と種類の引数を指定して呼び出します)、このような不明な引数リストは、基本的に低レベルのスタックフレームアクセスを抽象化するva_...- macrosおよびva_list- typeを介してインターフェイスされます。

70
Sebastian Mach

可変個関数は標準で定義されており、明示的な制限はほとんどありません。以下は、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;
}

想定はおおよそ次のとおりです。

  1. (少なくとも1つの)最初の固定の名前付き引数が必要です。 ...は、コンパイラーに正しいことを行うように指示する以外は、実際には何もしません。
  2. 固定引数は、不特定のメカニズムにより、可変個引数の数に関する情報を提供します。
  3. 固定引数から、va_startマクロが引数を取得できるオブジェクトを返すことが可能です。タイプはva_listです。
  4. va_listオブジェクトから、va_argが各可変個引数を反復処理し、その値を互換性のある型に強制変換することが可能です。
  5. va_startで奇妙なことが起こった可能性があるため、va_endを使用すると問題が解決します。

最も一般的なスタックベースの状況では、va_listはスタックに置かれている引数へのポインタにすぎず、va_argはポインタをインクリメントしてキャストし、値に逆参照します。次に、va_startは単純な算術(および内部知識)によってそのポインタを初期化し、va_endは何もしません。奇妙なアセンブリ言語はなく、スタック上のどこにあるかについての内部知識がいくつかあります。標準ヘッダーのマクロを読んで、それが何かを確認してください。

一部のコンパイラ(MSVC)では特定の呼び出しシーケンスが必要になるため、呼び出し元は呼び出し先ではなくスタックを解放します。

printfのような関数は、このように機能します。固定引数はフォーマット文字列で、引数の数を計算できます。

vsprintfのような関数は、va_listオブジェクトを通常の引数型として渡します。

レベルの詳細が必要な場合は、質問に追加してください。

5
david.pfx