设为首页 收藏本站
查看: 779|回复: 0

[经验分享] Linux中main是如何执行的

[复制链接]

尚未签到

发表于 2017-11-17 17:44:31 | 显示全部楼层 |阅读模式
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>
  Elf32_Half  e_phentsize;        /* Program header table entry>  Elf32_Half  e_phnum;        /* Program header table entry count */

  Elf32_Half  e_shentsize;        /* Section header table entry>  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
  

  

  看上去开始地址上存放的是叫做&quot;_start&quot;的启动例程。它所做的是清空寄存器,向栈中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上。这里的&quot;动态链接&quot;意味着实际的链接过程发生在运行时。否则我们就得编译出一个巨大的可执行文件,这个文件包含了它所调用的所有libraries(&quot;一个『静态链接的可执行文件』&quot;)。如果你执行下面的命令:
  

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>  这个概念粗略的讲述如下:


  • 在链接时我们不会得知一个动态符号的实际地址。只有在运行时我们才能知道这个实际地址。
  • 所以对于动态符号,我们为其实际地址预留出了存储单元。加载器会在运行时用动态符号的实际地址填充存储单元。
  • 我们的应用通过使用一种指针操作来间接得知动态符号的存储单元。在我们的例子中,在地址80482bc上,有一个简单的jump指令。jump到的单元由加载器在运行时存储到地址0x8049548上。
  我们通过使用objdump命令可以看到所有的动态链接入口:
  

objdump -R simple  

  simple:     file format elf32-i386
  


  DYNAMIC>  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被叫做&quot;JUMP SLOT&quot;,非常贴切。根据这个表,实际上我们想调用的是 __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参数的系统调用&quot;execve&quot;。
  • 内核的系统调用句柄开始处理这个系统调用。在内核代码中,这个句柄为&quot;sys_execve&quot;.在x86机器上,用户模式的应用会通过以下寄存器将所有需要的参数传递到内核中。


  • ebx:执行程序名字的字符串
  • ecx:argv数组指针
  • edx:环境变量数组指针

  • 通用的execve内核系统调用句柄——也就是do_execve——被调用。它所做的是建立一个数据结构,将所有用户空间数据拷贝到内核空间,最后调用search_binary_handler()。Linux能够同时支持多种可执行文件格式,例如a.out和ELF。对于这个功能,存在一个数据结构&quot;struct linux_binfmt&quot;,对于每个二进制格式的加载器在这个数据结构都会有一个函数指针。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 != 0)
  {
  printf(&quot;%s\n&quot;, env[i++]);
  }
  return(0);
  
}
  

  

结论
  在Linux中,我们的C main()函数由GCC,libc和Linux二进制加载器的共同协作来执行。

参考
  

objdump                         &quot;man objdump&quot;  

  
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
  

  

  

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-407987-1-1.html 上篇帖子: linux console 显示颜色【转】 下篇帖子: Linux下的编译器(转)
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表