1. ホーム
  2. c++

[解決済み] C言語のmain()メソッドはどのように動作するのでしょうか?

2023-01-26 16:39:03

質問

mainメソッドの書き方として、2種類のシグネチャがあることは知っています。

int main()
{
   //Code
}

また、コマンドライン引数を扱う場合は、次のように記述します。

int main(int argc, char * argv[])
{
   //code
}

C++ メソッドをオーバーロードすることができるのは知っていますが C の二つの異なるシグネチャをコンパイラはどのように扱うのでしょうか? main 関数をどのように扱うのでしょうか?

どのように解決するのですか?

C言語の機能のいくつかは、たまたま動作するようになったハックから始まりました。

可変長の引数リストと同様に、mainのための複数の署名は、それらの機能の1つです。

プログラマは、関数に余分な引数を渡しても、与えられたコンパイラで何も悪いことが起きないことに気づきました。

呼び出しの規約がそのようなものである場合、このようになります。

  1. 呼び出し側の関数が引数をクリーンアップする。
  2. 一番左の引数はスタックの一番上、またはスタックフレームの底に近いので、 偽の引数がアドレッシングを無効にすることはありません。

これらのルールに従った呼び出しの慣習の1つのセットは、呼び出し側が引数をポップし、それらが右から左にプッシュされるスタックベースのパラメータ渡しです。

 ;; pseudo-assembly-language
 ;; main(argc, argv, envp); call

 push envp  ;; rightmost argument
 push argv  ;; 
 push argc  ;; leftmost argument ends up on top of stack

 call main

 pop        ;; caller cleans up   
 pop
 pop

このような呼び出し規則があるコンパイラでは、2 種類の main の2種類、あるいは追加の種類をサポートするために特別なことをする必要はありません。 main は引数なしの関数にすることができ、その場合スタックにプッシュされた アイテムは無視されます。もしそれが2つの引数を持つ関数であれば、それは argcargv をスタックの一番上の 2 つの項目として返します。もしそれが環境ポインタ (一般的な拡張) を持つプラットフォーム固有の 3 つの引数のバリエーションであれば、それも動作します。

そして、固定コールはすべてのケースで動作し、プログラムにリンクされる単一の固定された起動モジュールを可能にします。そのモジュールはCで書くことができ、次のような関数として書くことができます。

/* I'm adding envp to show that even a popular platform-specific variant
   can be handled. */
extern int main(int argc, char **argv, char **envp);

void __start(void)
{
  /* This is the real startup function for the executable.
     It performs a bunch of library initialization. */

  /* ... */

  /* And then: */
  exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}

つまり、このスタートモジュールは、常に3つの引数を持つmainを呼び出すだけです。もし main が引数を取らないか、あるいは int, char ** であれば、呼び出しの規約により、引数を取らない場合と同様に、たまたまうまく動作します。

もしあなたがプログラムの中でこのようなことをするならば、それは移植性がなく、ISO C によって未定義の動作とみなされるでしょう: ある方法で関数を宣言して呼び出し、別の方法でそれを定義することです。しかし、コンパイラーのスタートアップ トリックは移植可能である必要はなく、移植可能なプログラムのための規則によって導かれるわけではありません。

しかし、呼び出しの規則がこの方法では動作しないようなものだったとします。その場合、コンパイラは起動時に main を特別に扱わなければなりません。コンパイラが、コンパイル中に main 関数をコンパイルしていることに気づくと、例えば3つの引数の呼び出しと互換性のあるコードを生成することができます。

つまり、こう書きます。

int main(void)
{
   /* ... */
}

しかし、コンパイラがこれを見ると、基本的にコード変換を行い、コンパイルする関数がよりこのように見えるようにします。

int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
   /* ... */
}

ただし,名前 __argc_ignore という名前が文字どおり存在しないことを除けば、です。そのような名前はスコープに導入されませんし、未使用の引数に関する警告も出ません。 コード変換により、コンパイラーは 3 つの引数をクリーンアップする必要があることを知っている正しいリンケージを持つコードを出力するようになります。

もうひとつの実装方法は、コンパイラやリンカがカスタム生成する __start 関数 (またはそれが何と呼ばれていようと) をカスタム生成するか、少なくともいくつかのプリコンパイルされた選択肢の中から 1 つを選択することです。のサポートされている形式がどれであるかについての情報をオブジェクト ファイルに保存することができます。 main のどの形式が使用されているかについての情報をオブジェクト ファイルに保存できます。 リンカーはこの情報を見て、スタートアップモジュールの正しいバージョンを選択することができ、そのバージョンには main への呼び出しを含むスタートアップモジュールの正しいバージョンを選択することができます。 C の実装では通常、サポートされる形式の数は少なく main というように、この方法は実現可能です。

C99言語用のコンパイラは、常に main がなければ関数が終了しないというハックをサポートするために、ある程度は特別に扱います。 return ステートメントがない場合、その動作はあたかも return 0 が実行されたかのような動作をします。 これもまた、コード変換で処理することができる。コンパイラは main という関数がコンパイルされていることに気づきます。そして、本体の末尾に到達できる可能性があるかどうかをチェックします。もしそうなら return 0;