为什么汇编程序的入口是_start,而C程序的入口是main函数呢?本节就来解释这个问题。在讲例 18.1 “最简单的汇编程序”时,我们的汇编和链接步骤是:

$ as hello.s -o hello.o
$ ld hello.o -o hello

以前我们常用gcc main.c -o main命令编译一个程序,其实也可以分三步做,第一步生成汇编代码,第二步生成目标文件,第三步生成可执行文件:

$ gcc -S main.c
$ gcc -c main.s
$ gcc main.o

-S选项生成汇编代码,-c选项生成目标文件,此外在第 2 节 “数组应用实例:统计随机数”还讲过-E选项只做预处理而不编译,如果不加这些选项则gcc执行完整的编译步骤,直到最后链接生成可执行文件为止。如下图所示。

图 19.2. gcc命令的选项

这些选项都可以和-o搭配使用,给输出的文件重新命名而不使用gcc默认的文件名(xxx.cxxx.sxxx.oa.out),例如gcc main.o -o mainmain.o链接成可执行文件main。先前由汇编代码例 18.1 “最简单的汇编程序”生成的目标文件hello.o我们是用ld来链接的,可不可以用gcc链接呢?试试看。

$ gcc hello.o -o hello
hello.o: In function `_start':
(.text+0x0): multiple definition of `_start'
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o:(.text+0x0): first defined here
/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o: In function `_start':
(.text+0x18): undefined reference to `main'
collect2: ld returned 1 exit status

提示两个错误:一是_start有多个定义,一个定义是由我们的汇编代码提供的,另一个定义来自/usr/lib/crt1.o;二是crt1.o_start函数要调用main函数,而我们的汇编代码中没有提供main函数的定义。从最后一行还可以看出这些错误提示是由ld给出的。由此可见,如果我们用gcc做链接,gcc其实是调用ld将目标文件crt1.o和我们的hello.o链接在一起。crt1.o里面已经提供了_start入口点,我们的汇编程序中再实现一个_start就是多重定义了,链接器不知道该用哪个,只好报错。另外,crt1.o提供的_start需要调用main函数,而我们的汇编程序中没有实现main函数,所以报错。

如果目标文件是由C代码编译生成的,用gcc做链接就没错了,整个程序的入口点是crt1.o中提供的_start,它首先做一些初始化工作(以下称为启动例程,Startup Routine),然后调用C代码中提供的main函数。所以,以前我们说main函数是程序的入口点其实不准确,_start才是真正的入口点,而main函数是被_start调用的。

我们继续研究上一节的例 19.1 “研究函数的调用过程”。如果分两步编译,第二步gcc main.o -o main其实是调用ld做链接的,相当于这样的命令:

$ ld /usr/lib/crt1.o /usr/lib/crti.o main.o -o main -lc -dynamic-linker /lib/ld-linux.so.2

也就是说,除了crt1.o之外其实还有crti.o,这两个目标文件和我们的main.o链接在一起生成可执行文件main-lc表示需要链接libc库,在第 1 节 “数学函数”讲过-lc选项是gcc默认的,不用写,而对于ld则不是默认选项,所以要写上。-dynamic-linker /lib/ld-linux.so.2指定动态链接器是/lib/ld-linux.so.2,稍后会解释什么是动态链接。

那么crt1.ocrti.o里面都有什么呢?我们可以用readelf命令查看。在这里我们只关心符号表,如果只看符号表,可以用readelf命令的-s选项,也可以用nm命令。

$ nm /usr/lib/crt1.o
00000000 R _IO_stdin_used
00000000 D __data_start
U __libc_csu_fini
U __libc_csu_init
U __libc_start_main
00000000 R _fp_hw
00000000 T _start
00000000 W data_start
U main
$ nm /usr/lib/crti.o
U _GLOBAL_OFFSET_TABLE_
w __gmon_start__
00000000 T _fini
00000000 T _init

