Linux中main是如何执行的
Linux中main是如何执行的#
这是一个看似简单的问题,但是要从Linux底层一点点研究问题比较多。找到了一遍研究这个问题的文章,但可能比较老了,还是在x86机器上进行的测试。
开始##
问题很简单:linux是怎么执行我的main()函数的?
在这片文档中,我将使用下面的一个简单c程序来阐述它是如何工作的。这个c程序的文件叫做"simple.c"
main()
{
return (0);
}
编译##
gcc -o simple simple.c
生成可执行文件simple.
在可执行文件中有些什么?##
为了看到在可执行文件中有什么,我们使用一个工具"objdump"
objdump -f simple
simple: file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482d0
输出给出了一些关键信息。首先,这个文件的格式是"ELF64"。其次是给出了程序执行的开始地址 "0x080482d0"
什么是ELF?##
ELF是执行和链接格式(Execurable and Linking Format)的缩略词。它是UNIX系统的几种可执行文件格式中的一种。对于我们的这次探讨,有关ELF的有意思的地方是它的头格式。每个ELF可执行文件都有ELF头,像下面这个样子:
typedef struct
{
unsigned char e_ident[EI_NIDENT]; /* Magic number and other info */
Elf32_Half e_type; /* Object file type */
Elf32_Half e_machine; /* Architecture */
Elf32_Word e_version; /* Object file version */
Elf32_Addr e_entry; /* Entry point virtual address */
Elf32_Off e_phoff; /* Program header table file offset */
Elf32_Off e_shoff; /* Section header table file offset */
Elf32_Word e_flags; /* Processor-specific flags */
Elf32_Half e_ehsize; /* ELF header size in bytes */
Elf32_Half e_phentsize; /* Program header table entry size */
Elf32_Half e_phnum; /* Program header table entry count */
Elf32_Half e_shentsize; /* Section header table entry size */
Elf32_Half e_shnum; /* Section header table entry count */
Elf32_Half e_shstrndx; /* Section header string table index */
} Elf32_Ehdr;
上面的结构中,"e_entry"字段是可执行文件的开始地址。
地址"0x080482d0"上存放的是什么?是程序执行的开始地址么?##
对于这个问题,我们来对"simple"做一下反汇编。有几种工具可以用来对可执行文件进行反汇编。我在这里使用了objdump:
objdump --disassemble simple
输出结果有点长,我不会分析objdump的所有输出。我们的意图是看一下地址0x080482d0上存放的是什么。下面是输出:
080482d0 <_start>:
80482d0: 31 ed xor %ebp,%ebp
80482d2: 5e pop %esi
80482d3: 89 e1 mov %esp,%ecx
80482d5: 83 e4 f0 and $0xfffffff0,%esp
80482d8: 50 push %eax
80482d9: 54 push %esp
80482da: 52 push %edx
80482db: 68 20 84 04 08 push $0x8048420
80482e0: 68 74 82 04 08 push $0x8048274
80482e5: 51 push %ecx
80482e6: 56 push %esi
80482e7: 68 d0 83 04 08 push $0x80483d0
80482ec: e8 cb ff ff ff call 80482bc <_init+0x48>
80482f1: f4 hlt
80482f2: 89 f6 mov %esi,%esi
看上去开始地址上存放的是叫做"_start"的启动例程。它所做的是清空寄存器,向栈中push一些数据并且调用一个函数。
Stack Top -------------------
0x80483d
-------------------
esi
-------------------
ecx
-------------------
0x8048274
-------------------
0x8048420
-------------------
edx
-------------------
esp
-------------------
eax
-------------------
三个问题##
现在,可能你已经想到了,关于这个栈帧我们有一些问题。
- 这些16进制数是什么?
- 地址80482bc上存放的是什么,哪个函数被_start调用了?
- 看起来这些汇编指令并没有用一些有意义的值来初始化寄存器。那么谁来初始化这些寄存器?
让我们来一个一个回答这个问题。
Q1>关于16进制数###
如果你仔细研究了用objdump得到的反汇编输出,你就能很容易回答这个问题。
下面是这个问题的回答:
0x80483d0: 这是main()函数的地址。
0x8048274: _init()函数的地址。
0x8048420: _finit()函数地址。
_init和_finit是GCC提供的initialization/finalization 函数。
现在,我们不要去关心这些东西。基本上所有这些16进制数都是函数指针。
Q2>地址80482bc上存放的是什么?###
让我们再次在反汇编输出中寻找地址80482bc。
如果你看到了,汇编代码如下:
80482bc: ff 25 48 95 04 08 jmp *0x8049548
这里的*0x8049548是一个指针操作。它跳到地址0x8049548存储的地址值上。
更多关于ELF和动态链接####
使用ELF,我们可以编译出一个可执行文件,它动态链接到几个libraries上。这里的"动态链接"意味着实际的链接过程发生在运行时。否则我们就得编译出一个巨大的可执行文件,这个文件包含了它所调用的所有libraries("一个『静态链接的可执行文件』")。如果你执行下面的命令:
ldd simple
libc.so.6 => /lib/i686/libc.so.6 (0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
你就能看到simple动态链接的所有libraries。所有动态链接的数据和函数都有『动态重定向入口(dynamic relocation entry)』。
这个概念粗略的讲述如下:
- 在链接时我们不会得知一个动态符号的实际地址。只有在运行时我们才能知道这个实际地址。
- 所以对于动态符号,我们为其实际地址预留出了存储单元。加载器会在运行时用动态符号的实际地址填充存储单元。
- 我们的应用通过使用一种指针操作来间接得知动态符号的存储单元。在我们的例子中,在地址80482bc上,有一个简单的jump指令。jump到的单元由加载器在运行时存储到地址0x8049548上。
我们通过使用objdump命令可以看到所有的动态链接入口:
objdump -R simple
simple: file format elf32-i386
DYNAMIC RELOCATION RECORDS
OFFSET TYPE VALUE
0804954c R_386_GLOB_DAT __gmon_start__
08049540 R_386_JUMP_SLOT __register_frame_info
08049544 R_386_JUMP_SLOT __deregister_frame_info
08049548 R_386_JUMP_SLOT __libc_start_main
这里的地址0x8049548被叫做"JUMP SLOT",非常贴切。根据这个表,实际上我们想调用的是 __libc_start_main。
__libc_start_main是什么?####
我们在玩一个接力游戏,现在球被传到了libc的手上。__libc_start_main是libc.so.6中的一个函数。如果你在glibc中查找__libc_start_main的源码,它的原型可能是这样的:
extern int BP_SYM (__libc_start_main) (int (*main) (int, char **, char **),
int argc,
char *__unbounded *__unbounded ubp_av,
void (*init) (void),
void (*fini) (void),
void (*rtld_fini) (void),
void *__unbounded stack_end)
__attribute__ ((noreturn));
所有汇编指令需要做的就是建立一个参数栈然后调用__libc_start_main。这个函数需要做的是建立/初始化一些数据结构/环境然后调用我们的main()。让我们看一下关于这个函数原型的栈帧,
Stack Top -------------------
0x80483d0 main
-------------------
esi argc
-------------------
ecx argv
-------------------
0x8048274 _init
-------------------
0x8048420 _fini
-------------------
edx _rtlf_fini
-------------------
esp stack_end
-------------------
eax this is 0
-------------------
根据这个栈帧我们得知,esi,ecx,edx,esp,eax寄存器在函数 __libc_start_main()被执行前需要被填充合适的值。很清楚的是这些寄存器不是被前面我们所展示的启动汇编指令所填充的。那么,谁填充了这些寄存器呢?现在只留下唯一的一个地方了——内核。现在让我们回到第三个问题上。
Q3>内核做了些什么?###
当我们通过在shell上输入一个名字来执行一个程序时,下面是Linux接下来会发生的:
- Shell调用内核的带argc/argv参数的系统调用"execve"。
- 内核的系统调用句柄开始处理这个系统调用。在内核代码中,这个句柄为"sys_execve".在x86机器上,用户模式的应用会通过以下寄存器将所有需要的参数传递到内核中。
- ebx:执行程序名字的字符串
- ecx:argv数组指针
- edx:环境变量数组指针
- 通用的execve内核系统调用句柄——也就是do_execve——被调用。它所做的是建立一个数据结构,将所有用户空间数据拷贝到内核空间,最后调用search_binary_handler()。Linux能够同时支持多种可执行文件格式,例如a.out和ELF。对于这个功能,存在一个数据结构"struct linux_binfmt",对于每个二进制格式的加载器在这个数据结构都会有一个函数指针。search_binary_handler()会找到一个合适的句柄并且调用它。在我们的例子中,这个合适的句柄是load_elf_binary()。解释函数的每个细节是非常乏味的工作。所以我在这里就不这么做了。如果你感兴趣,阅读相关的书籍即可。接下来是函数的结尾部分,首先为文件操作建立内核数据结构,来读入ELF映像。然后它建立另一个内核数据结构,这个数据结构包含:代码容量,数据段开始处,堆栈段开始处,等等。然后为这个进程分配用户模式页,将argv和环境变量拷贝到分配的页面地址上。最后,argc和argv指针,环境变量数组指针通过create_elf_tables()被push到用户模式堆栈中,使用start_thread()让进程开始执行起来。
当执行_start汇编指令时,栈帧会是下面这个样子。
Stack Top -------------
argc
-------------
argv pointer
-------------
env pointer
-------------
汇编指令通过以下方式从栈中获取所有信息:
pop %esi <--- get argc
move %esp, %ecx <--- get argv
actually the argv address is the same as the current
stack pointer.
现在所有东西都准备好了,可以开始执行了。
其他的寄存器呢?##
对于esp来说,它被用来当做应用程序的栈底。在弹出所有必要信息之后,_start例程简单的调整了栈指针(esp)——关闭了esp寄存器4个低地址位,这完全是有道理的,对于我们的main程序,这就是栈底。对于edx,它被rtld_fini使用,这是一种应用析构函数,内核使用下面的宏定义将它设为0:
#define ELF_PLAT_INIT(_r) do { \
_r->ebx = 0; _r->ecx = 0; _r->edx = 0; \
_r->esi = 0; _r->edi = 0; _r->ebp = 0; \
_r->eax = 0; \
} while (0)
0意味着在x86 Linux上我们不会使用这个功能。
关于汇编指令##
这些汇编codes来自哪里?它是GCC codes的一部分。这些code的目标文件通常在/usr/lib/gcc-lib/i386-redhat-linux/XXX 和 /usr/lib下面,XXX是gcc版本号。文件名为crtbegin.o,crtend.o和gcrt1.o。
总结##
我们总结一下整个过程。
- GCC将你的程序同crtbegin.o/crtend.o/gcrt1.o一块进行编译。其它默认libraries会被默认动态链接。可执行程序的开始地址被设置为_start。
- 内核加载可执行文件,并且建立正文段,数据段,bss段和堆栈段,特别的,内核为参数和环境变量分配页面,并且将所有必要信息push到堆栈上。
- 控制流程到了_start上面。_start从内核建立的堆栈上获取所有信息,为__libc_start_main建立参数栈,并且调用__libc_start_main。
- __libc_start_main初始化一些必要的东西,特别是C library(比如malloc)线程环境并且调用我们的main函数。
- 我们的main会以main(argv,argv)来被调用。事实上,这里有意思的一点是main函数的签名。__libc_start_main认为main的签名为main(int, char **, char **),如果你感到好奇,尝试执行下面的程序。
main(int argc, char** argv, char** env)
{
int i = 0;
while(env[i] != 0)
{
printf("%s\n", env[i++]);
}
return(0);
}
结论##
在Linux中,我们的C main()函数由GCC,libc和Linux二进制加载器的共同协作来执行。
参考##
objdump "man objdump"
ELF header /usr/include/elf.h
__libc_start_main glibc source
./sysdeps/generic/libc-start.c
sys_execve linux kernel source code
arch/i386/kernel/process.c
do_execve linux kernel source code
fs/exec.c
struct linux_binfmt linux kernel source code
include/linux/binfmts.h
load_elf_binary linux kernel source code
fs/binfmt_elf.c
create_elf_tables linux kernel source code
fs/binfmt_elf.c
start_thread linux kernel source code
include/asm/processor.h
Linux中main是如何执行的的更多相关文章
- Linux中Main函数的执行过程
1. 问题:Linux如何执行main函数. 本文使用一个简单的C程序(simple.c)作为例子讲解.代码如下, int main() { return(0); } 2. 编译 -#gcc -o ...
- Linux中的定时自动执行功能(at,crontab)
Linux中的定时自动执行功能(at,crontab) 概念 在Linux系统中,提供了两种提前对工作进行安排的方式 at 只执行一次 crontab 周期性重复执行 通过对这两个工具的应用可以让我们 ...
- linux中使用Crontab定时执行java的jar包无法使用环境变量的问题
1.crontab简单使用 cmd 其实就是5个星星的事情,随便百度一下吧 5个时间标签用来标注执行的设定.比如每5分钟执行一次/5 * * * cmd 要特别注意 2.有些命令在命令行里执行很好,到 ...
- 在浏览器中打开php文件时,是Linux中的哪个用户执行的?
https://segmentfault.com/q/1010000002541340 如题,这样我就可以针对这个用户设置权限了.而且这个用户是怎么关联上的,怎么查看? 解答一: .是执行 PHP 指 ...
- [转]Linux中文件权限目录权限的意义及权限对文件目录的意义
转自:http://www.jb51.net/article/77458.htm linux中目录与文件权限的意义 一.文件权限的意义 r:可以读这个文件的具体内容: w:可以编辑这个文件的内容,包括 ...
- linux中shell编程
shell编程 1 echo -e 识别\转义符 \a \b \t \n \x十六进制 \0八进制 等等 #!/bin/bash echo -e "hello world" 执行脚 ...
- Linux中安装python3
[centos7中安装python3]http://blog.csdn.net/wjqwinn/article/details/75633714 (一)安装python3前的准备工作1.修改文件中第一 ...
- 在Linux中配置jdk,Tomcat,MySQL
解压缩: tar 命令 : 使用方式 tar [参数] source [target] source - 压缩文件 target - 解压缩后的目标位置, 默认解压到当前目录 常用写法 : 解压缩 : ...
- Linux中的入口函数main
main()函数,想必大家都不陌生了,从刚开始写程序的时候,大家便开始写main(),我们都知道main是程序的入口.那main作为一个函数,又是谁调用的它,它是怎么被调用的,返回给谁,返回的又是什么 ...
随机推荐
- 关于APP分享到QQ、微信等
<script> var shares=null; var Intent=null,File=null,Uri=null,main=null; function plusRe ...
- 将Editplus添加到右键打开菜单
因为自己一直用Editplus作为文本打开工具,新的电脑将压缩文件复制了过来,但是没有右键打开了. 第一打开注册表 在命令框中输入regedit 第二在注册表中输入选项 如下图所示在下拉菜单中新建Ed ...
- Sql Server——运用代码创建数据库及约束
在没有学习运用代码创建数据库.表和约束之前,我们只能用鼠标点击操作,这样看起来就不那么直观(高大上)了. 在写代码前要知道在哪里写和怎么运行: 点击新建查询,然后中间的白色空白地方就是写代码的地方了. ...
- js 倒计时(服务器时间同步)
首先说一下,为什么要服务器时间同步, 因为服务器时间和本地电脑时间存在一定的时间差.有些对时效性要求非常高的应用,例如时时彩开奖,是不能容忍这种时间差存在的. 方案1:每次倒计时去服务端请求时间 // ...
- css控制div强制换行
div{white-space:nowrap;} 自动换行 div{ word-wrap: break-word; word-break: normal; } 强制英文单词断行 div{word-br ...
- Beauty Contest 凸包+旋转卡壳法
Beauty Contest Time Limit: 3000MS Memory Limit: 65536K Total Submissions: 27507 Accepted: 8493 D ...
- FPGA与安防领域
安防主要包括:闭路监控系统.防盗报警系统.楼宇对讲系统.停车厂管理系统.小区一卡通系统.红外周界报警系统.电子围栏.巡更系统.考勤门禁系统.安防机房系统.电子考场系统.智能门锁等等. 在监控系统中,F ...
- Java 继承、抽象、接口
一.继承 1. 概述 继承是面向对象的重要特征之一,当多个类中存在相同的属性和行为时,将这些内容抽取到单独一个类中,那多个类中无需再定义这些属性和行为,只需继承那个单独的类即可. 单独的类称为父类或超 ...
- Interface request structure used for socket ioctl's
1. 结构体定义 /* * Interface request structure used for socket * ioctl's. All interface ioctl's must have ...
- Jmeter脚本录制方法(二)——手工编写脚本(jmeter与fiddler结合使用)
jmeter脚本录制方法可以分三种,前几天写的一篇文章中,已介绍了前两种,今天来说下第三种,手工编写脚本,建议使用这一种方法,虽然写的过程有点繁琐,但调试脚本比前两者方式都要便捷. 首先来看下三种方式 ...