133

C++標準のセクション$3.6.1/ 1には、次のように書かれています。

プログラムには、プログラムの指定された開始点であるmainと呼ばれるグローバル関数が含まれている必要があります。

ここで、このコードについて考えてみましょう。

int square(int i) { return i*i; }
int user_main()
{ 
    for ( int i = 0 ; i < 10 ; ++i )
           std::cout << square(i) << endl;
    return 0;
}
int main_ret= user_main();
int main() 
{
        return main_ret;
}

このサンプルコードは、プログラムの「開始」となる関数に入る前に、0から9までの整数の2乗を出力するという、私が意図したことを実行します。main()

-pedanticまた、オプションのGCC4.5.0を使用してコンパイルしました。エラーは発生せず、警告も発生しません。

だから私の質問は、

このコードは本当に標準に準拠していますか?

それが標準に準拠している場合、それは標準が言っていることを無効にしませんか?main()このプログラムの開始ではありません!user_main()の前に実行されmain()ます。

main_retグローバル変数を初期化するには、が最初に実行されることを理解していますuse_main()が、それはまったく別のことです。重要なのは、プログラムの開始ではないので標準から引用されたステートメント$ 3.6.1/1を無効にするということです。実際、このプログラムはこれで終わりです。main()


編集:

「開始」という言葉をどのように定義しますか?

それは、「プログラムの開始」というフレーズの定義に要約されます。では、どのように正確に定義しますか?

4

12 に答える 12

91

文章を間違って読んでいます。

プログラムには、プログラムの指定された開始点であるmainと呼ばれるグローバル関数が含まれている必要があります。

この規格は、規格の残りの部分の目的のために「開始」という言葉を定義しています。が呼び出される前にコードが実行されないということではありませんmain。プログラムの開始は関数であると見なされると書かれていますmain

プログラムは準拠しています。mainが開始されるまで、プログラムは「開始」されていません。この関数は、標準の「開始」の定義に従ってプログラムが「開始」する前に呼び出されますが、それはほとんど問題ではありません。この例だけでなく、すべてのプログラムで呼び出される前に、大量のコードが実行されmainます

説明のために、関数はプログラムの「開始」の前に実行され、それは標準に完全に準拠しています。

于 2011-01-24T16:26:55.077 に答える
86

いいえ、C ++は、mainを呼び出す前に「環境を設定」するために多くのことを行います。ただし、mainは、C++プログラムの「ユーザー指定」部分の正式な開始です。

一部の環境設定は制御できません(std :: coutを設定する初期コードなど)。ただし、一部の環境は静的グローバルブロック(静的グローバル変数を初期化するため)のように制御できます。 mainの前に制御すると、静的ブロックが初期化される順序を完全に制御できません。

mainの後、実行する命令と実行順序の両方を指定できるという意味で、コードは概念的にプログラムを「完全に制御」します。マルチスレッドは、コードの実行順序を並べ替えることができます。ただし、コードのセクションを(おそらく)順不同で実行するように指定したため、C++を引き続き制御できます。

于 2011-01-24T16:11:30.817 に答える
24

メインがない限り、プログラムはリンクしないため、実行されません。ただし、ファイルレベルのオブジェクトには事前に実行されるコンストラクターがあり、main()に到達する前にその存続期間を実行するプログラム全体を記述して、main自体に実行させることができるため、main()はプログラムの実行を開始しません。空の体。

実際には、これを強制するには、プログラムのすべてのフローを呼び出すために、mainとそのコンストラクターの前に構築される1つのオブジェクトが必要になります。

これを見てください:

class Foo
{
public:
   Foo();

 // other stuff
};

Foo foo;

int main()
{
}

あなたのプログラムの流れは効果的にFoo::Foo()

于 2011-01-24T14:58:58.317 に答える
15

質問にも「C」のタグを付けました。厳密に言えば、Cについて言えば、ISOC99標準のセクション6.7.8「初期化」に従って初期化は失敗するはずです。

この場合に最も関連性があるのは、次のような制約#4のようです。