U main这一行表示main这个符号在crt1.o中用到了,但是没有定义(U表示Undefined),因此需要别的目标文件提供一个定义并且和crt1.o链接在一起。具体来说,在crt1.o中要用到main这个符号所代表的地址,例如有一条指令是push $符号main所代表的地址,但不知道这个地址是多少,所以在crt1.o中这条指令暂时写成push $0x0,等到和main.o链接成可执行文件时就知道这个地址是多少了,比如是0x80483c4,那么可执行文件main中的这条指令就被链接器改成了push $0x80483c4。链接器在这里起到符号解析(Symbol Resolution)的作用,在第 5.2 节 “可执行文件”我们看到链接器起到重定位的作用,这两种作用都是通过修改指令中的地址实现的,链接器也是一种编辑器,viemacs编辑的是源文件,而链接器编辑的是目标文件,所以链接器也叫Link Editor。T _start这一行表示_start这个符号在crt1.o中提供了定义,这个符号的类型是代码(T表示Text)。我们从上面的输出结果中选取几个符号用图示说明它们之间的关系:

图 19.3. C程序的链接过程

其实上面我们写的ld命令做了很多简化,gcc在链接时还用到了另外几个目标文件,所以上图多画了一个框,表示组成可执行文件main的除了main.ocrt1.ocrti.o之外还有其它目标文件,本书不做深入讨论,用gcc-v选项可以了解详细的编译过程:

$ gcc -v main.c -o main
Using built-in specs.
Target: i486-linux-gnu
...
/usr/lib/gcc/i486-linux-gnu/4.3.2/cc1 -quiet -v main.c -D_FORTIFY_SOURCE=2 -quiet -dumpbase main.c -mtune=generic -auxbase main -version -fstack-protector -o /tmp/ccRGDpua.s
...
as -V -Qy -o /tmp/ccidnZ1d.o /tmp/ccRGDpua.s
...
/usr/lib/gcc/i486-linux-gnu/4.3.2/collect2 --eh-frame-hdr -m elf_i386 --hash-style=both -dynamic-linker /lib/ld-linux.so.2 -o main -z relro /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crt1.o /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crti.o /usr/lib/gcc/i486-linux-gnu/4.3.2/crtbegin.o -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2 -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib -L/lib/../lib -L/usr/lib/../lib -L/usr/lib/gcc/i486-linux-gnu/4.3.2/../../.. /tmp/ccidnZ1d.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i486-linux-gnu/4.3.2/crtend.o /usr/lib/gcc/i486-linux-gnu/4.3.2/../../../../lib/crtn.o

链接生成的可执行文件main中包含了各目标文件所定义的符号,通过反汇编可以看到这些符号的定义:

$ objdump -d main
main: file format elf32-i386 Disassembly of section .init: 08048274 <_init>:
8048274: 55 push %ebp
8048275: 89 e5 mov %esp,%ebp
8048277: 53 push %ebx
...
Disassembly of section .text: 080482e0 <_start>:
80482e0: 31 ed xor %ebp,%ebp
80482e2: 5e pop %esi
80482e3: 89 e1 mov %esp,%ecx
...
08048394 <bar>:
8048394: 55 push %ebp
8048395: 89 e5 mov %esp,%ebp
8048397: 83 ec 10 sub $0x10,%esp
...
080483aa <foo>:
80483aa: 55 push %ebp
80483ab: 89 e5 mov %esp,%ebp
80483ad: 83 ec 08 sub $0x8,%esp
...
080483c4 <main>:
80483c4: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c8: 83 e4 f0 and $0xfffffff0,%esp
80483cb: ff 71 fc pushl -0x4(%ecx)
...
Disassembly of section .fini: 0804849c <_fini>:
804849c: 55 push %ebp
804849d: 89 e5 mov %esp,%ebp
804849f: 53 push %ebx

crt1.o中的未定义符号mainmain.o中定义了,所以链接在一起就没问题了。crt1.o还有一个未定义符号__libc_start_main在其它几个目标文件中也没有定义,所以在可执行文件main中仍然是个未定义符号。这个符号是在libc中定义的,libc并不像其它目标文件一样链接到可执行文件main中,而是在运行时做动态链接:

  1. 操作系统在加载执行main这个程序时,首先查看它有没有需要动态链接的未定义符号。

  2. 如果需要做动态链接,就查看这个程序指定了哪些共享库(我们用-lc指定了libc)以及用什么动态链接器来做动态链接(我们用-dynamic-linker /lib/ld-linux.so.2指定了动态链接器)。

  3. 动态链接器在共享库中查找这些符号的定义,完成链接过程。

