Chinaunix首页 | 论坛 | 博客
  • 博客访问: 59272
  • 博文数量: 27
  • 博客积分: 2000
  • 博客等级: 大尉
  • 技术积分: 300
  • 用 户 组: 普通用户
  • 注册时间: 2009-04-24 17:31
文章分类
文章存档

2011年(1)

2010年(8)

2009年(18)

我的朋友

分类: LINUX

2009-06-15 13:52:51


Before main() 分析(修订版)

作者:alert7 alert7@xfocus.org>

主页: http://www.xfocus.org
时间: 2001-9-25


★ 前言

本文分析了从动态链接器开始启动执行 到 main()执行之前的ELF程序加载流程,试图让您更清楚的把握程序的流程的脉络走向。从而更深入的了解ELF。不正确之处,还请斧正。


★ 综述

ELF的可执行文件与共享库在结构上非常类似,它们具有一张程序段表,用来描述这些段如何映射到进程空间。
对于可执行文件来说,段的加载位置是固定的,程序段表中如实反映了段在内存的加载地址.对于共享库来说,段的加载位置是浮动的,位置无关的,程序段表反映的是以0作为基准地址的相对加载地址.尽管共享库的连接是不充分(完整独立)的,但为了便于测试动态链接器,Linux允许应用程序直接加载共享库运行.如果应用程序里包含关于动态链接器的描述段(.interp section),内核首先加载应用程序的段,紧接着加载动态链接器的段,2者都加载进用户空间.然后从内核系统调用返回到用户空间,跳转到动态链接器(/lib/ld-2.2.4.so)的入口(动态连接器里定义的全局符号_start处).如果应用程序里没有包含关于动态链接器的描述段,就直接跳转到应用程序入口(start.s里定义的全局符号_start处)。
上述这部分请参考:《漫谈兼容内核之八 ELF映像的装入(一)》或 linuxforum论坛上opera写的《分析ELF的加载过程》

在内核把控制权交给动态链接器(/lib/ld-2.2.4.so)的入口后,
1. 首先获取应用程序入口地址(通过调用_dl_start函数),然后循环调用每个共享库的初始化函数,接着跳转到应用程序入口_start开始执行。(注:应用程序入口地址并不是想象中的 main的地址)
2. _start例程压入一些参数到堆栈,就直接调用__libc_start_main函数。
3. 在__libc_start_main函数中为动态连接器和应用程序安排destructor,并运行应用程序的初始化函数。然后才把控制权交给应用程序的main()函数。

★ main()之前流程

(1) 动态链接器/lib/ld-2.2.4.so 的入口代码分析
/* Initial entry point code for the dynamic linker.
The C function `_dl_start' is the real entry point;
its return value is the user program's entry point. */

#define RTLD_START asm ("\
.text\n\
.globl _start\n\    /* 这里定义了_start全局变量,后面的start.s代码里也定义了_start,应以start.s里的_start为准 */
.globl _dl_start_user\n\
_start:\n\
    pushl %esp\n\
    call _dl_start\n\    /* 此函数返回时 %eax中存放着user program's entry point */
    popl %ebx\n\         /* %ebx放着是esp的内容 */
_dl_start_user:\n\
    # Save the user entry point address in %edi.
    movl %eax, %edi\n\   /* 把user program's entry point放在%edi */

    # Point %ebx at the GOT. 下面3句指令,令%ebx 指向GOT首地址
    # GOT首地址的计算方法参考《Linux下的动态连接库及其实现机制》(a)装载GOT表首地址
    call 0f\n\        /* f是front的意思,即调用后面的0标号处代码 */
