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 的整数的平方。main()

我还使用-pedantic选项 GCC 4.5.0 编译它。它没有错误,甚至没有警告!

所以我的问题是,

这段代码真的符合标准吗?

如果它符合标准,那么它不会使标准所说的无效吗?main()不是这个程序的开始!user_main()之前执行的main()

我知道要初始化全局变量main_retuse_main()首先要执行,但这完全是另一回事;关键是,它确实使标准中引用的语句 $3.6.1/1 无效,因为main()它不是程序的开始;这实际上是这个程序的结束


编辑:

你如何定义“开始”这个词?

它归结为“程序开始”这个短语的定义。那么具体怎么定义呢?

4

12 回答 12

91

你读错了句子。

一个程序应包含一个名为 main 的全局函数,它是程序的指定开始。

该标准为该标准的其余部分定义了“开始”一词。它并不是说在main调用之前没有代码执行。它说程序的开始被认为是在 function 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 之前构造的对象及其构造函数来调用程序的所有流程。

看这个:

class Foo
{
public:
   Foo();

 // other stuff
};

Foo foo;

int main()
{
}

您的程序流程将有效地源于Foo::Foo()

于 2011-01-24T14:58:58.317 回答
15

您也将问题标记为“C”,然后,严格来说 C,根据 ISO C99 标准的第 6.7.8 节“初始化”,您的初始化应该失败。

在这种情况下,最相关的似乎是约束 #4,它说:

具有静态存储持续时间的对象的初始化程序中的所有表达式都应为常量表达式或字符串文字。

因此,您的问题的答案是代码不符合 C 标准。

如果您只对 C++ 标准感兴趣,您可能希望删除“C”标签。

于 2011-01-24T15:13:54.460 回答
10

3.6 节作为一个整体非常清楚地说明了main动态初始化和动态初始化的交互。“程序的指定开始”不在其他任何地方使用,仅描述main(). 以与标准中更详细和明确的要求相矛盾的规范方式来解释这一短语是没有任何意义的。

于 2011-01-24T15:37:42.627 回答
9

编译器通常必须在 main() 之前添加代码以符合标准。因为标准规定必须在程序执行之前完成全局/静态的初始化。如前所述,放置在文件范围(全局)的对象的构造函数也是如此。

因此,最初的问题与 C 相关,因为在 C 程序中,您仍然需要在程序启动之前进行全局/静态初始化。

标准假定这些变量是通过“魔术”初始化的,因为它们没有说明在程序初始化之前应该如何设置它们。我认为他们认为这超出了编程语言标准的范围。

编辑:参见例如 ISO 9899:1999 5.1.2:

所有具有静态存储持续时间的对象都应在程序启动之前进行初始化(设置为其初始值)。这种初始化的方式和时间是未指定的。

如何完成这种“魔法”背后的理论可以追溯到 C 的诞生,当时它是一种编程语言,仅用于基于 RAM 的计算机上的 UNIX 操作系统。理论上,该程序将能够将所有预初始化数据从可执行文件加载到 RAM 中,同时将程序本身上传到 RAM。

从那时起,计算机和操作系统不断发展,C 语言的使用范围比最初预期的要广泛得多。现代 PC 操作系统具有虚拟地址等,所有嵌入式系统都从 ROM 执行代码,而不是 RAM。所以有很多情况下无法“自动”设置 RAM。

此外,该标准过于抽象,无法了解堆栈和进程内存等。这些事情也必须在程序启动之前完成。

因此,几乎每个 C/C++ 程序都有一些在调用 main 之前执行的 init/“copy-down”代码,以符合标准的初始化规则。

例如,嵌入式系统通常有一个称为“非 ISO 兼容启动”的选项,其中出于性能原因跳过整个初始化阶段,然后代码实际上直接从 main 启动。但是这样的系统不符合标准,因为您不能依赖全局/静态变量的初始值。

于 2011-01-24T15:47:08.823 回答
4

main() 是由 C 运行时库调用的用户函数。

另请参阅:避免 C 程序中的 main(入口点)

于 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 之前做了一些设置,以便它的一些功能可以工作。让我们尝试追踪它的源代码。

你好ç

#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 的遗留/通用性级别可以预期,该函数有十亿个 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);
}

Before __libc_start_mainare already at _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>

通过查找源代码_start并关注 x86_64 命中,我们看到这似乎对应于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按预期调用。

不幸-static的是,btmain没有显示尽可能多的信息:

#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

通过 grepping 来源_dl_start_user似乎来自sysdeps/x86_64/dl-machine.h:L147

/* 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++ 程序时:

你好.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++ 中都很好地考虑了这些因素。

去做:

于 2020-09-29T09:19:20.700 回答
0

是的,main 是每个 C++ 程序的“入口点”,特定于实现的扩展除外。即便如此,一些事情还是发生在 main 之前,特别是全局初始化,例如 main_ret。

于 2011-01-24T15:01:12.200 回答