了解了这些原理之后,现在我们来看_start的反汇编:

...
Disassembly of section .text: 080482e0 <_start>:
80482e0: 31 ed xor %ebp,%ebp
80482e2: 5e pop %esi
80482e3: 89 e1 mov %esp,%ecx
80482e5: 83 e4 f0 and $0xfffffff0,%esp
80482e8: 50 push %eax
80482e9: 54 push %esp
80482ea: 52 push %edx
80482eb: 68 00 84 04 08 push $0x8048400
80482f0: 68 10 84 04 08 push $0x8048410
80482f5: 51 push %ecx
80482f6: 56 push %esi
80482f7: 68 c4 83 04 08 push $0x80483c4
80482fc: e8 c3 ff ff ff call 80482c4 <__libc_start_main@plt>
...

首先将一系列参数压栈,然后调用libc的库函数__libc_start_main做初始化工作,其中最后一个压栈的参数push $0x80483c4main函数的地址,__libc_start_main在完成初始化工作之后会调用main函数。由于__libc_start_main需要动态链接,所以这个库函数的指令在可执行文件main的反汇编中肯定是找不到的,然而我们找到了这个:

Disassembly of section .plt:
...
080482c4 <__libc_start_main@plt>:
80482c4: ff 25 04 a0 04 08 jmp *0x804a004
80482ca: 68 08 00 00 00 push $0x8
80482cf: e9 d0 ff ff ff jmp 80482a4 <_init+0x30>

这三条指令位于.plt段而不是.text段,.plt段协助完成动态链接的过程。我们将在下一章详细讲解动态链接的过程。

main函数最标准的原型应该是int main(int argc, char *argv[]),也就是说启动例程会传两个参数给main函数,这两个参数的含义我们学了指针以后再解释。我们到目前为止都把main函数的原型写成int main(void),这也是C标准允许的,如果你认真分析了上一节的习题,你就应该知道,多传了参数而不用是没有问题的,少传了参数却用了则会出问题。

由于main函数是被启动例程调用的,所以从main函数return时仍返回到启动例程中,main函数的返回值被启动例程得到,如果将启动例程表示成等价的C代码(实际上启动例程一般是直接用汇编写的),则它调用main函数的形式是:

exit(main(argc, argv));

也就是说,启动例程得到main函数的返回值后,会立刻用它做参数调用exit函数。exit也是libc中的函数,它首先做一些清理工作,然后调用上一章讲过的_exit系统调用终止进程,main函数的返回值最终被传给_exit系统调用,成为进程的退出状态。我们也可以在main函数中直接调用exit函数终止进程而不返回到启动例程,例如:

#include <stdlib.h>

int main(void)
{
exit(4);
}

这样和int main(void) { return 4; }的效果是一样的。在Shell中运行这个程序并查看它的退出状态:

$ ./a.out
$ echo $?
4

按照惯例,退出状态为0表示程序执行成功,退出状态非0表示出错。注意,退出状态只有8位,而且被Shell解释成无符号数,如果将上面的代码改为exit(-1);return -1;,则运行结果为

$ ./a.out
$ echo $?
255

注意,如果声明一个函数的返回值类型是int,函数中每个分支控制流程必须写return语句指定返回值,如果缺了return则返回值不确定(想想这是为什么),编译器通常是会报警告的,但如果某个分支控制流程调用了exit_exit而不写return,编译器是允许的,因为它都没有机会返回了,指不指定返回值也就无所谓了。使用exit函数需要包含头文件stdlib.h,而使用_exit函数需要包含头文件unistd.h,以后还要详细解释这两个函数。

