在C语言中,main()方法最初是如何被调用的?

67

C程序如何启动?


18
你需要调用main()函数!是的,没错,就是这样!没错,由你来调用! - recursive
10
谁监督监视者? - Cascabel
9
主题是一个陈述句,但正文是一个疑问句。 - Brian R. Bondy
4
为什么,贾斯汀,为什么?这是诗歌啊! - jball
3
哇,这个问题的发展方式让我想起了“如何在LOGO中移动海龟”的问题。 - Brian
显示剩余2条评论
7个回答

56
操作系统调用main()函数。最终。
许多Unix操作系统使用的可执行和可链接格式(ELF)定义了一个入口地址和INIT地址。这是程序在操作系统完成其exec()调用后开始运行的地方。在Linux系统上,这是.init部分中的_init。在此返回后,它跳转到入口点地址,即.text部分中的_start
C编译器将标准库链接到每个应用程序中,该库提供这些操作系统定义的初始化和入口点。然后该库调用main()
以下是我示例的C源代码:
#include <stdio.h>

int main() {
  puts("Hello world!");
  return 0;
}

来自 objdump -d

Disassembly of section .init:

0000000000001000 <_init>:
    1000:   f3 0f 1e fa             endbr64 
    1004:   48 83 ec 08             sub    $0x8,%rsp
    1008:   48 8b 05 d9 2f 00 00    mov    0x2fd9(%rip),%rax        # 3fe8 <__gmon_start__>
    100f:   48 85 c0                test   %rax,%rax
    1012:   74 02                   je     1016 <_init+0x16>
    1014:   ff d0                   callq  *%rax
    1016:   48 83 c4 08             add    $0x8,%rsp
    101a:   c3                      retq   

Disassembly of section .text:

0000000000001060 <_start>:
    1060:   f3 0f 1e fa             endbr64 
    1064:   31 ed                   xor    %ebp,%ebp
    1066:   49 89 d1                mov    %rdx,%r9
    1069:   5e                      pop    %rsi
    106a:   48 89 e2                mov    %rsp,%rdx
    106d:   48 83 e4 f0             and    $0xfffffffffffffff0,%rsp
    1071:   50                      push   %rax
    1072:   54                      push   %rsp
    1073:   4c 8d 05 66 01 00 00    lea    0x166(%rip),%r8        # 11e0 <__libc_csu_fini>
    107a:   48 8d 0d ef 00 00 00    lea    0xef(%rip),%rcx        # 1170 <__libc_csu_init>
    1081:   48 8d 3d c1 00 00 00    lea    0xc1(%rip),%rdi        # 1149 <main>
    1088:   ff 15 52 2f 00 00       callq  *0x2f52(%rip)          # 3fe0 <__libc_start_main@GLIBC_2.2.5>
    108e:   f4                      hlt    
    108f:   90                      nop

0000000000001140 <frame_dummy>:
    1140:   f3 0f 1e fa             endbr64 
    1144:   e9 77 ff ff ff          jmpq   10c0 <register_tm_clones>

通过 readelf -h 可以看到入口地址与 _start 匹配:

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x1060
  Start of program headers:          64 (bytes into file)
  Start of section headers:          17416 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         13
  Size of section headers:           64 (bytes)
  Number of section headers:         36
  Section header string table index: 35

来自 readelf -d

Dynamic section at offset 0x2dc8 contains 27 entries:
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11e8
 0x0000000000000019 (INIT_ARRAY)         0x3db8
 0x000000000000001b (INIT_ARRAYSZ)       8 (bytes)
 0x000000000000001a (FINI_ARRAY)         0x3dc0
 0x000000000000001c (FINI_ARRAYSZ)       8 (bytes)
 0x000000006ffffef5 (GNU_HASH)           0x3a0
 0x0000000000000005 (STRTAB)             0x470
 0x0000000000000006 (SYMTAB)             0x3c8
 0x000000000000000a (STRSZ)              130 (bytes)
 0x000000000000000b (SYMENT)             24 (bytes)
 0x0000000000000015 (DEBUG)              0x0
 0x0000000000000003 (PLTGOT)             0x3fb8
 0x0000000000000002 (PLTRELSZ)           24 (bytes)
 0x0000000000000014 (PLTREL)             RELA
 0x0000000000000017 (JMPREL)             0x5e0
 0x0000000000000007 (RELA)               0x520
 0x0000000000000008 (RELASZ)             192 (bytes)
 0x0000000000000009 (RELAENT)            24 (bytes)
 0x000000000000001e (FLAGS)              BIND_NOW
 0x000000006ffffffb (FLAGS_1)            Flags: NOW PIE
 0x000000006ffffffe (VERNEED)            0x500
 0x000000006fffffff (VERNEEDNUM)         1
 0x000000006ffffff0 (VERSYM)             0x4f2
 0x000000006ffffff9 (RELACOUNT)          3
 0x0000000000000000 (NULL)               0x0

你可以看到INIT等于_init的地址。
INIT_ARRAY中也有一整个函数指针数组。参见objdump -s -j .init_array c-test:
c-test:     file format elf64-x86-64

Contents of section .init_array:
 3db8 40110000 00000000                    @.......        

