Linux如何执行main()函数

来源:互联网 发布:数据科学考研科目 编辑:程序博客网 时间:2024/06/15 23:10
1. 开始
问题很简单:Linux如何执行main()函数呢
通过这个文档,将通过这个简单的C程序来介绍它怎么工作的,文件名为“simple.c”
main(){     return(0);}
2. 构建
gcc -o simple simple.c
3. 可执行程序里有什么
为了可以知道可执行程序包含什么,使用“objdump”工具
objdump -f simplesimple:     file format elf32-i386architecture: i386, flags 0x00000112:EXEC_P, HAS_SYMS, D_PAGEDstart address 0x080482d0
输出的信息显示了可执行程序一些关键信息。
首先,这文件是ELF32格式。其次,起始地址是“0x080482d0”
4. ELF是什么
ELF是Executable 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”域,是可执行文件的起始地址。

5. 在地址“0x080482d0”是什么,那是起始地址?
对于这个问题,我们反汇编“simple”,有一些工具是来反汇编可执行程序的。这里使用objdump。

objdump --disassemble simple

输出文件较长,只显示了部分输出,只要目的是看看在地址“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
                          -------------------

现在,正如你想知道的,有一些问题关于这个堆栈帧。
Q1. 这些十六进制数关于什么的
Q2. 在地址“80482bc”是什么,代码中被_start调用。
Q3. 似乎汇编代码没有用有效的值来初始化任何寄存器,后面谁来初始化它们呢

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”中的地址
6. 更多关于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的库。也可以知道所有动态链接的数据和函数有“动态重定位的入口”
这些概念可以粗略地如下描述:
a. 在链接的时候,我们不知道动态符号的实际地址。只有在运行时候,我们可以知道符号的实际地址。
b. 所以对于动态符号,为实际地址预留了内存位置。在运行时加载器将会用符号的实际地址填充预留位置的内存。
c. 我们的程序使用指针操作之类的方法通过预留位置的内存间接得到动态符号。在我们的例子中,在地址“80482bc”,存在简单的跳转指令。
   跳转到的位置,在运行时被加载器存储在了地址“0x8049548”的内存中。
使用objdump命令可以看到所有动态链接入口:
objdump -R simplesimple:     file format elf32-i386DYNAMIC RELOCATION RECORDSOFFSET   TYPE              VALUE 0804954c R_386_GLOB_DAT    __gmon_start__08049540 R_386_JUMP_SLOT   __register_frame_info08049544 R_386_JUMP_SLOT   __deregister_frame_info08049548 R_386_JUMP_SLOT   __libc_start_main
这里地址“0x8049548”称为“jump slot”,起着十分重要的作用。根据表格,实际上我们想调用__libc_start_main。
7. __libc_start_main是什么
现在轮到libc了,__libc_start_main是libc.so.6中的函数。如果查找下__libc_start_main在glibc中的源码,它的原型如下:
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

                       ------------------- 

根据堆栈帧,在__libc_start_main()执行之前,esi,ecx,edx,esp和eax寄存器应该被填入相应值。很明显这些寄存器不是被最开始示例汇编代码设置的。谁会设置这些寄存器呢,剩下就只能猜测是应该是,内核。
Q3>内核做了什么
当我们在shell里输入程序名字执行程序的时候,此时Linux发生了什么。
a. shell使用argc/argv调用了系统调用“execve”.
b. 内核系统调用处理程序得到控制权,开始处理系统调用。在内核态,处理程序是“sys_execve”。在x86,用户态应用程序传递所有需要的参数到内核,使用的是以下寄存器。
    => ebx: 程序名称字符串指针
    => ecx: argv指针数组
    => edx: 环境变量指针数组
c. 通常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()开始进程执行运作。
   
当_start汇编指令得到执行权限的时候,堆栈帧如下图:
Stack Top      -------------
                          argc
                        -------------
                          argv pointer
                        -------------
                          env pointer
                        -------------
汇编指令从栈得到所有信息如下:
pop %esi <--- get argc
move %esp, %ecx <--- get argv
        实际上argv地址与当前的栈指针相同.
现在所有的都设置了,可以开始执行了。

8. 其他寄存器呢

对于esp,在应用程序中 用于堆栈结束。当弹出所有必要信息,_start方法调整堆栈指针(esp),使esp寄存器最低四位关闭。这十分重要,因为实际上
对于我们的主程序,那是堆栈的结束。对于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)
在x86 Linux,0意味着不要使用那样的功能。
9. 关于汇编指令  
这些所有的代码来自哪里?它是GCC代码的一部分。通常可以在以下所示,找到所有的目标文件:
/usr/lib/gcc-lib/i386-redhat-linux/XXX 和 /usr/lib ;其中XXX是gcc版本.文件名是
crtbegin.o,crtend.o,gcrt1.o。
10. 总结
这里发生了什么.
a. gcc使用crtbegin.o/crtend.o/gcrt1.o编译构建了你的程序,当然其他默认库被默认动态链接。可执行程序的起始地址被设置为_start
b. Kernel加载可执行程序和设置text/data/bss/stack,特别是,kernel为参数和环境变量分配了页,把所有必要信息压栈。
c. 控制权限交给_start,_start通过kernel设置的栈获得所有信息,并为__libc_start_main设置参数堆栈,并且调用它。
d. __libc_start_main初始化必要内容,尤其是C库(比如malloc)和线程环境和调用main()。

e. 通常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);}
11. 结论

在Linux上,我们的C main函数执行是 GCC, libc和Linux's binary loader协同工作实现。
12.参考文献
=> 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

原文:http://www.tldp.org/LDP/LGNET/84/hawk.html

0 0
原创粉丝点击