main函数和启动例程的更多相关文章

  1. 第七章之main函数和启动例程

    main函数和启动例程 为什么汇编程序的入口是_start,而C程序的入口是main函数呢?本节就来解释这个问题.在讲例 18.1 “最简单的汇编程序”时,我们的汇编和链接步骤是: $ as hell ...

  2. [汇编与C语言关系]2. main函数与启动例程

    为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题 在<x86汇编程序基础(AT&T语法)>一文中我们汇编和链接的步骤是: $ as hell ...

  3. WPF 用Main函数方式启动程序

    原文:WPF 用Main函数方式启动程序 WPF默认程序启动:新建project后自动生成的App.xaml中指定程序启动方式(StartupUri="MainWindow.xaml&quo ...

  4. Linux0.11从开机到准备执行main函数的启动学习

    最近一直在看操作系统以及内核设计的东西,不确定自己有能力会参与到类似的开发之中,但是争取能自己改造这内核玩一下,然后按照Linux From Scratch那样的把改造后的系统编译运行就心满意足了.正 ...

  5. spring-boot 使用 main函数 无法启动的问题完美 解决方案。

    首先 是启动之后 ,直接回exit code  0,网址 里面输入localhost:8080显示站点未启动.网上查 了多种 方式 ,日志 也 打了,都没发现问题,最后到这篇文章里 找到了答案.但是这 ...

  6. 分布式缓存系统 Memcached 主线程之main函数

    前两节中对工作线程的工作流程做了较为详细的分析,现把其主要流程总结为下图: 接下来本节主要分析主线程相关的函数设计,主函数main的基本流程如下图所示: 对于主线程中的工作线程的初始化到启动所有的工作 ...

  7. 菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)

    俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中 ...

  8. PostgreSQL启动main函数都干了什么(一)

    DB Version:9.5.3 环境:CentOS7.x 调试工具:GDB source:src/backend/main/main.c 56 /* 57 * Any Postgres server ...

  9. 通过启动函数定位main()函数

      如下,通过vc6.0编写一个hello world程序.尝试结合汇编代码.分析启动函数找到main函数.   在printf(xxx)插入断点,调试执行.如下,在堆栈窗口中可见main()下的一个 ...

随机推荐

  1. 扯一扯前端css的整体架构设计:(2)base基础类的那些事儿

    周一下午在实验室写了第一篇博文,有几个人捧场,那咱就得接着下去啊.然后我觉得现在写的内容更多的偏向于谈一下我对于前端css架构的理解和前端经验的一个小总结,所以就把标题里原来的[项目总结]给删掉了.但 ...

  2. moses:processPhraseTable被删除

    今年一月,processPhraseTable被删除了,具体原因如下: https://www.mail-archive.com/moses-support@mit.edu/msg11372.html ...

  3. Android 使用Fragment,ViewPagerIndicator 制作csdn app主要框架

    转载  转载请注明出处:http://blog.csdn.net/lmj623565791/article/details/23513993 本来准备下载个CSDN的客户端放手机上,没事可以浏览浏览资 ...

  4. SQL1092N The requested command or operation failed because the user ID does not have the authority to perform the requested command or operation.

    1.前一天安装号db2后,做了如下处理: ************************************************************ 修改 /etc/sudoers 文件 ...

  5. hdu 5281 Senior's Gun

    题目连接 http://acm.hdu.edu.cn/showproblem.php?pid=5281 Senior's Gun Description Xuejiejie is a beautifu ...

  6. Spring框架中的IOC和DI的区别

    上次面试被问到IOC和DI的区别时,没怎么在意,昨天又被问到,感觉有点可惜.今晚总算抽点时间,查看了spring官方文档.发现,IoC更像是一种思想,DI是一种行为.为了降低程序的耦合度,利用spri ...

  7. centos rsync安装配置

    安装 1 yum -y install rsync ---------------------服务器安装------------------------------- 创建基础配置文件 1 2 3 4 ...

  8. PHP错误The server encountered an internal error or misconfiguration and was unable to complete your re

    我的笔记本电脑上的环境安装了很多次,但是运行项目时总是会报The server encountered an internal error or misconfiguration and was un ...

  9. Html5 常见的新增API详解

    1. getElementsByClassName()方法 getElementsByClassName()方法接收一个参数,即一个包含一或多个类名的字符串,返回带有指定类的所有元素的NodeList ...

  10. android开发类似coverflow效果的3d旋转

    源码下载地址:http://download.csdn.net/detail/feijian_/8888219