Main()真的是 C + + 程序的开始吗?

C + + 标准的 $3.6.1/1部分写道,

一个程序应该包含一个全局 函数 总台,它是 指定为程序的 开始

现在考虑这个代码,

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选项编译了它,GCC4.5.0。它没有错误,甚至没有警告!

所以我的问题是,

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

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

我理解为了初始化全局变量 main_retuse_main()首先执行,但这是完全不同的事情; 关键是,它 是的使引用的语句 $3.6.1/1从标准失效,因为 main()不是程序的 开始; 它实际上是 这个程序的 结束


编辑:

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

它可以归结为短语 “节目开始”的定义。那么你如何确切地定义它呢?

24988 次浏览

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

实际上,要实现这一点,您必须在 main 及其构造函数之前构造一个对象,以调用程序的所有流。

看看这个:

class Foo
{
public:
Foo();


// other stuff
};


Foo foo;


int main()
{
}

程序的流程实际上来自 Foo::Foo()

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

参见: 避免 C 程序中的主要(入口点)

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

您也将问题标记为“ C”,因此,严格地说,按照 ISO C99标准的6.7.8“初始化”部分,您的初始化应该会失败。

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

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

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

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

您的“程序”只是从全局变量返回一个值。其他的都是初始化代码。这样,标准就成立了——你只有一个非常琐碎的程序和更复杂的初始化。

作为一个整体,3.6节对于 main和动态初始化的交互非常清楚。“指定的程序启动”在其他任何地方都不使用,它只是对 main()的一般意图的描述。用一种与标准中更详细、更清晰的要求相矛盾的规范方式来解释这个短语是没有任何意义的。

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

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

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

编辑: 参见 ISO 9899:19995.1.2:

所有具有静态存储的对象 持续时间应初始化(设置为 他们的初始值) 启动的方式和时间 初始化则不然 没有具体说明。

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

从那时起,计算机和操作系统得到了发展,C 语言的应用范围比最初预想的要广得多。现代 PC 操作系统有虚拟地址等,所有嵌入式系统执行的代码从 ROM,而不是 RAM。因此,在许多情况下,RAM 不能被“自动”设置。

而且,这个标准太抽象了,根本不了解堆栈和进程内存等等。在程序启动之前,这些事情也必须完成。

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

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

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

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

在 main 之后,您的代码在概念上是“完全控制”程序的,在这个意义上,您可以指定要执行的指令和执行它们的顺序。多线程可以重新安排代码执行顺序; 但是,您仍然可以使用 C + + 控制代码执行顺序,因为您指定了代码执行部分(可能)无序。

你读错了这个句子。

程序应该包含一个名为 main 的全局函数 这是节目的指定开始。

该标准是为了标准的其余部分的目的而定义“开始”这个词。它没有说在调用 main之前没有代码执行。它说程序的开始被认为是在函数 main处。

你的程序是顺从的。在 main 启动之前,您的程序还没有“启动”。根据标准中“开始”的定义,在程序“启动”之前调用该函数,但这并不重要。在 main在每个程序中都是 永远不会调用之前,要执行大量的代码,而不仅仅是这个例子。

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

在初始化所有全局变量之后调用 main。

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

看起来像是英语语义学上的吹毛求疵。OP 首先将他的代码块称为“代码”,然后称为“程序”用户编写代码,然后编译器编写程序。

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

现在在广东发展银行:

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的调用者的行。

这个函数有10亿个 ifdefs,可以从 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是入口,因为链接器脚本包含:

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

通过抓取 _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时相同的位置,然后 -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 + + 之间有很好的分解。

待办事项: