dlopen源码分析

研究一下dlopen的源码。

通过调用链的形式分析dlopen源码。

首先,函数的入口是dlopen (const char *file, int mode)

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

函数跳转到了__dlopen,这个函数实际上做了两件事情,将struct dlopen_args args赋值,然后运行dlopen_doit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *
__dlopen (const char *file, int mode DL_CALLER_DECL)
{
...

# ifdef SHARED
return _dlerror_run (dlopen_doit, &args) ? NULL : args.new;
# else
if (_dlerror_run (dlopen_doit, &args))
return NULL;

...
return args.new;
}

其中结构体struct dlopen_args是这样的:

1
2
3
4
5
6
7
struct dlopen_args
{
const char *file; /* The arguments for dlopen_doit. */
int mode;
void *new; /* The return value of dlopen_doit. */
const void *caller; /* The return value of dlopen_doit. */
};

然后dlopen_doit的函数则是调用了__dl_open函数。

1
2
3
4
5
6
7
8
9
10
11

static void
dlopen_doit (void *a)
{
struct dlopen_args *args = (struct dlopen_args *) a;
...
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);
}

对于__dl_open函数,实际上调用了dl_open_worker函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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.caller_dl_open = RETURN_ADDRESS (0);
args.map = NULL;
args.nsid = nsid;
args.argc = argc;
args.argv = argv;
args.env = env;

const char *objname;
const char *errstring;
bool malloced;
int errcode = _dl_catch_error (&objname, &errstring, &malloced,
dl_open_worker, &args);
...
return args.map;
}

其中关键的数据存储在struct dl_open_args内,它的结构如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13

struct dl_open_args
{
const char *file;
int mode;
const void *caller_dlopen; /* This is the caller of dlopen(). */
const void *caller_dl_open; /* This is the caller of _dl_open(). */
struct link_map *map;
Lmid_t nsid; /* Namespace ID. */
int argc;
char **argv;
char **env;
};

dl_open_worker函数中,最重要的加载数据一步是通过调用_dl_map_object实现的。

1
2
3
4
/* Load the named object.  */
struct link_map *new;
args->map = new = _dl_map_object (call_map, file, lt_loaded, 0,
mode | __RTLD_CALLMAP, args->nsid);

接下来就是_dl_map_object函数,其主要的内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
struct link_map *
internal_function
_dl_map_object (struct link_map *loader, const char *name,
int type, int trace_mode, int mode, Lmid_t nsid)
{
int fd;
char *realname;
char *name_copy;
struct link_map *l;
struct filebuf fb;
...
/* Look for this name among those already loaded. */
for (l = GL(dl_ns)[nsid]._ns_loaded; l; l = l->l_next)
{
/* If the requested name matches the soname of a loaded object,
use that object. Elide this check for names that have not
yet been opened. */
if (__builtin_expect (l->l_faked, 0) != 0
|| __builtin_expect (l->l_removed, 0) != 0)
continue;
if (!_dl_name_match_p (name, l))
{
const char *soname;
if (__builtin_expect (l->l_soname_added, 1)
|| l->l_info[DT_SONAME] == NULL)
continue;

soname = ((const char *) D_PTR (l, l_info[DT_STRTAB])
+ l->l_info[DT_SONAME]->d_un.d_val);
if (strcmp (name, soname) != 0)
continue;

/* We have a match on a new name -- cache it. */
add_name_to_object (l, soname);
l->l_soname_added = 1;
}

/* We have a match. */
return l;
}

...
fd = open_path (name, namelen, mode & __RTLD_SECURE,
&main_map->l_rpath_dirs, &realname, &fb,
loader ?: main_map, LA_SER_RUNPATH, &found_other_class);

...
l = _dl_new_object (name_copy, name, type, loader,
mode, nsid))

return _dl_map_object_from_fd (name, fd, &fb, realname, loader, type, mode, &stack_end, nsid);
}

其中文件是通过open_path调用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int
open_path (const char *name, size_t namelen, int secure,
struct r_search_path_struct *sps, char **realname,
struct filebuf *fbp, struct link_map *loader, int whatcode,
bool *found_other_class)
{
...
do {
...
for (cnt = 0; fd == -1 && cnt < ncapstr; ++cnt) {
fd = open_verify (buf, fbp, loader, whatcode, found_other_class,
false);
...
}

if (fd != -1) {
*realname = (char *) malloc (buflen);
if (*realname != NULL)
{
memcpy (*realname, buf, buflen);
return fd; // here return the fd
}
else
{
/* No memory for the name, we certainly won't be able
to load and link it. */
__close (fd);
return -1;
}
}

/* Remember whether we found anything. */
any |= here_any;
}
while (*++dirs != NULL);

return -1;
}

其中open_verify函数通过调用__libc_read对ELF文件的文件头实现了读写。

接下来是_dl_new_object函数,它主要初始化一个新的struct link_map*数据结构,并把它加入到loader对应的单链中(猜测)。

最重要的是函数_dl_map_object_from_fd。这个函数包括了

  • link_map的二次查找,是否已经存在映射了.so文件的
  • 提取ELF头文件信息(程序入口地址,头文件信息等)
  • 对phdr(程序头表进行遍历),根据每个程序头的type类型做具体的操作
  • 进行映射
1
2
3
4
5
6
7
8
maplength = loadcmds[nloadcmds - 1].allocend - loadcmds[0].mapstart;

/* 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);
  • 调用mprotect修改内存权限
  • 修改TLS等设置
  • 修改符号表
1
2
/* Set up the symbol hash table.  */
_dl_setup_hash (l);

最后,是_dl_map_segments函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* This is a position-independent shared object.  We can let the
kernel map it anywhere it likes, but we must have space for all
the segments in their specified positions relative to the first.
So we map the first segment without MAP_FIXED, but with its
extent increased to cover all the segments. Then we remove
access from excess portion, and there is known sufficient space
there to remap from the later segments.

As a refinement, sometimes we have an address that we would
prefer to map such objects at; but this is only a preference,
the OS can do whatever it likes. */
ElfW(Addr) mappref
= (ELF_PREFERRED_ADDRESS (loader, maplength,
c->mapstart & GLRO(dl_use_load_bias))
- MAP_BASE_ADDR (l));

/* Remember which part of the address space this object uses. */
l->l_map_start = (ElfW(Addr)) __mmap ((void *) mappref, maplength,
c->prot,
MAP_COPY|MAP_FILE,
fd, c->mapoff);
if (__glibc_unlikely ((void *) l->l_map_start == MAP_FAILED))
return DL_MAP_SEGMENTS_ERROR_MAP_SEGMENT;

l->l_map_end = l->l_map_start + maplength;
l->l_addr = l->l_map_start - c->mapstart;

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. */
__mprotect ((caddr_t) (l->l_addr + c->mapend),
loadcmds[nloadcmds - 1].mapstart - c->mapend,
PROT_NONE);

l->l_contiguous = 1;

goto postmap;