main函数和启动例程
为什么汇编程序的入口是_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.c
、xxx.s
、xxx.o
和a.out
),例如gcc main.o -o main
将main.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.o
和crti.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 节 “可执行文件”我们看到链接器起到重定位的作用,这两种作用都是通过修改指令中的地址实现的,链接器也是一种编辑器,vi
和emacs
编辑的是源文件,而链接器编辑的是目标文件,所以链接器也叫Link Editor。T _start
这一行表示_start
这个符号在crt1.o
中提供了定义,这个符号的类型是代码(T表示Text)。我们从上面的输出结果中选取几个符号用图示说明它们之间的关系:
图 19.3. C程序的链接过程

其实上面我们写的ld
命令做了很多简化,gcc
在链接时还用到了另外几个目标文件,所以上图多画了一个框,表示组成可执行文件main
的除了main.o
、crt1.o
和crti.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
中的未定义符号main
在main.o
中定义了,所以链接在一起就没问题了。crt1.o
还有一个未定义符号__libc_start_main
在其它几个目标文件中也没有定义,所以在可执行文件main
中仍然是个未定义符号。这个符号是在libc
中定义的,libc
并不像其它目标文件一样链接到可执行文件main
中,而是在运行时做动态链接:
操作系统在加载执行
main
这个程序时,首先查看它有没有需要动态链接的未定义符号。如果需要做动态链接,就查看这个程序指定了哪些共享库(我们用
-lc
指定了libc
)以及用什么动态链接器来做动态链接(我们用-dynamic-linker /lib/ld-linux.so.2
指定了动态链接器)。动态链接器在共享库中查找这些符号的定义,完成链接过程。
了解了这些原理之后,现在我们来看_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 $0x80483c4
是main
函数的地址,__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函数和启动例程的更多相关文章
- 第七章之main函数和启动例程
main函数和启动例程 为什么汇编程序的入口是_start,而C程序的入口是main函数呢?本节就来解释这个问题.在讲例 18.1 “最简单的汇编程序”时,我们的汇编和链接步骤是: $ as hell ...
- [汇编与C语言关系]2. main函数与启动例程
为什么汇编程序的入口是_start,而C程序的入口是main函数呢?以下就来解释这个问题 在<x86汇编程序基础(AT&T语法)>一文中我们汇编和链接的步骤是: $ as hell ...
- WPF 用Main函数方式启动程序
原文:WPF 用Main函数方式启动程序 WPF默认程序启动:新建project后自动生成的App.xaml中指定程序启动方式(StartupUri="MainWindow.xaml&quo ...
- Linux0.11从开机到准备执行main函数的启动学习
最近一直在看操作系统以及内核设计的东西,不确定自己有能力会参与到类似的开发之中,但是争取能自己改造这内核玩一下,然后按照Linux From Scratch那样的把改造后的系统编译运行就心满意足了.正 ...
- spring-boot 使用 main函数 无法启动的问题完美 解决方案。
首先 是启动之后 ,直接回exit code 0,网址 里面输入localhost:8080显示站点未启动.网上查 了多种 方式 ,日志 也 打了,都没发现问题,最后到这篇文章里 找到了答案.但是这 ...
- 分布式缓存系统 Memcached 主线程之main函数
前两节中对工作线程的工作流程做了较为详细的分析,现把其主要流程总结为下图: 接下来本节主要分析主线程相关的函数设计,主函数main的基本流程如下图所示: 对于主线程中的工作线程的初始化到启动所有的工作 ...
- 菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)
俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中 ...
- PostgreSQL启动main函数都干了什么(一)
DB Version:9.5.3 环境:CentOS7.x 调试工具:GDB source:src/backend/main/main.c 56 /* 57 * Any Postgres server ...
- 通过启动函数定位main()函数
如下,通过vc6.0编写一个hello world程序.尝试结合汇编代码.分析启动函数找到main函数. 在printf(xxx)插入断点,调试执行.如下,在堆栈窗口中可见main()下的一个 ...
随机推荐
- python解析页面上json字段
一般来说,当我们从一个网页上拿下来数据,就是一个字符串,比如: url_data = urllib2.urlopen(url).readline() 当我们这样得到页面数据,url_data是全部页面 ...
- Collection中的排序
我们来了解一下Collection的框架与接口: Set接口下面已经有SortedSet接口,其中提供了很多自带排序的实现类,例如ThreeSet,用户还能够自定义比较器来规定自己的排序规则. 本篇着 ...
- QT 按钮类继承处理带定时器
01.class KeyButton : public QPushButton 02.{ 03. Q_OBJECT 04.public: 05. explicit KeyButto ...
- 创建并配置Filter
创建Filter需要两个步骤: 创建FIlter处理类. web.xml文件中配置Filter. 创建Filter类 创建Filter必须实现javax.servlet.Filter接口,在该接口中定 ...
- 不再让内容把td撑开
<style type="text/css"> table {width:600px;table-layout:fixed;} td {white-space:nowr ...
- UIToolbar swift
// // ViewController.swift // UILabelTest // // Created by mac on 15/6/23. // Copyright (c) 2015年 fa ...
- [shell基础]——I/O重定向
文件标识符(FD) 1. Linux使用文件标识符(FD)来标识一个进程正在访问的特定文件 2. 当打开一个文件或创建一个文件时,Linux将返回一个文件标识符供其他操作引用 3. 文件标识符是一个小 ...
- 软件工程课后作业——用JAVA编写的随机产生30道四则运算
package com.java.sizeyunsuan; public class lianxi { String f() { int i=(int)(Math.random()*10); int ...
- Vim实用命令
[n]yy:从当前行复制n行 [n]p:粘贴n次 [n]dd:删除当前行往下的n行 / : 向后查找 ?:向前查找 u → undo 撤销上一操作 <C-r> → redo 0 → 开启 ...
- C#如何设置Listview的行高-高度
Winform窗口中,控件listview是无法设置行高的. 以加入一个imagelist(图片列表控件)实现行高的设置. ImageList imageList = new ImageList(); ...