Linux系统中main()函数怎么执行的

  • Post author:
  • Post category:linux




Starting

问题很简单:Linux如何执行main()?

在本文档中,我将使用以下简单的C程序来说明其工作方式。它叫做“ simple.c”

main()

{


return(0);

}



Build

gcc -o simple simple.c

可执行文件中有什么?

要查看可执行文件中的内容,我们使用工具“ objdump”

objdump -f simple

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

输出为我们提供了有关可执行文件的一些关键信息。

首先,文件为“ ELF32”格式。第二,起始地址为“ 0x080482d0”

什么是ELF?

ELF是“可执行文件和链接格式”的缩写。它是Unix系统上使用的几种对象和可执行文件格式之一。对于我们的问题,关于ELF的有趣之处在于其标头格式。每个ELF可执行文件都有ELF标头,如下所示。

typedef struct 
{ 
	unsigned char e_ident [EI_NIDENT]; / *幻数和其他信息* / 
	Elf32_Half e_type; / *目标文件类型* / 
	Elf32_Half e_machine; / *体系结构* / 
	Elf32_Word e_version; / *目标文件版本* / 
	Elf32_Addr e_entry; / *入口点虚拟地址* / 
	Elf32_Off e_phoff; / *程序头表文件偏移* / 
	Elf32_Off e_shoff; / *节头表文件偏移量* / 
	Elf32_Word e_flags; / *特定于处理器的标志* / 
	Elf32_Half e_ehsize; / * ELF标头大小(以字节为单位)* / 
	Elf32_Half e_phentsize; / *程序标头表条目大小* / 
	Elf32_Half e_phnum; / *程序标头表条目计数* /
	Elf32_Half e_shentsize; / *节头表条目大小* / 
	Elf32_Half e_shnum; / *节头表条目计数* / 
	Elf32_Half e_shstrndx; / *节头字符串表索引* / 
} 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”的启动例程位于起始地址。它的作用是清除寄存器,将某些值压入堆栈并调用函数。根据此指令,堆栈帧应如下所示。

Stack Top	-------------------
		0x80483d
		-------------------
		esi
		-------------------
		ecx
		-------------------
		0x8048274
		-------------------
		0x8048420
		-------------------
		edx
		-------------------
		esp
		-------------------
		eax
		-------------------

现在,也许您会此堆栈框架有一些疑问。

1.这些十六进制值是关于什么的? 
2._start调用的地址80482bc是什么? 
3.汇编指令好像并未使用可能有意义的值初始化任何寄存器。那谁初始化寄存器呢?

让我们一个接一个地回答这些问题。



Q1>十六进制值。

如果您仔细查看objdump的反汇编输出,则可以轻松回答此问题。

这是答案。

0x80483d0:这是我们main()函数的地址。

0x8048274:_init函数

0x8048420:_fini函数_init和_fini是GCC提供的初始化/完成函数。

现在,我们无需关注这些东西。事实上,所有这些十六进制值都是函数指针。



Q2>地址80482bc是什么?

同样,让我们​​从反汇编输出中查找地址80482bc。

如果您寻找它,程序集是

80482bc:ff 25 48 95 04 08 jmp * 0x8049548
  • 在这里的0x8049548是指针操作。它只是跳转到存储在地址0x8049548的地址。



有关ELF和动态链接的更多信息

使用ELF,我们可以构建与库动态链接的可执行文件。

这里的“动态链接”是指实际的链接过程在运行时发生。否则,我们将必须构建一个巨大的可执行文件,其中包含它调用的所有库(“静态链接的可执行文件”)。如果发出命令

"ldd simple"

	  libc.so.6 => /lib/i686/libc.so.6 (0x42000000)
	  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

您可以看到所有与”simple”动态链接的库。并且所有动态链接的数据和功能都具有“动态重定位条目”。

这个概念大致是这样的。

  • 我们不知道链接时动态符号的实际地址。我们只能在运行时知道符号的实际地址。
  • 因此对于动态符号,我们为实际地址保留一个存储位置。
  • 加载程序在运行时将用符号的实际地址填充存储位置。
  • 我们的应用程序通过使用一种指针操作来间接地看到动态符号和内存位置。在我们的例子中,在地址80482bc处只有一条简单的跳转指令。

    加载程序在运行期间将跳转位置存储在地址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称为“跳转槽”,这很有意义。根据表,实际上我们要调用__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)(intchar **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
                     -------------------

根据此堆栈框架,在执行__libc_start_main()之前,应使用适当的值填充esi,ecx,edx,esp,eax寄存器。显然,该寄存器未由前面显示的启动汇编指令设置。那么,谁来设置这些寄存器?

现在我们就想到了—–内核。

现在让我们回到第三个问题。



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和环境变量复制到那些分配的页面地址。最后,由create_elf_tables()将argc,argv指针和环境变量数组指针推送到用户模式堆栈,然后start_thread()开始滚动执行流程。并且它为此过程分配用户模式页面,并将argv和环境变量复制到那些分配的页面地址。最后,由create_elf_tables()将argc,argv指针和环境变量数组指针推送到用户模式堆栈,然后start_thread()开始滚动执行流程。并且它为此过程分配用户模式页面,并将argv和环境变量复制到那些分配的页面地址。最后,由create_elf_tables()将argc,argv指针和环境变量数组指针推送到用户模式堆栈,然后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 rountine只需通过关闭esp寄存器的低4位来调整堆栈指针(esp)。这完全是有道理的,因为实际上对我们的主程序而言,这就是堆栈的结尾。对于用于rtld_fini(一种应用程序析构函数)的edx,内核仅使用以下宏将其设置为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上使用该功能。



关于组装说明

这些代码来自哪里?它是GCC代码的一部分。通常,您可以在

/ usr / lib / gcc-lib / i386-redhat-linux / XXX和

/ usr / lib中找到代码的所有目标文件,其中XXX是gcc版本。

文件名是crtbegin.o,crtend.o,gcrt1.o。



总结一下

这是发生了什么。

1.GCC使用crtbegin.o / crtend.o / gcrt1.o构建程序。默认情况下,其他默认库是动态链接的。可执行文件的起始地址设置为_start

2.内核加载可执行文件和设置文本/数据/ bss /堆栈,尤其是内核为参数和环境变量分配页面,并将所有必要的信息压入堆栈

3.控件已粘贴到_start。_start从内核的堆栈设置中获取所有信息,为__libc_start_main设置参数堆栈,然后调用它

4.__libc_start_main初始化必要的东西,尤其是C库(例如malloc)和线程环境,并调用我们的main

5.我们的main其实是被main(argv,argv)调用,这里有趣的一点是main。__libc_start_main认为main的应该是main(int,char **,char **)如果您很好奇,请尝试以下prgram

main(int argc,char ** argv,char ** env
{ 
    int i = 0; 
    while(env [i]= 0{ 
       printf(“%s \ n”,env [i ++]; 
    } 
    return0; 
}

总的来说,在Linux上,我们的C main()函数由GCC,libc和Linux的二进制加载器共同完成



版权声明:本文为errly_liu2828原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。