0:  popl %ebx\n\      /* 结果相当于movl %eip, %ebx,将标号0的地址放入%ebx中 */
    addl $_GLOBAL_OFFSET_TABLE_+[.-0b], %ebx\n\      /* b是back的意思 */

    # Store the highest stack address
    movl __libc_stack_end@GOT(%ebx), %eax\n\         /* 把__libc_stack_end符号对应的GOT表项地址赋值给%eax */
    movl %esp, (%eax)\n\        /* 把栈顶%esp放到__libc_stack_end符号对应的GOT表项地址所指内存单元中 */

    # See if we were run as a command with the executable file
    # name as an extra leading argument.
    movl _dl_skip_args@GOT(%ebx), %eax\n\            /* 把_dl_skip_args符号对应的GOT表项地址放到%eax */
    movl (%eax), %eax\n\                             /* 把_dl_skip_args符号对应的GOT表项的内容放到%eax */
    /* readelf -a /lib/ld-2.2.4.so 可查看_dl_start和__libc_stack_end和_dl_skip_args的信息 */

    # Pop the original argument count.
    /* 弹出原始的参数个数agrc到%ecx。这些参数应该是do_execve()的create_elf_tables()填写到堆栈上的。参见:《漫谈兼容内核之八 ELF映像的装入(一)》*/
    popl %ecx\n\

    # Subtract _dl_skip_args from it.
    subl %eax, %ecx\n\      /* 从ecx中减掉_dl_skip_args, _dl_skip_args为需要忽略的参数个数 */

    # Adjust the stack pointer to skip _dl_skip_args words.\n\
    leal (%esp,%eax,4), %esp\n\      /* 根据需要忽略的参数个数调整esp的位置 */

    # Push back the modified argument count.
    pushl %ecx\n\

    # Push the searchlist of the main object as argument in _dl_init_next call below.
    movl _dl_main_searchlist@GOT(%ebx), %eax\n\   /* 把_dl_main_searchlist符号对应的GOT表项地址放到%eax */
    movl (%eax), %esi\n\  /* 把_dl_main_searchlist符号对应的GOT表项的内容放到%esi */
0:  movl %esi,%eax\n\

    # Call _dl_init_next to return the address of an initializer function to run.\n\
    call _dl_init_next@PLT\n\   /* 此函数返回共享库的初始化函数的地址,放在%eax中 */

    # Check for zero return, when out of initializers.\n\
    testl %eax, %eax\n\
    jz 1f\n\

    # Call the shared object initializer function.\n\
    # NOTE: We depend only on the registers (%ebx, %esi and %edi)\n\
    # and the return address pushed by this call;\n\
    # the initializer is called with the stack just\n\
    # as it appears on entry, and it is free to move\n\
    # the stack around, as long as it winds up jumping to\n\
    # the return address on the top of the stack.\n\
    call *%eax\n\             /*调用共享库的初始化函数*/

    # Loop to call _dl_init_next for the next initializer.\n\
    jmp 0b\n\     /* 循环调用 _dl_init_next,为每个共享库进行初始化/

1:  # Clear the startup flag.\n\
    movl _dl_starting_up@GOT(%ebx), %eax\n\
    movl $0, (%eax)\n\

    # Pass our finalizer function to the user in %edx, as per ELF ABI.\n\
    movl _dl_fini@GOT(%ebx), %edx\n\   /* 把_dl_fini符号对应的GOT表项地址放在%edx,后面_start会用到 */

    # Jump to the user's entry point.\n\
    jmp *%edi\n\      /* 最后跳转到user program's entry point 开始执行 */
.previous\n\
");


(2) _start()的代码分析
前面的 user program's entry point 也就是下面的_start例程开始地址,位于sysdeps\i386\start.s中:

/* This is the canonical(规范的) entry point, usually the first thing in the text
segment. The SVR4/i386 ABI (pages 3-31, 3-32) says that when the entry
point runs, most registers' values are unspecified, except for:

%edx  Contains a function pointer to be registered with `atexit'.
This is how the dynamic linker arranges to have DT_FINI
functions called for shared libraries that have been loaded
before this code runs.
进入entry point(即_start)开始执行时,除了%edx %esp以外,其他寄存器都是未定义的:
%edx 保存_dl_fini地址,参见上面的(1)动态链接器的入口代码分析


%esp The stack contains the arguments and environment:

0(%esp)                   argc                  堆栈顶(低地址)
4(%esp)                   argv[0]
8(%esp)                   argv[1]
...
(4*argc)(%esp)            NULL
(4*(argc+1))(%esp)        envp[0]
(4*(argc+2))(%esp)        envp[1]               (高地址)
...
                          NULL
%esp 所指示的堆栈中,包含用户程序参数变量和环境变量。这些参数应该是do_execve()的create_elf_tables()填写的。根据上面的堆栈内容排放顺序,来看看下面的 start.s 的代码

*/

    .text
    .globl
