6

以下のソースコードを使用して、「関数の動的呼び出し」を試しています。最初の 2 つの引数のみを受け入れる te​​sting_function でこのコードを正常にテストした後、3 番目の引数を追加し、関数を呼び出すときに「引数を指定しない」ことにしました。これを行うと、3 番目の引数の値が (必ずしも) 0 ではなく、元がわからない「ランダムな」値であることに気付きました。

質問は次のとおりです。

  • これらの値はどこから来たのですか?
  • さらに、引数はどのように関数に渡されますか?
  • 引数を渡さないのは悪い習慣ですか?
  • 関数を利用するコードを再コンパイルせずに、関数の引数への追加に備えることができますか? (例: 動的にロードされたライブラリの関数は受け入れられる引数を取得しますが、関数を使用するコードは再コンパイルされません)。

ソースコードの序文は次のとおりです。

Linux を使用して実行し、GCC 4.6.3 でリンカーをコンパイル/呼び出していますが、このコードを使用してもコンパイル/リンクの警告/エラーは表示されません。このコードは「完全に」実行されます。次のように gcc を呼び出します。

gcc -x c -ansi -o (output file) (input file, .c suffix)

ソースコードは次のとおりです。

#include <errno.h>
#include <stdio.h>
#include <stdlib.h>

/* Function for testing. */
int testing_function(char* something, char* somethingelse, int somethingadditional)
{
    int alt_errno = errno;
    if ((something != NULL)&&(somethingelse != NULL))
    {
        errno = 0;
        if (fprintf(stdout, "testing_function(\"%s\", \"%s\", %d);\n", something, somethingelse, somethingadditional) <= 0)
        {
            if (errno != 0)
            {
                int alt_alt_errno = errno;
                perror("fprintf(stdout, \"testing_function(\\\"%%s\\\", \\\"%%s\\\", %%d);\\n\", something, somethingelse, somethingadditional)");
                errno = alt_errno;
                return alt_alt_errno;
            }
            else
            {
                errno = ENOSYS;
                perror("fprintf(stdout, \"testing_function(\\\"%%s\\\", \\\"%%s\\\", %%d);\\n\", something, somethingelse, somethingadditional)");
                errno = alt_errno;
                return ENOSYS;
            }
        }
        else
        {
            errno = alt_errno;
            return 0;
        }
    }
    else
    {
        errno = ENOSYS;
        perror("testing_function(char* something, char* somethingelse, int somethingadditional)");
        errno = alt_errno;
        return ENOSYS;
    }
}

/* Main function. */
int main(int argc, char** argv)
{
    int (*function)(char*, char*);
    *(void**) (&function) = testing_function;
    exit(function("Hello", "world!"));
}
4

5 に答える 5

7

これらの値はどこから来たのですか?

通常、それらは以前の操作からのメモリまたはレジスタのガベージです。

さらに、引数はどのように関数に渡されますか?

プラットフォームの ABI によって異なります。通常、指定されたレジスタセットまたは「スタックポインタ」からの固定オフセットのいずれかです。

引数を渡さないのは悪い習慣ですか?

はい。「未定義の動作」を引き起こします。コンパイラは、実行した瞬間にプログラムをクラッシュさせるか、さらに悪いことをする権利があります。

関数を利用するコードを再コンパイルせずに、関数の引数への追加に備えることができますか? (例: 動的にロードされたライブラリの関数は受け入れられる引数を取得しますが、関数を使用するコードは再コンパイルされません)。

いいえ。ライブラリ ABI の一部である C 関数の引数リストを変更するときはいつでも、その名前も変更する必要があります。(ソースレベル API でこれを隠すために利用できるトリックがありますが、それらはすべて、関数の名前を変更するという基本的な戦術を覆すものです。)

もちろん C++ では、変更された引数リストは新しいオーバーロードですが、これはコンパイラによって名前が変更されて実装されます。

于 2013-05-14T21:30:07.090 に答える
2

関数パラメーターは、コンパイラーが使用する C ABI に応じて渡されます。これは、それらがスタックまたはレジスター、またはその両方の組み合わせで渡されることを意味します。32 ビットの Intel システムは通常、スタックでパスを渡しますが、64 ビットの Intel システムは主にレジスタでパスし、スタックでオーバーフローが発生すると思います。

渡されていない引数のランダム値はどこから来るのですか? それらは、値を保持する必要があるレジスタまたはスタック位置から取得されます。呼び出された関数は、引数が渡されなかったことを認識していないため、とにかくプルします。

すべての引数がスタック上にあると想定されている場合、関数が存在するよりも多くのスタック項目をプルするため、これは悪い問題につながる可能性があります。最悪の場合、関数の戻りアドレスが消去されます。

レジスタを使用する場合、ランダムな値を除いて、それほど問題はありません。