静的な保存期間を持つオブジェクトの初期化子のすべての式は、定数式または文字列リテラルでなければなりません。

したがって、あなたの質問に対する答えは、コードがC標準に準拠していないということです。

C ++標準のみに関心がある場合は、「C」タグを削除することをお勧めします。

于 2011-01-24T15:13:54.460 に答える
10

セクション3.6は全体として、main動的初期化の相互作用について非常に明確です。「プログラムの指定された開始」は他の場所では使用されておらず、の一般的な意図を説明しているにすぎませんmain()。その1つのフレーズを、標準のより詳細で明確な要件と矛盾する規範的な方法で解釈することは意味がありません。

于 2011-01-24T15:37:42.627 に答える
9

コンパイラは、標準に準拠するために、main()の前にコードを追加しなければならないことがよくあります。標準では、プログラムを実行する前にグローバル/スタティックの初期化を実行する必要があると指定されているためです。また、前述のように、ファイルスコープ(グローバル)に配置されたオブジェクトのコンストラクターについても同じことが言えます。

したがって、元の質問Cにも関連します。これは、Cプログラムでは、プログラムを開始する前にグローバル/静的初期化を実行する必要があるためです。

標準では、これらの変数は「魔法」によって初期化されると想定しています。これは、プログラムの初期化前に変数をどのように設定するかが規定されていないためです。彼らはそれをプログラミング言語標準の範囲外のものだと考えていたと思います。

編集:たとえば、ISO 9899:19995.1.2を参照してください。

静的ストレージ期間を持つすべてのオブジェクトは、プログラムの起動前に初期化(初期値に設定)する必要があります。そのような初期化の方法とタイミングは、他の点では指定されていません。

この「魔法」がどのように行われるかについての背後にある理論は、RAMベースのコンピューターでUNIXOS専用に使用されることを目的としたプログラミング言語であったCの誕生にまでさかのぼります。理論的には、プログラム自体がRAMにアップロードされると同時に、プログラムは事前に初期化されたすべてのデータを実行可能ファイルからRAMにロードできます。

それ以来、コンピュータとOSは進化し、Cは当初の予想よりもはるかに広い領域で使用されています。最新のPCOSには仮想アドレスなどがあり、すべての組み込みシステムはRAMではなくROMからコードを実行します。そのため、RAMを「自動的に」設定できない状況はたくさんあります。

また、標準は抽象的すぎて、スタックやプロセスメモリなどについて何も知ることができません。これらのことも、プログラムを開始する前に行う必要があります。

したがって、ほとんどすべてのC / C ++プログラムには、標準の初期化ルールに準拠するために、mainが呼び出される前に実行されるinit/"copy-down"コードがあります。

例として、組み込みシステムには通常、「非ISO準拠の起動」と呼ばれるオプションがあり、パフォーマンス上の理由から初期化フェーズ全体がスキップされ、コードは実際にはメインから直接開始されます。ただし、グローバル/静的変数のinit値に依存できないため、このようなシステムは標準に準拠していません。

于 2011-01-24T15:47:08.823 に答える
4

main()は、Cランタイムライブラリによって呼び出されるユーザー関数です。

参照:Cプログラムのメイン(エントリポイント)の回避

于 2011-01-24T14:59:45.707 に答える
4

「プログラム」は、グローバル変数から値を返すだけです。それ以外はすべて初期化コードです。したがって、標準が成り立ちます-あなたは非常に些細なプログラムとより複雑な初期化を持っているだけです。

于 2011-01-24T15:14:47.400 に答える
2

英語のセマンティクスはごちゃごちゃしているようです。OPは、彼のコードブロックを最初に「コード」と呼び、後で「プログラム」と呼びます。ユーザーがコードを記述し、次にコンパイラーがプログラムを記述します。

于 2014-06-27T01:31:22.220 に答える
1

mainは、すべてのグローバル変数を初期化した後に呼び出されます。

標準で指定されていないのは、すべてのモジュールと静的にリンクされたライブラリのすべてのグローバル変数の初期化の順序です。

于 2011-01-24T22:36:37.727 に答える
1