_start
_start:
    /* Clear the frame pointer. The ABI suggests this be done, to mark
    the outermost(最外面的) frame obviously(明确地). */
    xorl %ebp, %ebp

    /* Extract the arguments as encoded on the stack and set up
    the arguments for `main': argc, argv. envp will be determined
    later in __libc_start_main. */
    popl %esi           /* Pop the argument count. 参照上面的堆栈结构:把堆栈顶的argc放入esi中,此时%esp = argv[0] */
    movl %esp, %ecx     /* argv starts just at the current stack top.令%ecx = %esp = argv[0] */

    /* Before pushing the arguments align the stack to a double word
    boundary to avoid penalties from misaligned(不重合的,未对准的) accesses. Thanks
    to Edward Seidl for pointing this out. */
    andl $0xfffffff8, %esp
    pushl %eax              /* Push garbage because we allocate 28 more bytes. */

    /* Provide the highest stack address to the user code (for stacks
    which grow downwards向下).把堆栈顶部地址传递给 用户代码 */
    pushl %esp

    /* Push address of the shared library termination function.
    (%edx 参见前面,保存_dl_fini地址) */
    pushl %edx

    /* Push address of our own entry points to .fini and .init. */
    pushl $_fini
    pushl $_init

    pushl %ecx      /* Push second argument: argv. (见上面:%ecx = argv[0]) */
    pushl %esi      /* Push first argument: argc. (见上面:%esi = argc) */

    pushl $main

    /* Call the user's main function, and exit with its value. But let the libc call main. */
    call __libc_start_main

    hlt /* Crash if somehow `exit' does return. */


(3) __libc_start_main代码分析

__libc_start_main在sysdeps\generic\libc_start.c中。假设定义的是PIC的代码。
 
struct startup_info
{
  void *sda_base;
  int (*main) (int, char **, char **, void *);
  int (*init) (int, char **, char **, void *);
  void (*fini) (void);
};

int
__libc_start_main (int argc, char **argv, char **envp,
           void *auxvec, void (*rtld_fini) (void),
           struct startup_info *stinfo,
           char **stack_on_entry)
{

  /* the PPC SVR4 ABI says that the top thing on the stack will
     be a NULL pointer, so if not we assume that we're being called
     as a statically-linked program by Linux...     */
  if (*stack_on_entry != NULL)
    {
      /* ...in which case, we have argc as the top thing on the
     stack, followed by argv (NULL-terminated), envp (likewise),
     and the auxilary vector.  */
      argc = *(int *) stack_on_entry;
      argv = stack_on_entry + 1;
      envp = argv + argc + 1;
      auxvec = envp;
      while (*(char **) auxvec != NULL)
    ++auxvec;
      ++auxvec;
      rtld_fini = NULL;
    }

  /* Store something that has some relationship to the end of the
     stack, for backtraces.  This variable should be thread-specific.  */
  __libc_stack_end = stack_on_entry + 4;

  /* Set the global _environ variable correctly.  */
  __environ = envp;

  /* Register the destructor of the dynamic linker if there is any.  */
  if (rtld_fini != NULL)
    atexit (rtld_fini);/*替动态连接器安排destructor*/

  /* Call the initializer of the libc.  */

  __libc_init_first (argc, argv, envp); /*一个空函数*/

  /* Register the destructor of the program, if any.  */
  if (stinfo->fini)
    atexit (stinfo->fini); /*安排程序自己的destructor*/

  /* Call the initializer of the program, if any.  */

  /*运行程序的初始化函数*/
  if (stinfo->init)
    stinfo->init (argc, argv, __environ, auxvec);

  /* 运行程序main函数,到此,控制权才交给我们一般所说的程序入口: _main() */
  exit (stinfo->main (argc, argv, __environ, auxvec));

}



void
__libc_init_first (int argc __attribute__ ((unused)), ...)
{
}



int
atexit (void (*func) (void))
{
  struct exit_function *new = __new_exitfn ();

  if (new == NULL)
    return -1;

  new->flavor = ef_at;
  new->func.at = func;
  return 0;
}




/* Run initializers for MAP and its dependencies, in inverse dependency
   order (that is, leaf nodes first). (前面动态链接器代码里用到的_dl_init_next,获取共享库的初始化函数。) */