你可以看到,地址0x3db8与ELF头文件中的INIT_ARRAY相同。
地址0x1140(记住来自40110000的小端字节布局)是函数frame_dummy,在反汇编中可以看到。然后调用register_tm_clones和其他函数。
初始化代码在一组名为crtbegin.o和crtend.o(以及这些名称的变体)的文件中。__libc_start_main函数在libc.so.6中定义。这些库是GCC的一部分。该代码执行了C程序所需的各种操作,如设置stdin、stdout、全局和静态变量等等。
以下文章非常好地描述了Linux中它的作用(摘自下面得票较少的答案):http://dbp-consulting.com/tutorials/debugging/linuxProgramStartup.html 我相信其他人的答案已经描述了Windows的做法。

4
它不调用 _init 或其他任何函数,而是调用入口地址。入口地址可以是任何位置。 - Andrey
需要 libstdc++.so.6 吗?这不是来自 C 程序! - Jens
@Jens:在这种情况下没有任何区别。 - Zan Lynx
2
@ZanLynx 你怎么知道的?我怎么知道呢?为什么不直接看一个C程序呢?C和C++是非常不同的语言,程序启动就是其中一个显著区别(例如调用构造函数)。 - Jens
@Jens:既然你不相信我,那么你怎样才能判断呢?可以使用objdump -d 反汇编器来查看机器码,并使用readelf -d来查看ELF文件。或者获取一个十六进制编辑器和ELF规范的副本。 - Zan Lynx
1
@ZanLynx 我已经做了所有的事情。当问题是关于 C 语言时,我质疑证明 C++ 程序的有效性。特别是当在真正的 C 程序上执行相同的分析非常容易时。用 C 编译器编译一个 C 程序然后运行 objdump,这有多难?你不会用 FORTRAN 程序演示同样的东西,对吧? :-) - Jens

26

最终它是操作系统。通常在真正的入口点和主函数之间有一些中介,这是由链接器插入的。

一些细节(与Windows相关):PE文件中有一个称为IMAGE_OPTIONAL_HEADER的头,其中包含字段AddressOfEntryPoint,这又是文件中将要执行的第一个代码字节的地址。


3
иҝҷ并дёҚжҳҜзј–иҜ‘еҷЁжҸ’е…Ҙзҡ„пјҢиҖҢжҳҜй“ҫжҺҘеҷЁжҸ’е…Ҙзҡ„пјҢйҖҡеёёжҳҜйҖҡиҝҮй“ҫжҺҘеҲ°зұ»дјјдәҺcrt.a(crt.o)жҲ–crt.lib(crt.obj)иҝҷж ·зҡ„дёңиҘҝжқҘе®һзҺ°зҡ„пјҢиҖҢиҝҷйҖҡеёёжҳҜзұ»дјјдәҺlibc.aжҲ–c.libзҡ„дёҖйғЁеҲҶгҖӮ - Christian Hujer
1
@ChristianHujer 你说得对,我所指的编译器其实是工具链。 - Andrey
你确定是关于链接器(linker)的吗?我猜你知道装载器(loader)是做什么的? - roottraveller
1
@roottraveller 链接器进行链接。可执行文件甚至可能永远不会被加载。 - Andrey

11
操作系统调用main函数。在可重定位可执行文件中,会有一个指向main函数位置的地址(有关更多信息,请参阅Unix ABI)。
但是,谁调用操作系统呢?
当中央处理单元接收到“复位”信号(也在上电时发出)时,它将开始在某个ROM中查找给定地址(比如0xffff)的指令。
通常会有一些跳转指令到BIOS,这样就可以配置内存芯片、加载基本硬盘驱动程序等等。然后读取硬盘的引导扇区,启动下一个引导程序,该程序加载包含如何读取NTFS分区以及如何读取内核文件本身的基本信息的文件。内核环境将被设置,内核被加载,然后 - 然后! - 内核将被跳转执行。
在完成所有这些艰苦的工作之后,内核才能继续加载我们的软件。

9

我不知道为什么这个被踩了,链接里有很好的信息。 - zwol
8
@Zack 通常在这里发布链接而没有任何概要是不被看好的。 - Michael Mrozek

5

太好了!非常感谢! - Gab是好人

5
操作系统调用包含在C运行时库(CRT)中的函数并链接到您的可执行文件中。称之为“CRT主函数”。
CRT主函数会执行一些操作,其中至少在C ++中最重要的两个是运行全局C ++类数组并调用它们的构造函数,并调用您的main()函数并将其返回值传递给shell。
如果记忆无误,Visual C ++ CRT主函数会执行更多操作。它配置内存分配器,如果使用Debug CRT来帮助查找内存泄漏或错误访问非常重要。它还在结构化异常处理程序中调用main以捕获错误的内存访问和其他崩溃并显示它们。

1
尽管你在谈论C++而不是C,但是你的帖子很有启发性! - Gab是好人

4

请注意,除了已发布的答案外,您也可以自己调用main函数。一般来说,这是一个保留给混淆代码的坏主意。


5
顺便说一下,在C++中这是不合法的——这是C++不是一个严格的超集的另一种方式。 - David Thornley

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接