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;
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)
所有的动态链接的数据和函数都有动态重定位的入口,可以用以下命令查看
~#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
那么,__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));
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:环境变量数组指针
当_start标号所在的汇编指令开始执行时,函数的栈帧如下所示:
- Stack Top ————-
- argc
- ————-
- argv pointer
- ————-
- env pointer
- ————-
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。