main()真的是一个C ++程序的开始吗?

时间:2011-01-24 14:54:11

标签: c++ standards-compliance main entry-point

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()不是程序的 start ;它实际上是这个程序的结束


编辑:

如何定义“开始”一词?

归结为“程序开始”这个短语的定义。那么你究竟如何定义呢?

12 个答案:

答案 0 :(得分:86)

您正在错误地阅读该句子。

  

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

标准是为了标准的其余部分而定义“开始”一词。它并没有说在调用main之前没有代码执行。它表示程序的开头被认为是函数main

您的计划符合规定。在main启动之前,您的程序尚未“启动”。根据标准中“start”的定义,在程序“启动”之前调用构造函数,但这几乎不重要。在每个程序中调用main 永远之前执行了大量代码,而不仅仅是这个例子。

出于讨论的目的,您的构造函数代码在程序的“开始”之前执行,并且完全符合标准。

答案 1 :(得分:82)

不,C ++在调用main之前做了很多事情来“设置环境”;但是,main是C ++程序中“用户指定”部分的正式开始。

某些环境设置不可控(就像设置std :: cout的初始代码;但是,某些环境可以像静态全局块一样控制(用于初始化静态全局变量)。注意,因为你不能在main之前有完全控制权,你没有完全控制静态块初始化的顺序。

在主要之后,您的代码在概念上“完全控制”程序,在某种意义上,您既可以指定要执行的指令,也可以指定执行它们的顺序。多线程可以重新排列代码执行顺序;但是,你仍然可以控制C ++,因为你指定让代码段执行(可能)乱序。

答案 2 :(得分:23)

您的程序不会链接,因此除非有主程序,否则不会运行。但是main()不会导致程序执行的开始,因为文件级别的对象具有预先运行的构造函数,并且可以在main()到达之前编写一个运行其生命周期的整个程序,并让main本身具有一个空虚的身体。

实际上要强制执行此操作,您必须拥有一个在main及其构造函数之前构造的对象来调用程序的所有流程。

看看这个:

class Foo
{
public:
   Foo();

 // other stuff
};

Foo foo;

int main()
{
}

您的计划流程实际上源于Foo::Foo()

答案 3 :(得分:15)

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

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

  

对象的初始值设定项中的所有表达式   静态存储持续时间应为常量表达式或字符串文字。

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

如果您只对C ++标准感兴趣,可能需要删除“C”标记。

答案 4 :(得分:10)

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

答案 5 :(得分:9)

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

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

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

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

  

所有具有静态存储的对象   持续时间应初始化(设置为   他们的初始值)在节目之前   启动。这样的方式和时间   否则初始化   未指定的。

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

从那时起,计算机和操作系统已经发展,并且C用于比原先预期的更广泛的区域。现代PC OS具有虚拟地址等,并且所有嵌入式系统都从ROM而不是RAM执行代码。因此,有很多情况下RAM无法“自动”设置。

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

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

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

答案 6 :(得分:4)

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

另见:Avoiding the main (entry point) in a C program

答案 7 :(得分:4)

您的“程序”只是从全局变量返回一个值。其他一切都是初始化代码。因此,标准成立 - 你只需要一个非常简单的程序和更复杂的初始化。

答案 8 :(得分:2)

看起来像英语语义狡辩。 OP首先将他的代码块称为“代码”,然后将其称为“程序”。用户编写代码,然后编译器编写程序。

答案 9 :(得分:1)

初始化所有全局变量后调用

main。

标准未指定的是所有模块和静态链接库的所有全局变量的初始化顺序。

答案 10 :(得分:0)

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

答案 11 :(得分:0)

Ubuntu 20.04 glibc 2.31 RTFS + GDB

glibc在main之前进行了一些设置,以便其某些功能可以使用。让我们尝试跟踪它的源代码。

hello.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。

该函数具有十亿个ifdef,这可以从glibc的遗留/通用性的水平来预期,但是一些对我们似乎有效的关键部分应该简化为:

# 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 we know is the entry point,因为链接描述文件包含:

ENTRY(_start)

因此是动态加载程序完成后实际执行的第一条指令。

为确认这一点,在GDB中,我们通过使用-static进行编译来摆脱了动态加载器:

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

然后创建GDB stop at the very first instruction executed with startiprint the first instructions

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使得bt中的main没有显示太多信息:

#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

通过抓取_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 ++程序时:

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 ++中因素都很好。

待办事项: