早在盘古开天之前 (Main的故事)

不知道你是否有过这样的疑问:为什么C语言总是从main( )开始?定义一个比如brush_your_teeth( )开始行不行?似乎从一开始就觉得天经地义的事情,现在看起来也并不是不可或缺。不如直接编译起来试一试:

  1 void brush_your_teeth(void) { }
  1 Undefined symbols for architecture x86_64:
  2 "_main", referenced from:
  3 implicit entry/start for main executable
  4 ld: symbol(s) not found for architecture x86_64
  5 clang: error: linker command failed with exit code 1 (use -v to see inv

结果直接链接的时候就报错了,看来C语言教材诚不欺我,工程里是真的要有main( )函数。 那么main函数真的就是第一个被执行的函数吗?

一个典型程序的大致运行步骤

操作系统创建进程后,把控制权交到了程序入口,这个入口往往是程序运行库中的某个入口函数。

入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量的构造等等。

入口函数在完成初始化之后,调用main函数,正式开始执行函数主体部分。

main函数执行完毕之后,返回到入口函数,入口函数进行清理工作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。

main函数是如何被调用的 ?

我们编写一个最简单的函数来测试一下 prog1.c :

  1 int main(){ }

我们调用下readelf -h prog1 来看一下elf头信息:

   1 ELF Header:
   2 Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
   3 Class: ELF32
   4 Data: 2's complement, little endian
   5 Version: 1 (current)
   6 OS/ABI: UNIX - System V
   7 ABI Version: 0
   8 Type: EXEC (Executable file)
   9 Machine: ARM
  10 Version: 0x1
  11 Entry point address: 0x102e0
  12 Start of program headers: 52 (bytes into file)
  13 Start of section headers: 7304 (bytes into file)
  14 Flags: 0x5000400, Version5 EABI, hard-flo
  15 Size of this header: 52 (bytes)
  16 Size of program headers: 32 (bytes)
  17 Number of program headers: 9
  18 Size of section headers: 40 (bytes)
  19 Number of section headers: 35
  20 Section header string table index: 34

原来程序的入口地址是:0x102e0,再用objdump -d prog1 看一看反汇编:

   1 000102e0 <_start>:
   2 102e0: e3a0b000 mov fp, #0
   3 102e4: e3a0e000 mov lr, #0
   4 102e8: e49d1004 pop {r1} ; (ldr r1, [sp], #4)
   5 102ec: e1a0200d mov r2, sp
   6 102f0: e52d2004 push {r2} ; (str r2, [sp, #-4]!)
   7 102f4: e52d0004 push {r0} ; (str r0, [sp, #-4]!)
   8 102f8: e59fc010 ldr ip, [pc, #16] ; 10310 <_start+0x30>
   9 102fc: e52dc004 push {ip} ; (str ip, [sp, #-4]!)
  10 10300: e59f000c ldr r0, [pc, #12] ; 10314 <_start+0x34>
  11 10304: e59f300c ldr r3, [pc, #12] ; 10318 <_start+0x38>
  12 10308: ebffffeb bl 102bc <__libc_start_main@plt>
  13 ...

我们可以看出elf的入口地址上,实际执行的是一个 _start 的函数。由一个名为crt0.o的文件提供,该文件包含C运行时环境的启动代码。 _start 会设置一些东西,包括将ebp寄存器( x86 中 ebp = ARM 中 的 fp ) 清 零 , 填 充 参 数 数 组 argv , 统 计 参 数 , 然 后 调用 __libc_start_main 。(不熟悉这块的朋友可以看之前的文章 C语言 Memory Layout 里面的command line部分就是讲这块过程。)综上分析,我们可以把 _start 改成一段更具可读性的伪代码:

  1 void _start() {
  2 %ebp = 0;
  3 int argc = pop from stack
  4 char ** argv = top of stack;
  5 __libc_start_main(main, argc, argv, __libc_csu_init, __linc_csu_fini
  6 edx, top of stack);
  7 }

接下来将会调用__libc_start_main ,这时候已经离我们真正的main函数很近了。

  1 int __libc_start_main( int (*main) (int, char * *, char * *),
  2 int argc, char * * ubp_av,
  3 void (*init) (void),
  4 void (*fini) (void),
  5 void (*rtld_fini) (void),
  6 void (* stack_end));

该函数以程序 main 函数的地址,argc 和 argv 作为输入。init 和 fini 函数分别是程序 的构造函数和析构函数。rtld_fini 是当程序退出时调用的终止函数,用来终止以及释放动 态 段 。 __libc_start_main 函 数 的 最 后 一 个 参 数 是 一 个 指 向 程 序 栈 的 指 针 。 在 __libc_start_main 的结尾,main函数终于被调用,并退出。

  1 result = main(argc, argv, _environ);
  2 exit(result);

当然,这里只是简单讨论了下在linux glibc环境下的main函数调用过程,真正的情况要把这个更复杂。

早在盘古开天之前 (Main的故事):原文配图 1

关于编译和链接相关的内容比较复杂,如果大家感兴趣的话,还可以看看以下文章:

第28期:编译与链接

{荐书} 第2期《程序员的自我修养》

第130期:C语言 Memory Layout

汽车ECU设计

早在盘古开天之前 (Main的故事):原文配图 2

一个菜鸟程序猿的成长之路。116篇原创内容

公众号

完

推荐阅读

早在盘古开天之前 (Main的故事):原文配图 3

↙戳链接一秒找到我有观点分享,请留言↘

编程 · 目录

上一篇 下一篇

C语言 Memory Layout 什么是哈希 Hash ?

阅读原文

← 返回文章列表