上記の情報から、それがサポートされておらず、行うべきではなく、一般的には機能しないことが収集できるはずです。

機能するの可変引数リストです。たとえば、printfそうします。open()POSIX 関数も同様です。open 宣言は次のようになります。

extern int open (__const char *__file, int __oflag, ...);

トリプルドットが見えますか?それは可変引数リストを宣言します。0 から任意の数の引数を含めることができます。それらは、特別な関数を使用してアクセスされます。予想される引数の数を知る唯一の方法は、前の引数の 1 つです。の場合はopen()oflagprintf()フォーマット文字列用。

于 2013-05-14T21:42:36.190 に答える
1

引数が少なすぎる関数を呼び出すのは非常に危険です。ほとんどの ABI では、引数のスタック スロットは呼び出し保存されません。つまり、コンパイラはスタックのこの部分を上書きする関数コードを自由に生成できます。呼び出し元が、呼び出し先が期待する引数の実際の数を認識しておらず、そのために十分なスペースを残していなかった場合、呼び出し先は喜んで呼び出し元のローカル ストレージを上書きし、おそらく戻りアドレスも含めます。

レジスタ渡しの一部のアーキテクチャ/ABI では、レジスタで渡される引数の数を超えるまでこれは適用されませんが、他のレジスタ渡しシステム (MIPS が思い浮かびます) では、スタック上の引数スロットは予約されています (呼び出し先はそれらを自由に上書きできます) レジスタに渡された引数についても同様です。

つまり、間違った数または型の引数で関数を呼び出さないでください。非常に正当な理由で未定義です。

于 2013-05-15T02:50:53.763 に答える
0

すべてのコンピューティング環境で、関数の引数は収集され、どこか (通常は CPU スタック上) のシーケンシャル メモリに配置されますが、一部のアーキテクチャでは、CPU レジスタのシーケンス、またはレジスタとメモリの組み合わせになる場合があります。

呼び出された関数が渡されたパラメータの数を決定して検証するためのメカニズムを提供する CPU はごくわずかです。VAX CPU は主要な例です。

ほとんどのアーキテクチャは、プログラマーが正しいことを行うことに依存しています。関数が 3 つのパラメーターを受け入れるように宣言されている場合、その関数が呼び出される場所では、(少なくとも) 3 つのパラメーターが必要です。そうでない場合、C 標準では、「未定義の動作」が発生すると言われています。あなたの特定のケースでは、3番目のパラメーターが配置されるべき場所に最後に書き込まれたものは何でも得られます。x86 上の gcc/Linux の場合、CPU スタック メモリになります。

于 2013-05-14T21:38:48.993 に答える
0

これらの値はどこから来たのですか?

コンパイラは、呼び出しが行われる前に呼び出しを設定します。関数が入力されると、関数はそのパラメーターを見つける方法と戻り値を格納する場所を認識します。具体的には、コンパイラには、「わかりました。関数シグネチャがあれば、このパラメーターをこのレジスターに期待できます」または「現在のスタック位置を N バイトオフセットすることにより、パラメーターがスタックに渡された場合」と言うことができる仕様があります。これは、アーキテクチャの ABI (Application Binary Interface) によって指定された呼び出し規約に基づいています。そのため、パラメーターはレジスターやスタックに格納される可能性があり、戻り値の場所も予約されています。この関数は、スタック上の現在の位置も認識しています。

そのため、関数は、パラメーターが存在すると予想される場所からパラメーターを読み取るだけです。一般に、渡していないパラメーターは、レジスターまたはスタックから読み取られたガベージ値であり、呼び出しの前に書き込まれませんでした。関数はこれらの値を読み取るだけでなく、書き込むこともできることに注意してください。

さらに、引数はどのように関数に渡されますか?

コンパイラは、ABI で指定されたレジスタまたはスタック領域にそれらを書き込むだけです。

引数を渡さないのは悪い習慣ですか?

はい。これに対する例外は va list (それ自体が危険な獣):int foo(int a, ...);で、関数はセンチネルやフォーマット指定子などのメカニズムを使用して期待値を指定します。

関数を利用するコードを再コンパイルせずに、関数の引数への追加に備えることができますか? (例: 動的にロードされたライブラリの関数は受け入れられる引数を取得しますが、関数を使用するコードは再コンパイルされません)。

C 関数を動的に配置して呼び出すことができます (C++ では失敗します)。したがって、出荷された API の署名は、動的に読み込まれたり、翻訳に表示されるヘッダーと同期していない静的な画像にリンクされたりすると凍結されると考えるのが一般的です。

さて、これのいくつかを偽造して動作させることができますが、1 つの小さなミスが未定義の動作を導入する可能性があるため、通常は悪い考えです。

于 2013-05-14T21:40:12.920 に答える