早在盘古开天之前 (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函数调用过程,真正的情况要把这个更复杂。

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

一个菜鸟程序猿的成长之路。116篇原创内容
公众号
完
推荐阅读

↙戳链接一秒找到我有观点分享,请留言↘
编程 · 目录
上一篇 下一篇
C语言 Memory Layout 什么是哈希 Hash ?
阅读原文