dlopen代碼詳解——從ELF格式到mmap


最近一個月的時間大部分在研究glibc中dlopen的代碼,基本上對整個流程建立了一個基本的了解。由於網上相關資料比較少,走了不少彎路,故在此記錄一二,希望后人能夠站在我這個矮子的肩上做出精彩的成果。

ELF格式簡介

dlopen是用來加載ELF文件中的共享對象(shared object,下文簡稱為so)的。ELF文件有多種類別,通過其header中0x10處的兩個字節標識,參考Wikipedia。ELF的header中還包含了一些額外信息如指令集、操作系統信息等等,在本文中不會涉及。
可以把一個ELF文件分為4塊:header、program header(phdr) table、section header(shdr) table、sections。下圖將其解釋地比較清楚了:

其中,最重要的概念就是phdr與shdr,它們分別對應着segment與section這兩個在dlopen過程中至關重要的概念,可以使用以下命令查看:

readelf -S lib1.so  #查看section信息
There are 33 section headers, starting at offset 0x20f8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .note.gnu.build-i NOTE             00000000000001c8  000001c8
       0000000000000024  0000000000000000   A       0     0     4
  [ 2] .gnu.hash         GNU_HASH         00000000000001f0  000001f0
       0000000000000050  0000000000000000   A       3     0     8
  [ 3] .dynsym           DYNSYM           0000000000000240  00000240
       0000000000000198  0000000000000018   A       4     1     8
  [ 4] .dynstr           STRTAB           00000000000003d8  000003d8
       00000000000000c5  0000000000000000   A       0     0     1
      ......

每一個section中存放不同用途的數據,以“.”開頭,比如我們熟悉的.text,.data,.bss。

readelf -l lib1.so  #查看segment信息
Elf file type is DYN (Shared object file)
Entry point 0x600
There are 7 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000007cc 0x00000000000007cc  R E    0x200000
  LOAD           0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
                 0x0000000000000230 0x0000000000000288  RW     0x200000
  DYNAMIC        0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_EH_FRAME   0x000000000000072c 0x000000000000072c 0x000000000000072c
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
                 0x0000000000000200 0x0000000000000200  R      0x1

 Section to Segment mapping:
  Segment Sections...
   00     .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame 
   01     .init_array .fini_array .dynamic .got .got.plt .data .bss 
   02     .dynamic 
   03     .note.gnu.build-id 
   04     .eh_frame_hdr 
   05     
   06     .init_array .fini_array .dynamic .got 

詳細地顯示了每個segment的類型、虛擬地址、物理地址、占文件空間(FileSiz)占內存空間(MemSiz)、保護模式、對齊信息,以及每一個segment包含哪些section
一句話概括,不同意義的信息存儲在不同的section中,數個section聚合為一個segment。在加載時,我們只關心segment。

dlopen的代碼結構

dlopen定義在頭文件dlfcn.h中,但其實現橫跨了dlfcn/與elf/兩個文件夾,且涉及了多個文件與函數,相當復雜。下面簡單分析其調用流程:
(in dlfcn/dlopen.c)dlopen -> __dlopen -> dlopen_doit -> (in elf/dl-open.c) _dl_open -> dl_open_worker -> (in dl-load.c) _dl_map_object -> _dl_map_object_from_fd
(in elf/dl-map-segments.h) _dl_map_segments -> __mmap -> 系統調用
這樣分配的原因可能是,dlfcn文件夾下的文件被編譯為libdl.so,而elf文件夾下的文件部分被編譯成ld.so,部分被編譯為libc.so。有些接口與成員只能在ld.so內被使用,如下面的例子:
In include/link.h:

struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;		/* Difference between the address in the ELF
				   file and the addresses in memory.  */
    char *l_name;		/* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;		/* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */

    /* All following members are internal to the dynamic linker.
       They may change without notice.  */

    /* This is an element which is only ever different from a pointer to
       the very same copy of this type for ld.so when it is used in more
       than one namespace.  */
    struct link_map *l_real;
    ......

所以,因為在libdl.so中不能訪問到某些元素,決定了dlopen不能只在dlfcn/下實現,所以真正的工作需要elf/中的文件進行實現,類似於幫助dlopen干活的工人,即dl_open_worker。而dlfcn/中的部分主要負責配置參數與錯誤處理。

dlopen實現詳解

注:此處只對dlopen的主干進行解釋,沒有涉及邊界條件以及次要部分(如加載一個so的依賴等)

dlopen

void *
dlopen (const char *file, int mode)
{
  return __dlopen (file, mode, RETURN_ADDRESS (0));
}

為用戶提供調用的接口,調用實際進行工作的函數__dlopen

