_dl_start源码分析

时间:2022-12-10 15:30:34

_dl_start源码分析

上一章分析了linux系统调用sys_execv的源码,最终linux会将控制权交给解释器的_start函数,该函数紧接着调用_dl_start函数,本章就分析该函数。因为glibc内部的宏定义太多,不适合代码的阅读,因此本章以及后面的章节删除了一部分定义和代码分支。

elf/rtld.c
_dl_start第一部分

static ElfW(Addr) __attribute_used__ internal_function
_dl_start (void *arg)
{
# define bootstrap_map GL(dl_rtld_map)

#define RTLD_BOOTSTRAP
#define RESOLVE_MAP(sym, version, flags) (&bootstrap_map)
#include "dynamic-link.h"

  if (HP_TIMING_INLINE && HP_TIMING_AVAIL)
    HP_TIMING_NOW (start_time);

GL的宏定义就是在变量名前面加上下划线,例如GL(dl_rtld_map)也即_dl_rtld_map,其为link_map结构。
HP_TIMING_NOW内部使用rdtsc指令获得cpu的计数器值,计数器一共64位,高位在rdx寄存器中,低位在rax寄存器中。

elf/rtld.c
_dl_start第二部分

  bootstrap_map.l_addr = elf_machine_load_address ();
  bootstrap_map.l_ld = (void *) bootstrap_map.l_addr + elf_machine_dynamic ();
  elf_get_dynamic_info (&bootstrap_map, NULL);

#if NO_TLS_OFFSET != 0
  bootstrap_map.l_tls_offset = NO_TLS_OFFSET;
#endif

elf_machine_load_address获得解释器的装载地址,elf_machine_dynamic获得.dynamic节的静态地址,加上装载地址即是.dynamic节的实际地址。
elf_get_dynamic_info设置info,后面的链接过程要大量用到这个数组的信息。

sysdeps/x86_64/dl-machine.h
_dl_start->elf_machine_load_address

static inline Elf64_Addr __attribute__ ((unused))
elf_machine_load_address (void)
{
  Elf64_Addr addr;
  asm ("leaq _dl_start(%%rip), %0\n\t"
       "subq 1f(%%rip), %0\n\t"
       ".section\t.data.rel.ro\n"
       "1:\t.quad _dl_start\n\t"
       ".previous\n\t"
       : "=r" (addr) : : "cc");

  return addr;
}

data段中存储了_dl_start静态连接后的地址,汇编的第一句获取了_dl_start当前运行时的实际地址,两者的差即是解释器的装载地址addr。

sysdeps/x86_64/dl-machine.h
_dl_start->elf_machine_dynamic

static inline Elf64_Addr __attribute__ ((unused))
elf_machine_dynamic (void)
{
  Elf64_Addr addr;
  addr = (Elf64_Addr) &_DYNAMIC;
  return addr;
}

elf_machine_dynamic返回.dynamic节的地址,因为.dynamic节的地址是GOT表中的第一个项,所以addr也即GOT表的地址。

sysdeps/x86_64/dl-machine.h
_dl_start->elf_machine_dynamic

inline void __attribute__ ((unused, always_inline))
elf_get_dynamic_info (struct link_map *l, ElfW(Dyn) *temp)
{
  ElfW(Dyn) *dyn = l->l_ld;
  ElfW(Dyn) **info;
  typedef Elf64_Xword d_tag_utype;
  info = l->l_info;

  while (dyn->d_tag != DT_NULL)
    {
      if ((d_tag_utype) dyn->d_tag < DT_NUM)
    info[dyn->d_tag] = dyn;
      else if (dyn->d_tag >= DT_LOPROC &&
           dyn->d_tag < DT_LOPROC + DT_THISPROCNUM)
    info[dyn->d_tag - DT_LOPROC + DT_NUM] = dyn;
      else if ((d_tag_utype) DT_VERSIONTAGIDX (dyn->d_tag) < DT_VERSIONTAGNUM)
    info[VERSYMIDX (dyn->d_tag)] = dyn;
      else if ((d_tag_utype) DT_EXTRATAGIDX (dyn->d_tag) < DT_EXTRANUM)
    info[DT_EXTRATAGIDX (dyn->d_tag) + DT_NUM + DT_THISPROCNUM
         + DT_VERSIONTAGNUM] = dyn;
      else if ((d_tag_utype) DT_VALTAGIDX (dyn->d_tag) < DT_VALNUM)
    info[DT_VALTAGIDX (dyn->d_tag) + DT_NUM + DT_THISPROCNUM
         + DT_VERSIONTAGNUM + DT_EXTRANUM] = dyn;
      else if ((d_tag_utype) DT_ADDRTAGIDX (dyn->d_tag) < DT_ADDRNUM)
    info[DT_ADDRTAGIDX (dyn->d_tag) + DT_NUM + DT_THISPROCNUM
         + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM] = dyn;
      ++dyn;
    }

  ADJUST_DYN_INFO (DT_HASH);
  ADJUST_DYN_INFO (DT_PLTGOT);
  ADJUST_DYN_INFO (DT_STRTAB);
  ADJUST_DYN_INFO (DT_SYMTAB);
  ADJUST_DYN_INFO (DT_RELA);
  ADJUST_DYN_INFO (DT_JMPREL);
  ADJUST_DYN_INFO (VERSYMIDX (DT_VERSYM));
  ADJUST_DYN_INFO (DT_ADDRTAGIDX (DT_GNU_HASH) + DT_NUM + DT_THISPROCNUM
               + DT_VERSIONTAGNUM + DT_EXTRANUM + DT_VALNUM);
}

ElfW的宏定义为

#define ElfW(type) _ElfW (Elf, __ELF_NATIVE_CLASS, type)
#define _ElfW(e,w,t) _ElfW_1 (e, w, _##t)
#define _ElfW_1(e,w,t) e##w##t

例如上面代码中的ElfW(Addr)其实就是Elf64_Addr(如果是32位机则是Elf32_Addr)。
因此ElfW(Dyn)拓展开来就是Elf64_Dyn,其定义如下,

typedef struct
{
  Elf64_Sxword  d_tag;
  union
    {
      Elf64_Xword d_val;
      Elf64_Addr d_ptr;
    } d_un;
} Elf64_Dyn;

d_tag内存储的是.dynamic节中每个值的类型,d_val和d_ptr则是其值或地址。
elf_get_dynamic_info函数首先遍历.dynamic节中所有的值,根据d_tag类型将不同的值存入link_map的info数组中。
接下来通过ADJUST_DYN_INFO宏调整.dynamic节中对应的地址,例如.hash、.plt.got、.symtab对应的section的地址。
ADJUST_DYN_INFO宏定义定义在函数内部,

# define ADJUST_DYN_INFO(tag) 
      do                                      
    if (info[tag] != NULL)                            
      {                                                           
          info[tag]->d_un.d_ptr += l_addr;                    
      }                                   
      while (0)

这里就是简单加上解释器的实际装载地址。

elf/rtld.c
_dl_start第三部分

  if (bootstrap_map.l_addr || ! bootstrap_map.l_info[VALIDX(DT_GNU_PRELINKED)])
    {
      ELF_DYNAMIC_RELOCATE (&bootstrap_map, 0, 0, 0);
    }
  bootstrap_map.l_relocated = 1;

  ElfW(Addr) entry = _dl_start_final (arg);

# define ELF_MACHINE_START_ADDRESS(map, start) (start)
  return ELF_MACHINE_START_ADDRESS (GL(dl_ns)[LM_ID_BASE]._ns_loaded, entry);

}

