Linux系统中,main函数的执行过程

1. 问题:linux如何执行main函数。

本文使用一个简单的C程序(simple.c)作为例子讲解。代码如下,

int main()
{
    return(0);
}

2. 编译

~#gcc -o simple simple.c


3. 查看可执行文件的基本信息

~#objdump -f simple

simple:     file format elf32-i386
architecture: i386, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x080482d0

借助objdump这个工具,可以获得可执行文件的一些关键信息。

比如,simple文件的格式是“ELF32”,该文件的起始地址是0x80482d0,,等。


4. 什么是ELF

ELF是Executable and Linking Format的缩写,是Unix上常见的几种目标文件格式(及可执行文件格式)之一。

ELF的头部结构提供了ELF文件的基本信息,其数据结构可以在/usr/include/elf.h 中看到,如下所示:

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存储了该执行文件的起始地址。


5. 关于起始地址

~#objdump -d simple

 80482d0 <_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

该命令可以得到simple的反汇编代码,可以看到,起始地址0x80482d0对应的是_start这个routine。这段代码所做的事情是,将ebp清0,调整esp的值,然后将一些数据压栈,最后调用一个函数。

看到这段代码,也许你会有以下疑问:

问题1:压栈使用的常量地址对应的是什么?

问题2:地址0x80482bc,是什么代码?

问题3:压栈使用的寄存器里面,存储的是些什么信息?由谁提供这些信息?

我们来一一解答。

问题1

只要认真看一下simple的反汇编输出,就可以得到答案:

0x80483d0是main函数的地址;

0x8048274是_init函数的地址;

0x8048420是_fini函数的地址。

_init和_fini函数是gcc提供的初始化和终止函数。这些函数指针将被传递给地址0x80482bc对应的函数,然后由该函数调用这些函数指针。

问题2

从反汇编输出中查找0x80482bc,可以看到

80482bc:	ff 25 48 95 04 08    	jmp    *0x8049548
可以看到这是一个跳转指令,跳转到0x8049548存储的代码地址。

这种情况的出现,是由动态链接库引起的。以下命令可以查看simple使用的所有动态链接库。

~#ldd simple

	  libc.so.6 => /lib/i686/libc.so.6 (0x42000000)
	  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
动态链接的过程发生在运行时。所以,在链接时,编译器不知道动态符号的地址,只好留下一些空白地址,等运行时由加载器填写动态符号的实际地址。应用程序就通过这些指针操作来间接访问动态符号。在上面的例子中,0x80449548就是这样的一个空白地址,其内容由加载器在运行时填写。

所有的动态链接的数据和函数都有动态重定位的入口,可以用以下命令查看

~#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
可以看到,call 0x80482bc实际上会跳转到__libc_start_main。

那么,__libc_start_main又是干什么的呢?

可以从glibc的源代码目录中sysdeps/generic/libc-start.c看到,__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));

再联系前面_start的代码,可以得到这样的映射:

0x80483d0: main

esi: argc of main

ecx: argv of main

0x8048274: _init

0x8048420: _fini

edx: _rtlf_fini

esp: stack_end

eax: 此时为0

显然,simple的_start部分,没有设置这些寄存器的值。那么,这些参数值是谁设置的呢?

问题3

这些值应该是由内核设置的。

当我们在shell里面调用一个可执行文件,linux会进行以下操作:

(1)shell调用系统调用“execve”,带上参数信息argc/argv。

(2)内核系统调用的handler获得控制,并处理系统调用。在内核代码中,execve对应的handler是"sys_execve"。在x86中,用户模式的应用会使用以下寄存器向内核传递一些必要的参数:

  • ebx: 指向程序名的指针
  • ecx:参数数组指针
  • edx:环境变量数组指针
(3)linux然后调用通用的execve内核系统调用handler,do_execve。该调用建立一个数据结构,并将用户空间的数据拷贝到内核空间,然后调用search_binary_handler。Linux能够同时支持多种可执行文件的格式,比如a.out和ELF 。为了实现这种功能,linux利用一个“struct linux_binfmt”数据结构,存储各种二进制格式的加载器的函数指针。search_binary_handler查询并调用合适的加载器(本例子中是load_elf_binary)。加载器首先建立一个数据结构用来存储ELF文件映像。然后,再建立一个内核数据结构,存储相关信息,包括代码大小,数据段起始地址,栈起始地址,等等。然后,为该程序分配用户模式的页表,并将参数以及环境变量拷贝到页面地址中。最后,create_elf_tables()函数将参数指针,环境变量数组指针压入用户模式的栈;start_thread()函数启动_start开始的进程代码。

当_start标号所在的汇编指令开始执行时,函数的栈帧如下所示:

Stack Top	        -------------
                            argc
                        -------------
                            argv pointer
                        -------------
                            env pointer
                        ------------- 

然后,这些信息传递给_start函数:

pop %esi //获得argc

move %esp, %ecx //获得argv

当这些信息传递给_start函数以后,_start函数通过将esp的低4位清0(即16字节对齐)来设置我们的主程序的栈的起始地址。

//注:这些代码所在的文件是crtbegin.o, crtend.o, gcrt1.o。


6. 总结

(1)内核加载可执行文件,并建立text/data/bss/stack。此外,内核为参数和环境变量分配页,并将它们压入用户模式栈。

(2)GCC通过crtbegin.o/crtend.o/gcrt1.o来建立程序。另外的默认库默认是动态链接的。可执行文件的开始地址是_start的地址。

(3)控制传递给_start以后,_start从由内核设置的栈中获取参数和环境变量信息,然后调用__libc_start_main。

(4)__libc_start_main初始化必要的数据结构,尤其是C库(比如malloc)和线程环境,然后调用用户的main函数。值得注意的是,__libc_start_main认为main

函数的签名是:

int main(int argc, char ** argv, char ** env)。

(5)main函数的返回值由__libc_start_main接收,并传递给exit。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
YOLO高分设计资源源码,详情请查看资源内容使用说明 YOLO高分设计资源源码,详情请查看资源内容使用说明 YOLO高分设计资源源码,详情请查看资源内容使用说明 YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明YOLO高分设计资源源码,详情请查看资源内容使用说明

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值