__dlopen

struct dlopen_args
{
  /* The arguments for dlopen_doit.  */
  const char *file;
  int mode;
  /* The return value of dlopen_doit.  */
  void *new; //返回一個地址,即加載完成之后返回handle的地址
  /* Address of the caller.  */
  const void *caller;
};

void *
__dlopen (const char *file, int mode DL_CALLER_DECL)
{
# ifdef SHARED
  if (!rtld_active ())
    return _dlfcn_hook->dlopen (file, mode, DL_CALLER);
# endif

  struct dlopen_args args; //准備下一步調用的參數,裝在這個struct中
  args.file = file;
  args.mode = mode;
  args.caller = DL_CALLER;

# ifdef SHARED
  return _dlerror_run (dlopen_doit, &args) ? NULL : args.new; //_dlerror_run是用來錯誤處理的外層函數,接受一個函數指針與一個dlopen_args
  //在這個函數內部,dlopen_doit接受以參數args運行,在其執行結束之后取出args.new
# else
  if (_dlerror_run (dlopen_doit, &args))
    return NULL;

  __libc_register_dl_open_hook ((struct link_map *) args.new); //與libc內部調用dlopen有關,非主干內容
  __libc_register_dlfcn_hook ((struct link_map *) args.new);

  return args.new;
# endif
}

dlopen_doit

static void
dlopen_doit (void *a)
{
  struct dlopen_args *args = (struct dlopen_args *) a;

  if (args->mode & ~(RTLD_BINDING_MASK | RTLD_NOLOAD | RTLD_DEEPBIND
		     | RTLD_GLOBAL | RTLD_LOCAL | RTLD_NODELETE
		     | __RTLD_SPROF))
    _dl_signal_error (0, NULL, NULL, _("invalid mode parameter"));

  args->new = GLRO(dl_open) (args->file ?: "", args->mode | __RTLD_DLOPEN,
			     args->caller,
			     args->file == NULL ? LM_ID_BASE : NS,
			     __dlfcn_argc, __dlfcn_argv, __environ); //GLRO為預編譯命令,此處調用_dl_open
  //調用結束之后將args->new配置好
}

_dl_open

struct dl_open_args //同樣是承載參數的結構
{
  const char *file;
  int mode;
  /* This is the caller of the dlopen() function.  */
  const void *caller_dlopen;
  struct link_map *map;
  /* Namespace ID.  */
  Lmid_t nsid;

  /* Original value of _ns_global_scope_pending_adds.  Set by
     dl_open_worker.  Only valid if nsid is a real namespace
     (non-negative).  */
  unsigned int original_global_scope_pending_adds;

  /* Original parameters to the program and the current environment.  */
  int argc;
  char **argv;
  char **env;
};