ELF_DYNAMIC_RELOCATE宏定义用来对ld.so自身进行重定位,主要是修改.rela.dyn和.rela.plt中的各个地址值。这就是鸡生蛋,蛋生鸡的问题,ld.so需要自己给自己重定位。然后将l_relocated设置为1表示重定位完成。后面就可以访问全局变量和函数了。

elf/dynamic-link.h
_dl_start->ELF_DYNAMIC_RELOCATE

# define ELF_DYNAMIC_RELOCATE(map, lazy, consider_profile, skip_ifunc) \
  do {                                        \
    int edr_lazy = elf_machine_runtime_setup ((map), (lazy),              \
                          (consider_profile));        \
    ELF_DYNAMIC_DO_REL ((map), edr_lazy, skip_ifunc);                 \
    ELF_DYNAMIC_DO_RELA ((map), edr_lazy, skip_ifunc);                \
  } while (0)

elf_machine_runtime_setup什么也不做,假设ELF_DYNAMIC_DO_REL为空定义,因此下面只看ELF_DYNAMIC_DO_RELA。

elf/dynamic-link.h
_dl_start->ELF_DYNAMIC_RELOCATE->_ELF_DYNAMIC_DO_RELOC

# define _ELF_DYNAMIC_DO_RELOC(RELOC, reloc, map, do_lazy, skip_ifunc, test_rel) 
  do {                                        
    struct { ElfW(Addr) start, size;                          
         __typeof (((ElfW(Dyn) *) 0)->d_un.d_val) nrelative; int lazy; } ranges[2] = { { 0, 0, 0, 0 }, { 0, 0, 0, 0 } }; if ((map)->l_info[DT_##RELOC]) { ranges[0].start = D_PTR ((map), l_info[DT_##RELOC]); ranges[0].size = (map)->l_info[DT_##RELOC##SZ]->d_un.d_val; 
    if (map->l_info[VERSYMIDX (DT_##RELOC##COUNT)] != NULL) 
      ranges[0].nrelative                             
        = MIN (map->l_info[VERSYMIDX (DT_##RELOC##COUNT)]->d_un.d_val, 
           ranges[0].size / sizeof (ElfW(reloc)));            
      }                                       
    if ((map)->l_info[DT_PLTREL] && (!test_rel || (map)->l_info[DT_PLTREL]->d_un.d_val == DT_##RELOC)) { ElfW(Addr) start = D_PTR ((map), l_info[DT_JMPREL]); ranges[0].size += (map)->l_info[DT_PLTRELSZ]->d_un.d_val;                             
      }                                       
      elf_dynamic_do_##reloc ((map), ranges[0].start, ranges[0].size, 
                  ranges[0].nrelative, 0, skip_ifunc);                                   
  } while (0)

_ELF_DYNAMIC_DO_RELOC宏定义首先根据.dynamic节中的信息,获取.rel.dyn节的起始地址存放在start中,再获取.rel.dyn的大小存放在size中,然后获取.rel.dyn中类型为R_X86_64_RELATIVE的大小存放在nrelative中,因为.rel.dyn和.rel.plt节是紧挨着的,再往下获取.rel.plt节的大小,累加在size中,根据这些信息,最后调用elf_dynamic_do_Rel函数执行重定位。

elf/do-rel.h
_dl_start->ELF_DYNAMIC_RELOCATE->_ELF_DYNAMIC_DO_RELOC->elf_dynamic_do_Rel

auto inline void __attribute__ ((always_inline))
elf_dynamic_do_Rel (struct link_map *map,
            ElfW(Addr) reladdr, ElfW(Addr) relsize,
            __typeof (((ElfW(Dyn) *) 0)->d_un.d_val) nrelative, int lazy, int skip_ifunc) { const ElfW(Rel) *r = (const void *) reladdr; const ElfW(Rel) *end = (const void *) (reladdr + relsize); ElfW(Addr) l_addr = map->l_addr;

  const ElfW(Sym) *const symtab = (const void *) D_PTR (map, l_info[DT_SYMTAB]);
  const ElfW(Rel) *relative = r;
  r += nrelative;

  for (; relative < r; ++relative)
    DO_ELF_MACHINE_REL_RELATIVE (map, l_addr, relative);

  const ElfW(Half) *const version = (const void *) D_PTR (map, l_info[VERSYMIDX (DT_VERSYM)]);

  for (; r < end; ++r)
  {
    ElfW(Half) ndx = version[ELFW(R_SYM) (r->r_info)] & 0x7fff;
    elf_machine_rel (map, r, &symtab[ELFW(R_SYM) (r->r_info)], &map->l_versions[ndx],
                   (void *) (l_addr + r->r_offset), skip_ifunc);
  }
}

传入的参数reladdr为.rel.dyn节的起始地址,relsize为.rel.dyn和.rel.plt节的总大小,因此end为.rel.plt的结束地址。参数nrelative为.rel.dyn中类型为R_X86_64_RELATIVE的项的个数。
首先从ld.so对应的link_map结构中获取符号表,relative为.rel.dyn的起始地址,r累加后变为.rel.dyn中最后一个类型为R_X86_64_RELATIVE的项的结束地址,也即第一个类型为R_X86_64_GLOB_DAT的起始地址。
首先通过DO_ELF_MACHINE_REL_RELATIVE宏对.rel.dyn节中类型为R_X86_64_RELATIVE的项进行重定位。然后通过elf_machine_rel函数对.rel.dyn中剩余的项进行重定位。其中重定位项中的r_info成员变量存储了该重定位项在符号表中的索引信息。

elf/do-rel.h
_dl_start->ELF_DYNAMIC_RELOCATE->_ELF_DYNAMIC_DO_RELOC->elf_dynamic_do_Rel->DO_ELF_MACHINE_REL_RELATIVE

# define DO_ELF_MACHINE_REL_RELATIVE(map, l_addr, relative) 
  elf_machine_rel_relative (l_addr, relative,                     
                (void *) (l_addr + relative->r_offset))

auto inline void
__attribute ((always_inline))
elf_machine_rela_relative (Elf64_Addr l_addr, const Elf64_Rela *reloc,
               void *const reloc_addr_arg)
{
  Elf64_Addr *const reloc_addr = reloc_addr_arg;
  *reloc_addr = l_addr + reloc->r_addend;
}

DO_ELF_MACHINE_REL_RELATIVE宏定义继而调用elf_machine_rel_relative函数,elf_machine_rel_relative的宏定义为elf_machine_rela_relative,传入的参数l_addr为ld.so的实际装载地址,reloc为.rel.dyn中类型为R_X86_64_RELATIVE的项的地址,reloc_addr_arg为需要更改值的最终地址(例如在GOT表中),将.rel.dyn中的起始数r_addend加上装载地址ld.so写入最终地址reloc_addr_arg中。

sysdeps/x86_64/dl-machine.h
_dl_start->ELF_DYNAMIC_RELOCATE->_ELF_DYNAMIC_DO_RELOC->elf_dynamic_do_Rel->elf_machine_rela

auto inline void
__attribute__ ((always_inline))
elf_machine_rela (struct link_map *map, const Elf64_Rela *reloc,
          const Elf64_Sym *sym, const struct r_found_version *version,
          void *const reloc_addr_arg, int skip_ifunc)
{
  Elf64_Addr *const reloc_addr = reloc_addr_arg;
  const unsigned long int r_type = ELF64_R_TYPE (reloc->r_info);

  if (__builtin_expect (r_type == R_X86_64_NONE, 0))
    return;
  else
    {
      struct link_map *sym_map = RESOLVE_MAP (&sym, version, r_type);
      Elf64_Addr value = (sym == NULL ? 0
              : (Elf64_Addr) sym_map->l_addr + sym->st_value);

      if (sym != NULL
      && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC,0)
      && __builtin_expect (sym->st_shndx != SHN_UNDEF, 1)
      && __builtin_expect (!skip_ifunc, 1))
    value = ((Elf64_Addr (*) (void)) value) ();

      switch (r_type)
    {
    case R_X86_64_GLOB_DAT:
    case R_X86_64_JUMP_SLOT:
      *reloc_addr = value + reloc->r_addend;
      break;

    ...

    }
    }
}

首先检查.rel.dyn和.rel.plt中项的类型是否为R_X86_64_NONE,如果是则直接返回。
接下来获取符号表中对应符号的地址st_value,加上装载地址l_addr。
ld.so中.rel.dyn节中的项类型有R_X86_64_RELATIVE和R_X86_64_GLOB_DAT两种,.rel.plt中的项类型只有R_X86_64_JUMP_SLOT一种,因此无论哪种类型,最终都加上项中的初始值r_addend填入最终地址reloc_addr中。

elf/rtld.c
_dl_start->_dl_start_final

static inline ElfW(Addr) __attribute__ ((always_inline))
_dl_start_final (void *arg)
{
  ElfW(Addr) start_addr;

  _dl_setup_hash (&GL(dl_rtld_map));
  GL(dl_rtld_map).l_real = &GL(dl_rtld_map);
  GL(dl_rtld_map).l_map_start = (ElfW(Addr)) _begin;
  GL(dl_rtld_map).l_map_end = (ElfW(Addr)) _end;
  GL(dl_rtld_map).l_text_end = (ElfW(Addr)) _etext;
  __libc_stack_end = __builtin_frame_address (0);

  start_addr = _dl_sysdep_start (arg, &dl_main);
  return start_addr;
}

首先通过_dl_setup_hash函数获取ld.so中的hash表并将其设置到全局link_map中(_dl_rtld_map)。
接下来设置l_map_start设置装载的起始地址,即0,l_map_end指向.bss节的结束地址,_etext为.text的结束地址,其中_begin在elf文件夹下的makefile中指定,其余位置在链接脚本中确定。
__builtin_frame_address是gcc提供的用于获得调用栈的地址。
最终调用_dl_sysdep_start函数继续执行。

elf/rtld.c
_dl_start->_dl_start_final->_dl_setup_hash

void
internal_function
_dl_setup_hash (struct link_map *map)
{
  Elf_Symndx *hash;

  if (!map->l_info[DT_HASH])
    return;
  hash = (void *) D_PTR (map, l_info[DT_HASH]);

  map->l_nbuckets = *hash++;
  hash++;
  map->l_buckets = hash;
  hash += map->l_nbuckets;
  map->l_chain = hash;
}

这里通过前面DYNAMIC节的信息获取hash表的地址hash,然后访问该地址,获取桶的数量存储在l_nbuckets中,然后跳过nchain即链表的数量,再访问桶的起始地址存储在l_buckets中,最后将链表地址存储在l_chain中。

elf/dl-sysdep.c
_dl_start->_dl_start_final->_dl_sysdep_start

ElfW(Addr)
_dl_sysdep_start (void **start_argptr,
          void (*dl_main) (const ElfW(Phdr) *phdr, ElfW(Word) phnum,
                   ElfW(Addr) *user_entry, ElfW(auxv_t) *auxv))
{
  const ElfW(Phdr) *phdr = NULL;
  ElfW(Word) phnum = 0;
  ElfW(Addr) user_entry;
  ElfW(auxv_t) *av;

  uid_t uid = 0;
  gid_t gid = 0;
  unsigned int seen = 0;

# define set_seen_secure() (seen = -1)
# define M(type) (1 << (type))
# define set_seen(tag) seen |= M ((tag)->a_type)

  __libc_stack_end = DL_STACK_END (start_argptr);
  DL_FIND_ARG_COMPONENTS (start_argptr, _dl_argc, INTUSE(_dl_argv), _environ,
              GLRO(dl_auxv));

  user_entry = (ElfW(Addr)) ENTRY_POINT;
  GLRO(dl_platform) = NULL; 

  for (av = GLRO(dl_auxv); av->a_type != AT_NULL; set_seen (av++))
    switch (av->a_type)
      {
      case AT_PHDR:
    phdr = (void *) av->a_un.a_val;
    break;
      case AT_PHNUM:
    phnum = av->a_un.a_val;
    break;
      case AT_PAGESZ:
    GLRO(dl_pagesize) = av->a_un.a_val;
    break;
      case AT_ENTRY:
    user_entry = av->a_un.a_val;
    break;
      case AT_SECURE:
    seen = -1;
    INTUSE(__libc_enable_secure) = av->a_un.a_val;
    break;
      case AT_PLATFORM:
    GLRO(dl_platform) = (void *) av->a_un.a_val;
    break;
      case AT_HWCAP:
    GLRO(dl_hwcap) = (unsigned long int) av->a_un.a_val;
    break;
      case AT_HWCAP2:
    GLRO(dl_hwcap2) = (unsigned long int) av->a_un.a_val;
    break;
      case AT_CLKTCK:
    GLRO(dl_clktck) = av->a_un.a_val;
    break;
      case AT_FPUCW:
    GLRO(dl_fpu_control) = av->a_un.a_val;
    break;
      case AT_RANDOM:
    _dl_random = (void *) av->a_un.a_val;
    break;
      }

  if (GLRO(dl_pagesize) == 0)
    GLRO(dl_pagesize) = __getpagesize ();

  DL_SYSDEP_INIT;
  DL_PLATFORM_INIT;

  if (GLRO(dl_platform) != NULL)
    GLRO(dl_platformlen) = strlen (GLRO(dl_platform));

  if (__sbrk (0) == _end)
    __sbrk (GLRO(dl_pagesize)
        - ((_end - (char *) 0) & (GLRO(dl_pagesize) - 1)));

  (*dl_main) (phdr, phnum, &user_entry, GLRO(dl_auxv));
  return user_entry;
}

首先通过DL_FIND_ARG_COMPONENTS宏获取栈中的数据,根据sys_execve的源码可知这里依次保存了参数数量、用户变量、环境变量和其他变量(该变量在linux源码的create_elf_table函数中设置),DL_FIND_ARG_COMPONENTS宏将这些变量分别存储在用户空间的_dl_argc、_dl_argv、_environ和_dl_auxv中。
接着初始化user_entry为ENTRY_POINT。然后遍历create_elf_table函数中设置的变量,依次设置到各个变量中,其中最重要的就是获取用户程序的入口设置到user_entry中。再往下设置_dl_pagesize和_dl_platformlen变量。
然后通过DL_SYSDEP_INIT宏用于设置堆的起始地址,再通过DL_PLATFORM_INIT检查相应变量。接着调整堆的起始地址和页面对齐。
最后调用dl_main准备为用户程序的执行搭建环境,传入的参数phdr为用户程序的程序头,phnum为程序头的个数,user_entry为程序的起始点,_dl_auxv为create_elf_table中设置的变量,dl_main函数留在下一章分析。

elf/dl-sysdep.c
_dl_start->_dl_start_final->_dl_sysdep_start->DL_FIND_ARG_COMPONENTS

# define DL_FIND_ARG_COMPONENTS(cookie, argc, argv, envp, auxp) 
  do {                                        
    void **_tmp;                                  
    (argc) = *(long int *) cookie;                        
    (argv) = (char **) ((long int *) cookie + 1);                 
    (envp) = (argv) + (argc) + 1;                         
    for (_tmp = (void **) (envp); *_tmp; ++_tmp)                  
      continue;                                   
    (auxp) = (void *) ++_tmp;                             
  } while (0)

cookie是栈的指针,这里移动该指针依次获取argc、argv、envp和auxp。

sysdeps/unix/sysv/linux/dl-sysdep.c
_dl_start->_dl_start_final->_dl_sysdep_start->DL_SYSDEP_INIT

# define DL_SYSDEP_INIT frob_brk ()

static inline void
frob_brk (void)
{
  __brk (0);
}

int
__brk (void *addr)
{
  void *newbrk;

  __curbrk = newbrk = (void *) INLINE_SYSCALL (brk, 1, addr);
  return 0;
}

SYSCALL_DEFINE1(brk, unsigned long, brk)
{
    ...

    min_brk = mm->start_brk;
    if (brk < min_brk)
        goto out;

    ...

    retval = mm->brk;
    return retval;
}

__brk函数会执行系统调用sys_brk,但是由于传入的参数为0,小于start_brk,因此最后只会返回当前堆的位置,但是由于当前进程还没有使用堆,因此实际上返回的就是堆的起始地址start_brk,然后返回到glibc中,将其保存到__curbrk中。