Ubuntu 20.04 glibc 2.31 RTFS + GDB

glibcは、その機能の一部が機能するように、mainの前にいくつかのセットアップを行います。そのためのソースコードを追跡してみましょう。

こんにちはC

#include <stdio.h>

int main() {
    puts("hello");
    return 0;
}

コンパイルとデバッグ:

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out

現在GDBにあります:

b main
r
bt -past-main

与える:

#0  main () at hello.c:3
#1  0x00007ffff7dc60b3 in __libc_start_main (main=0x555555555149 <main()>, argc=1, argv=0x7fffffffbfb8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffbfa8) at ../csu/libc-start.c:308
#2  0x000055555555508e in _start ()

これには、mainの呼び出し元の行がすでに含まれています:https://github.com/cirosantilli/glibc/blob/glibc-2.31/csu/libc-start.c#L308。

この関数には、glibcのレガシー/一般性のレベルから予想できるように、10億のifdefがありますが、私たちにとって有効であると思われるいくつかの重要な部分は、次のように単純化する必要があります。

# define LIBC_START_MAIN __libc_start_main

STATIC int
LIBC_START_MAIN (int (*main) (int, char **, char **),
         int argc, char **argv,
{

      /* Initialize some stuff. */

      result = main (argc, argv, __environ MAIN_AUXVEC_PARAM);
  exit (result);
}

以前__libc_start_mainはすでにになっていますが、リンカースクリプトには次のものが含まれているため_start、これを追加gcc -Wl,--verboseすることでエントリポイントであることがわかります。

ENTRY(_start)

したがって、これはダイナミックローダーが終了した後に実行される実際の最初の命令です。

GDBでそれを確認するために、次のコマンドを使用して動的ローダーを削除し-staticます。

gcc -ggdb3 -O0 -std=c99 -Wall -Wextra -pedantic -o hello.out hello.c
gdb hello.out

次に、で実行された最初の命令でGDBを停止させstarti、最初の命令を出力します。

starti
display/12i $pc

これは次のようになります。

=> 0x401c10 <_start>:   endbr64 
   0x401c14 <_start+4>: xor    %ebp,%ebp
   0x401c16 <_start+6>: mov    %rdx,%r9
   0x401c19 <_start+9>: pop    %rsi
   0x401c1a <_start+10>:        mov    %rsp,%rdx
   0x401c1d <_start+13>:        and    $0xfffffffffffffff0,%rsp
   0x401c21 <_start+17>:        push   %rax
   0x401c22 <_start+18>:        push   %rsp
   0x401c23 <_start+19>:        mov    $0x402dd0,%r8
   0x401c2a <_start+26>:        mov    $0x402d30,%rcx
   0x401c31 <_start+33>:        mov    $0x401d35,%rdi
   0x401c38 <_start+40>:        addr32 callq 0x4020d0 <__libc_start_main>

x86_64ヒットのソースを調べて_start焦点を当てると、これは次のように対応しているように見えますsysdeps/x86_64/start.S:58


ENTRY (_start)
    /* Clearing frame pointer is insufficient, use CFI.  */
    cfi_undefined (rip)
    /* Clear the frame pointer.  The ABI suggests this be done, to mark
       the outermost frame obviously.  */
    xorl %ebp, %ebp

    /* Extract the arguments as encoded on the stack and set up
       the arguments for __libc_start_main (int (*main) (int, char **, char **),
           int argc, char *argv,
           void (*init) (void), void (*fini) (void),
           void (*rtld_fini) (void), void *stack_end).
       The arguments are passed via registers and on the stack:
    main:       %rdi
    argc:       %rsi
    argv:       %rdx
    init:       %rcx
    fini:       %r8
    rtld_fini:  %r9
    stack_end:  stack.  */

    mov %RDX_LP, %R9_LP /* Address of the shared library termination
                   function.  */
#ifdef __ILP32__
    mov (%rsp), %esi    /* Simulate popping 4-byte argument count.  */
    add $4, %esp
#else
    popq %rsi       /* Pop the argument count.  */
#endif
    /* argv starts just at the current stack top.  */
    mov %RSP_LP, %RDX_LP
    /* Align the stack to a 16 byte boundary to follow the ABI.  */
    and  $~15, %RSP_LP

    /* Push garbage because we push 8 more bytes.  */
    pushq %rax

    /* Provide the highest stack address to the user code (for stacks
       which grow downwards).  */
    pushq %rsp

#ifdef PIC
    /* Pass address of our own entry points to .fini and .init.  */
    mov __libc_csu_fini@GOTPCREL(%rip), %R8_LP
    mov __libc_csu_init@GOTPCREL(%rip), %RCX_LP

    mov main@GOTPCREL(%rip), %RDI_LP
#else
    /* Pass address of our own entry points to .fini and .init.  */
    mov $__libc_csu_fini, %R8_LP
    mov $__libc_csu_init, %RCX_LP

    mov $main, %RDI_LP
#endif

    /* Call the user's main function, and exit with its value.
       But let the libc call main.  Since __libc_start_main in
       libc.so is called very early, lazy binding isn't relevant
       here.  Use indirect branch via GOT to avoid extra branch
       to PLT slot.  In case of static executable, ld in binutils
       2.26 or above can convert indirect branch into direct
       branch.  */
    call *__libc_start_main@GOTPCREL(%rip)

期待どおりに呼び出す__libc_start_mainことになります。

残念ながら-staticbtfrommainはそれほど多くの情報を表示しません:

#0  main () at hello.c:3
#1  0x0000000000402560 in __libc_start_main ()
#2  0x0000000000401c3e in _start ()

を削除-staticしてから開始するとstarti、代わりに次のようになります。

=> 0x7ffff7fd0100 <_start>:     mov    %rsp,%rdi
   0x7ffff7fd0103 <_start+3>:   callq  0x7ffff7fd0df0 <_dl_start>
   0x7ffff7fd0108 <_dl_start_user>:     mov    %rax,%r12
   0x7ffff7fd010b <_dl_start_user+3>:   mov    0x2c4e7(%rip),%eax        # 0x7ffff7ffc5f8 <_dl_skip_args>
   0x7ffff7fd0111 <_dl_start_user+9>:   pop    %rdx

これのソースをgrepすることにより、 sysdeps / x86_64 / dl-machine.h:L147_dl_start_userから来ているようです。

/* Initial entry point code for the dynamic linker.
   The C function `_dl_start' is the real entry point;
   its return value is the user program's entry point.  */
#define RTLD_START asm ("\n\
.text\n\
    .align 16\n\
.globl _start\n\
.globl _dl_start_user\n\
_start:\n\
    movq %rsp, %rdi\n\
    call _dl_start\n\
_dl_start_user:\n\
    # Save the user entry point address in %r12.\n\
    movq %rax, %r12\n\
    # See if we were run as a command with the executable file\n\
    # name as an extra leading argument.\n\
    movl _dl_skip_args(%rip), %eax\n\
    # Pop the original argument count.\n\
    popq %rdx\n\

これはおそらく動的ローダーのエントリポイントです。

で中断し_startて続行すると、これは、を使用したときと同じ場所にあるように見えます。-staticこれにより、が呼び出されます__libc_start_main

代わりにC++プログラムを試してみると:

hello.cpp

#include <iostream>

int main() {
    std::cout << "hello" << std::endl;
}

と:

g++ -ggdb3 -O0 -std=c++11 -Wall -Wextra -pedantic -o hello.out hello.cpp

結果は基本的に同じです。たとえば、のバックトレースmainはまったく同じです。

C ++コンパイラは、C ++固有の機能を実現するためにフックを呼び出しているだけであり、C /C++全体でかなりうまく考慮されていると思います。

TODO:

于 2020-09-29T09:19:20.700 に答える
0

はい、mainは、実装固有の拡張機能を除いて、すべてのC++プログラムの「エントリポイント」です。それでも、mainの前にいくつかのことが起こります。特に、main_retなどのグローバル初期化です。

于 2011-01-24T15:01:12.200 に答える