void *
_dl_open (const char *file, int mode, const void *caller_dlopen, Lmid_t nsid,
	  int argc, char *argv[], char *env[])
{
  ......

  struct dl_open_args args;
  args.file = file;
  args.mode = mode;
  args.caller_dlopen = caller_dlopen;
  args.map = NULL;
  args.nsid = nsid;
  args.argc = argc;
  args.argv = argv;
  args.env = env;
  
  struct dl_exception exception;
  int errcode = _dl_catch_exception (&exception, dl_open_worker, &args); //與上面的_dlerror_run類似,是一個接受參數並處理錯誤的wrapper

dl_open_worker

static void
dl_open_worker (void *a)
{
  struct dl_open_args *args = a; //創建臨時變量承載參數
  const char *file = args->file;
  int mode = args->mode;
  struct link_map *call_map = NULL;
  ......
  /* Load the named object.  */
  struct link_map *new; //創建一個新的link_map,用來存放要加載的so
  args->map = new = _dl_map_object (call_map, file, lt_loaded, 0,
				    mode | __RTLD_CALLMAP, args->nsid); //開始將so映射到內存中去
  ......
}

_dl_map_object

struct link_map *
_dl_map_object (struct link_map *loader, const char *name,
		int type, int trace_mode, int mode, Lmid_t nsid)
{
  ......
  //主要在尋找是否存在已經打開了的so,如果有,直接將對應的link_map返回
  return _dl_map_object_from_fd (name, origname, fd, &fb, realname, loader,
				 type, mode, &stack_end, nsid); //用一個fd開始進行內存映射

_dl_map_object_from_fd

struct link_map *
_dl_map_object_from_fd (const char *name, const char *origname, int fd,
			struct filebuf *fbp, char *realname,
			struct link_map *loader, int l_type, int mode,
			void **stack_endp, Lmid_t nsid)
{
  ......
  {
    /* Scan the program header table, collecting its load commands.  */
    struct loadcmd loadcmds[l->l_phnum]; //loadcmd中每一個元素對應elf中的一個segment,所以它的長度等於elf中phdr的個數
    size_t nloadcmds = 0; //並非loadcmd的長度,而是LOAD類segment的個數,見下文
    bool has_holes = false; 

    for (ph = phdr; ph < &phdr[l->l_phnum]; ++ph)
      switch (ph->p_type)
	{
        case PT_DYNAMIC: //別的類型的segment,可以無視
            ......
        case PT_PHDR:
            ......
        case PT_LOAD: //最重要的類型,每一個LOAD segment都要被加載進內存
            ......
          struct loadcmd *c = &loadcmds[nloadcmds++]; //只有PT_LOAD類型才會增加nloadcmds
	  c->mapstart = ALIGN_DOWN (ph->p_vaddr, GLRO(dl_pagesize));  //獲得映射的開始地址,由於直接與虛擬內存對應,需要頁對齊
	  c->mapend = ALIGN_UP (ph->p_vaddr + ph->p_filesz, GLRO(dl_pagesize)); //獲取結束地址
	  c->dataend = ph->p_vaddr + ph->p_filesz; //filesz與memsz只在一種情況時不同,見下文。
	  c->allocend = ph->p_vaddr + ph->p_memsz; 
	  c->mapoff = ALIGN_DOWN (ph->p_offset, GLRO(dl_pagesize));

          if (nloadcmds > 1 && c[-1].mapend != c->mapstart) // 當一個LOAD類型的開始地址與上一個LOAD的結束地址不同時,判定為有洞
	    has_holes = true;
          /* Now process the load commands and map segments into memory.
          This is responsible for filling in:
          l_map_start, l_map_end, l_addr, l_contiguous, l_text_end, l_phdr
          */
          errstring = _dl_map_segments (l, fd, header, type, loadcmds, nloadcmds,
				  maplength, has_holes, loader); //將整理好的loadcmds作為參數,開始進行真正的映射
        }
  }
  ......
}

這里的switch與上文中講的segment的類型相對應,不同的segment對應不同的操作。只有segment類型為PT_LOAD的才會放到loadcmds中,加載到內存中去。loadcmds也是在這里配置完畢的。

_dl_map_segments

static __always_inline const char *
_dl_map_segments (struct link_map *l, int fd,
                  const ElfW(Ehdr) *header, int type,
                  const struct loadcmd loadcmds[], size_t nloadcmds,
                  const size_t maplength, bool has_holes,
                  struct link_map *loader)
{
  ......
  ElfW(Addr) mappref
        = (ELF_PREFERRED_ADDRESS (loader, maplength,
                                  c->mapstart & GLRO(dl_use_load_bias))
           - MAP_BASE_ADDR (l)); //mmap的第一個參數接受一個preferred location,一般來說這個值都是0,即由OS決定基地址

  l->l_map_start = (ElfW(Addr)) __mmap ((void *) mappref, maplength,
                                            c->prot,
                                            MAP_COPY|MAP_FILE,
                                            fd, c->mapoff); //注意此處MAP_FIXED flag沒有打開,不會分配到固定地址
  ......
  if (has_holes)
        {
          /* Change protection on the excess portion to disallow all access;
             the portions we do not remap later will be inaccessible as if
             unallocated.  Then jump into the normal segment-mapping loop to
             handle the portion of the segment past the end of the file
             mapping.  */
          if (__glibc_unlikely
              (__mprotect ((caddr_t) (l->l_addr + c->mapend),
                           loadcmds[nloadcmds - 1].mapstart - c->mapend,
                           PROT_NONE) < 0)) //使用mprotect改變上文中提到的“洞”的訪問權限為不允許任何訪問
            return DL_MAP_SEGMENTS_ERROR_MPROTECT;
        }
  while (c < &loadcmds[nloadcmds])
    {
      if (c->mapend > c->mapstart //mapend > mapstart是expected behavior
          /* Map the segment contents from the file.  */
          && (__mmap ((void *) (l->l_addr + c->mapstart),
                      c->mapend - c->mapstart, c->prot,
                      MAP_FIXED|MAP_COPY|MAP_FILE, //后續的segment被映射到固定的地址,從前一個的結束地址開始
                      fd, c->mapoff)
              == MAP_FAILED)) //當mmap出錯時,退出;否則就是正常的mmap loadcmds中下一個segment
        return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;
      ......
      if (c->allocend > c->dataend) //這個條件用來判斷是否進入了最后一個LOAD
        {
          /* Extra zero pages should appear at the end of this segment,
             after the data mapped from the file.   */ //在最后一個segment中,沒有被用到的部分用0填充
          ElfW(Addr) zero, zeroend, zeropage;

          zero = l->l_addr + c->dataend; //.data section的結束
          zeroend = l->l_addr + c->allocend; //.bss section的結束
          zeropage = ((zero + GLRO(dl_pagesize) - 1)
                      & ~(GLRO(dl_pagesize) - 1)); //.data section結束地址的下一頁的開始地址
          if (zeroend < zeropage)
            /* All the extra data is in the last page of the segment.
               We can just zero it.  */
            zeropage = zeroend;

          if (zeropage > zero)
            {
              /* Zero the final part of the last page of the segment.  */
              if (__glibc_unlikely ((c->prot & PROT_WRITE) == 0))
                {
                  /* Dag nab it.  */
                  if (__mprotect ((caddr_t) (zero
                                             & ~(GLRO(dl_pagesize) - 1)),
                                  GLRO(dl_pagesize), c->prot|PROT_WRITE) < 0)
                    return DL_MAP_SEGMENTS_ERROR_MPROTECT;
                }
              memset ((void *) zero, '\0', zeropage - zero);
              if (__glibc_unlikely ((c->prot & PROT_WRITE) == 0))
                __mprotect ((caddr_t) (zero & ~(GLRO(dl_pagesize) - 1)),
                            GLRO(dl_pagesize), c->prot);
            }

          if (zeroend > zeropage) //當.bss section的長度超過最后一頁的剩余長度時,此時需要新增若干頁,需要再次調mmap
            {
              /* Map the remaining zero pages in from the zero fill FD.  */
              caddr_t mapat;
              mapat = __mmap ((caddr_t) zeropage, zeroend - zeropage,
                              c->prot, MAP_ANON|MAP_PRIVATE|MAP_FIXED, //MAP_ANON打開,因為建立的映射不對應於任何一個fd
                              -1, 0);
              if (__glibc_unlikely (mapat == MAP_FAILED))
                return DL_MAP_SEGMENTS_ERROR_MAP_ZERO_FILL;
            }
        }
     ++c; //loadcmds中下一條命令
    }

這是最重要,最復雜的一個函數,也是dlopen最底層的系統調用。它的工作流程如下:

  1. 沒有特殊情況時,mappref為0,由OS自行選擇基地址,並將其返回
  2. 后續的segment緊接着這個地址進行映射
  3. 到達最后一個segment時,需要處理allocend和dataend的情況,由.bss section引起

此處結合ELF文件的格式,講解為什么.bss section有這樣的情況:
回顧上文中lib1.so的phdr table:

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x00000000000007cc 0x00000000000007cc  R E    0x200000
  LOAD           0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
                 0x0000000000000230 0x0000000000000288  RW     0x200000
  DYNAMIC        0x0000000000000e10 0x0000000000200e10 0x0000000000200e10
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_EH_FRAME   0x000000000000072c 0x000000000000072c 0x000000000000072c
                 0x0000000000000024 0x0000000000000024  R      0x4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     0x10
  GNU_RELRO      0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
                 0x0000000000000200 0x0000000000000200  R      0x1

只有第二個LOAD中出現了FileSiz != MemSiz的情況。這是因為,在ELF中需要存儲全局變量的初始值,而由於.bss沒有初始值,默認被初始化為0,所以不會在ELF中存儲,使得變量在文件中占用的大小(FileSiz)小於運行時占用的內存空間(MemSiz)。在加載到內存中時,使用這個特征判斷是否到達了最后一個LOAD segment。
同時,可以注意到兩個LOAD之間的虛擬地址(即加載到虛擬內存中時的偏移量,上文中的VirtAddr)差距很大,這是因為想要盡量保證可執行的部分與不可執行的部分相差盡可能大,從而最小化溢出時可能造成的寫掉.text的風險,見出處。這也是上文中“洞”的由來。

在筆者所做的實驗中,所有so都只有兩個LOAD segment,一個是可執行的,另一個是不可執行的,包含的section見上文輸出。然而,在某些系統上,可能會有其它的聚合方式,詳見這個例子。這與系統產生ELF文件的實現有關。

關於link_map

link_map是用來存儲ELF文件的數據結構,其詳細定義可以在include/link.h下找到。
dlopen返回的打開的so的handle。這個handle是一個可以被其它libdl函數使用的接口,如dlsym,dlclose。需要注意它與so不存儲在一起,也不是so在內存中的基地址。

結語

時間倉促,dlopen的實現只挑了主干研究,其它部分還沒空顧及,一些支撐我得到結論的實驗也沒有放上來。希望能與各路大神深入交流。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM