dlopen代码详解——从ELF格式到mmap
最近⼀个⽉的时间⼤部分在研究glibc中dlopen的代码,基本上对整个流程建⽴了⼀个基本的了解。由于⽹上相关资料⽐较少,⾛了不少弯路,故在此记录⼀⼆,希望后⼈能够站在我这个矮⼦的肩上做出精彩的成果。
ELF格式简介
dlopen是⽤来加载ELF⽂件中的共享对象(shared object,下⽂简称为so)的。ELF⽂件有多种类别,通过其header中0x10处的两个字节标识,。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] .u.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:
00    .u.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt . .text .fini .rodata .eh_frame_hdr .eh_frame
01    .init_array .fini_array .dynamic .got .got.plt .data .bss
02    .dynamic
03    .u.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.caller = DL_CALLER;
# ifdef SHARED
return _dlerror_run (dlopen_doit, &args) ? NULL : w; //_dlerror_run是⽤来错误处理的外层函数,接受⼀个函数指针与⼀个dlopen_args  //在这个函数内部,dlopen_doit接受以参数args运⾏,在其执⾏结束之后取出w
# else
if (_dlerror_run (dlopen_doit, &args))
return NULL;
__libc_register_dl_open_hook ((struct link_map *) w); //与libc内部调⽤dlopen有关,⾮主⼲内容
__libc_register_dlfcn_hook ((struct link_map *) w);
w;
# 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.caller_dlopen = caller_dlopen;
args.map = NULL;
args.nsid = nsid;
args.argc = argc;
args.argv = argv;
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:mmap格式怎么打开
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)

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。