ElfW(Addr)
internal_function
_dl_init_next (struct r_scope_elem *searchlist)
{
  unsigned int i;

  /* The search list for symbol lookup is a flat list in top-down
     dependency order, so processing that list from back to front gets us
     breadth-first leaf-to-root order.  */

  i = searchlist->r_nlist;
  while (i-- > 0)
    {
      struct link_map *l = searchlist->r_list[i];

      if (l->l_init_called)
    /* This object is all done.  */
    continue;

      if (l->l_init_running)
    {
      /* This object's initializer was just running.
         Now mark it as having run, so this object
         will be skipped in the future.  */
      l->l_init_running = 0;
      l->l_init_called = 1;
      continue;
    }

      if (l->l_info[DT_INIT]
      && (l->l_name[0] != '\0' || l->l_type != lt_executable))
    {
      /* Run this object's initializer.  */
      l->l_init_running = 1;

      /* Print a debug message if wanted.  */
      if (_dl_debug_impcalls)
        _dl_debug_message (1, "\ncalling init: ",
                l->l_name[0] ? l->l_name : _dl_argv[0],
                "\n\n", NULL);

      /*共享库的基地址+init在基地址中的偏移量*/
      return l->l_addr + l->l_info[DT_INIT]->d_un.d_ptr;
      
    }

      /* No initializer for this object.
     Mark it so we will skip it in the future.  */
      l->l_init_called = 1;
    }


  /* Notify the debugger all new objects are now ready to go.  */
  _r_debug.r_state = RT_CONSISTENT;
  _dl_debug_state ();

  return 0;
}


在main()之前的程序流程看似有点简单,但正在运行的时候还是比较复杂的
(自己用GBD跟踪下就知道了),因为一般的程序都需要涉及到PLT,GOT标号的
重定位。弄清楚这个对ELF尤为重要,以后有机会再补上一篇吧。



★ 手动确定程序和动态连接器的入口

[alert7@redhat62 alert7]$ cat helo.c
#include
int main(int argc,char **argv)
{
    printf("hello\n");
    return 0;
}

[alert7@redhat62 alert7]$ gcc -o helo helo.c
[alert7@redhat62 alert7]$ readelf -h helo
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x8048320
Start of program headers: 52 (bytes into file)
Start of section headers: 8848 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 6
Size of section headers: 40 (bytes)
Number of section headers: 29
Section header string table index: 26
在这里我们看到程序的入口为0x8048320,可以看看是否为main函数。

[alert7@redhat62 alert7]$ gdb -q helo
(gdb) disass 0x8048320
Dump of assembler code for function _start:
0x8048320 <_start>: xor %ebp,%ebp
0x8048322 <_start+2>: pop %esi
0x8048323 <_start+3>: mov %esp,%ecx
0x8048325 <_start+5>: and $0xfffffff8,%esp
0x8048328 <_start+8>: push %eax
0x8048329 <_start+9>: push %esp
0x804832a <_start+10>: push %edx
0x804832b <_start+11>: push $0x804841c
0x8048330 <_start+16>: push $0x8048298
0x8048335 <_start+21>: push %ecx
0x8048336 <_start+22>: push %esi
0x8048337 <_start+23>: push $0x80483d0
0x804833c <_start+28>: call 0x80482f8 <__libc_start_main>
0x8048341 <_start+33>: hlt
0x8048342 <_start+34>: nop
End of assembler dump.
呵呵,程序的入口(entry point)是_start例程。不是main。

再来看动态连接器的入口是多少
[alert7@redhat62 alert7]$ ldd helo
libc.so.6 => /lib/libc.so.6 (0x40018000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
动态连接器ld-linux.so.2加载到进程用户地址空间0x40000000。

[alert7@redhat62 alert7]$ readelf -h /lib/ld-linux.so.2
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Intel 80386
Version: 0x1
Entry point address: 0x1990
Start of program headers: 52 (bytes into file)
Start of section headers: 328916 (bytes into file)
Flags: 0x0
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 3
Size of section headers: 40 (bytes)
Number of section headers: 23
Section header string table index: 20
共享object入口地址为0x1990。加上整个ld-linux.so.2被加载到进程地址空间0x40000000。
那么动态连接器的入口地址为0x1990+0x40000000=0x40001990。

内核加载应用程序段和动态连接器段到用户空间,然后从内核态返回用户空间,执行的第一条指令地址就是动态连接器首地址:0x40001990,既上面开始处的 #define RTLD_START

 
 
阅读(1398) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~