Android linker运行机制分析

这周分析了一下安卓源码中 linker 的部分,了解了 Android 平台下是如何加载动态链接库的。

Android源码linker的实现

首先下载安卓源码,过程参考

下载源代码|Android 开源项目|Android Open Source Project

我们这次分析的是android-4.0.1_r1

Linker的代码位于linker.c和linker.h。我们从dlfcn.c中的dlopen入手:

dlopen

首先将dl_lock锁住。dlopen 主要函数为 find_library。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void *dlopen(const char *filename, int flag)
{
soinfo *ret;

pthread_mutex_lock(&dl_lock);
ret = find_library(filename);
if (unlikely(ret == NULL)) {
set_dlerror(DL_ERR_CANNOT_LOAD_LIBRARY);
} else {
ret->refcount++;
}
pthread_mutex_unlock(&dl_lock);
return ret;
}

find_library

先从solist中搜索是否已加载还没加载则调用load_library加载so,和init_library初始化库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bname = strrchr(name, '/');
bname = bname ? bname + 1 : name;

for(si = solist; si != 0; si = si->next){
if(!strcmp(bname, si->name)) {
if(si->flags & FLAG_ERROR) {
DL_ERR("%5d '%s' failed to load previously", pid, bname);
return NULL;
}
if(si->flags & FLAG_LINKED) return si;
DL_ERR("OOPS: %5d recursive link to '%s'", pid, si->name);
return NULL;
}
}

TRACE("[ %5d '%s' has not been loaded yet. Locating...]\n", pid, name);
si = load_library(name);
if(si == NULL)
return NULL;
return init_library(si);

load_library

首先open_library打开共享库

open_library

如果是绝对路径,则直接打开

否则从ldpath和sopath中搜索然后打开

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if ((name[0] == '/') && ((fd = _open_lib(name)) >= 0))
return fd;

for (path = ldpaths; *path; path++) {
n = format_buffer(buf, sizeof(buf), "%s/%s", *path, name);
if (n < 0 || n >= (int)sizeof(buf)) {
WARN("Ignoring very long library path: %s/%s\n", *path, name);
continue;
}
if ((fd = _open_lib(buf)) >= 0)
return fd;
}
for (path = sopaths; *path; path++) {
n = format_buffer(buf, sizeof(buf), "%s/%s", *path, name);
if (n < 0 || n >= (int)sizeof(buf)) {
WARN("Ignoring very long library path: %s/%s\n", *path, name);
continue;
}
if ((fd = _open_lib(buf)) >= 0)
return fd;
}
read header

读取so的第一页,即header部分的内容

1
2
3
4
5
6
7
8
9
if (lseek(fd, 0, SEEK_SET) < 0) {
DL_ERR("lseek() failed!");
goto fail;
}

if ((cnt = read(fd, &__header[0], PAGE_SIZE)) < 0) {
DL_ERR("read() failed!");
goto fail;
}
get_lib_extents

这里负责设定共享库的基地址和加载时的大小,大小通过参数total_sz传入并设定

1
2
static unsigned
get_lib_extents(int fd, const char *name, void *__hdr, unsigned *total_sz)

如果没有prelink过,则基址设为0,后续mmap时交给自动指定合适的机制

1
2
3
4
5
6
7
8
9
req_base = (unsigned) is_prelinked(fd, name);
if (req_base == (unsigned)-1)
return -1;
else if (req_base != 0) {
TRACE("[ %5d - Prelinked library '%s' requesting base @ 0x%08x ]\n",
pid, name, req_base);
} else {
TRACE("[ %5d - Non-prelinked library '%s' found. ]\n", pid, name);
}

遍历PT_LOAD的程序头,计算最大地址和最小地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
phdr = (Elf32_Phdr *)(_hdr + ehdr->e_phoff);

/* find the min/max p_vaddrs from all the PT_LOAD segments so we can
* get the range. */
for (cnt = 0; cnt < ehdr->e_phnum; ++cnt, ++phdr) {
if (phdr->p_type == PT_LOAD) {
if ((phdr->p_vaddr + phdr->p_memsz) > max_vaddr)
max_vaddr = phdr->p_vaddr + phdr->p_memsz;
if (phdr->p_vaddr < min_vaddr)
min_vaddr = phdr->p_vaddr;
}
}
min_vaddr &= ~PAGE_MASK;

/* round max_vaddr up to the next page */
max_vaddr = (max_vaddr + PAGE_SIZE - 1) & ~PAGE_MASK;

*total_sz = (max_vaddr - min_vaddr);
return (unsigned)req_base;
alloc_mem_region

先初始化soinfo结构体,然后分配初始内存

1
2
3
4
5
6
7
8
9
10
si->base = req_base;
si->size = ext_sz;
si->flags = 0;
si->entry = 0;
si->dynamic = (unsigned *)-1;
if (alloc_mem_region(si) < 0)
goto fail;

TRACE("[ %5d allocated memory for %s @ %p (0x%08x) ]\n",
pid, name, (void *)si->base, (unsigned) ext_sz);

使用mmap映射匿名空间,权限为读取和执行

1
2
3
4
5
6
7
8
9
10
11
12
void *base = mmap(NULL, si->size, PROT_READ | PROT_EXEC,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
if (base == MAP_FAILED) {
DL_ERR("%5d mmap of library '%s' failed: %d (%s)\n",
pid, si->name,
errno, strerror(errno));
goto err;
}
si->base = (unsigned) base;
PRINT("%5d mapped library '%s' to %08x via kernel allocator.\n",
pid, si->name, si->base);
return 0;
load_segments

将每一个段加载进内存

自然是遍历每个PT_LOAD的程序头

1
2
3
for (cnt = 0; cnt < ehdr->e_phnum; ++cnt, ++phdr) {
if (phdr->p_type == PT_LOAD) {

将该段映射到地址空间,大小对齐到页,权限为程序头中的权限。此时的映射就不是匿名影射了,直接将so的文件映射过去,所以会在/proc/[pid]/maps中看到路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
/* we want to map in the segment on a page boundary */
tmp = base + (phdr->p_vaddr & (~PAGE_MASK));
/* add the # of bytes we masked off above to the total length. */
len = phdr->p_filesz + (phdr->p_vaddr & PAGE_MASK);

TRACE("[ %d - Trying to load segment from '%s' @ 0x%08x "
"(0x%08x). p_vaddr=0x%08x p_offset=0x%08x ]\n", pid, si->name,
(unsigned)tmp, len, phdr->p_vaddr, phdr->p_offset);
pbase = mmap(tmp, len, PFLAGS_TO_PROT(phdr->p_flags),
MAP_PRIVATE | MAP_FIXED, fd,
phdr->p_offset & (~PAGE_MASK));
if ((len & PAGE_MASK) && (phdr->p_flags & PF_W))
memset((void *)(pbase + len), 0, PAGE_SIZE - (len & PAGE_MASK));

如果整个段的长度大于页的大小,则还要额外映射额外的长度。

比如在这个例子中,第二个段的结束地址0x2d48+0x2c0 = 0x3008,超出了当前页的大小0x3000,需要额外8字节的页

1
2
3
4
5
6
7
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000d60 0x0000000000000d60 R E 0x1000
LOAD 0x0000000000001d48 0x0000000000002d48 0x0000000000002d48
0x00000000000002c0 0x00000000000002c0 RW 0x1000
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
tmp = (unsigned char *)(((unsigned)pbase + len + PAGE_SIZE - 1) &
(~PAGE_MASK));
if (tmp < (base + phdr->p_vaddr + phdr->p_memsz)) {
extra_len = base + phdr->p_vaddr + phdr->p_memsz - tmp;
TRACE("[ %5d - Need to extend segment from '%s' @ 0x%08x "
"(0x%08x) ]\n", pid, si->name, (unsigned)tmp, extra_len);
/* map in the extra page(s) as anonymous into the range.
* This is probably not necessary as we already mapped in
* the entire region previously, but we just want to be
* sure. This will also set the right flags on the region
* (though we can probably accomplish the same thing with
* mprotect).
*/
extra_base = mmap((void *)tmp, extra_len,
PFLAGS_TO_PROT(phdr->p_flags),
MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS,
-1, 0);
if (extra_base == MAP_FAILED) {
DL_ERR("[ %5d - failed to extend segment from '%s' @ 0x%08x"
" (0x%08x) ]", pid, si->name, (unsigned)tmp,
extra_len);
goto fail;
}
/* TODO: Check if we need to memset-0 this region.
* Anonymous mappings are zero-filled copy-on-writes, so we
* shouldn't need to. */
TRACE("[ %5d - Segment from '%s' extended @ 0x%08x "
"(0x%08x)\n", pid, si->name, (unsigned)extra_base,
extra_len);
}

如果有写权限还要加上写权限:

这里的pflag和prot中的读和执行权限和map的是反的,所以用了一个宏反了过来。

1
2
3
4
5
6
7
8
if (!(phdr->p_flags & PF_W)) {
if ((unsigned)pbase < si->wrprotect_start)
si->wrprotect_start = (unsigned)pbase;
if (((unsigned)pbase + len) > si->wrprotect_end)
si->wrprotect_end = (unsigned)pbase + len;
mprotect(pbase, len,
PFLAGS_TO_PROT(phdr->p_flags) | PROT_WRITE);
}

全部segments load完毕后,关闭文件,然后进行初始化。

init_library

这里主要要做的工作就是对共享库重定向,将其中的地址修改为正确地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static soinfo *
init_library(soinfo *si)
{
unsigned wr_offset = 0xffffffff;

/* At this point we know that whatever is loaded @ base is a valid ELF
* shared library whose segments are properly mapped in. */
TRACE("[ %5d init_library base=0x%08x sz=0x%08x name='%s') ]\n",
pid, si->base, si->size, si->name);

if(link_image(si, wr_offset)) {
/* We failed to link. However, we can only restore libbase
** if no additional libraries have moved it since we updated it.
*/
munmap((void *)si->base, si->size);
return NULL;
}

return si;
}

link_image主要做这些事:

首先遍历程序头,获取dynmic段的地址:

1
2
3
4
5
6
7
8
9
10
11
} else if (phdr->p_type == PT_DYNAMIC) {
if (si->dynamic != (unsigned *)-1) {
DL_ERR("%5d multiple PT_DYNAMIC segments found in '%s'. "
"Segment at 0x%08x, previously one found at 0x%08x",
pid, si->name, si->base + phdr->p_vaddr,
(unsigned)si->dynamic);
goto fail;
}
DEBUG_DUMP_PHDR(phdr, "PT_DYNAMIC", pid);
si->dynamic = (unsigned *) (si->base + phdr->p_vaddr);
}

遍历Elf32_Dyn结构体数组,设置soinfo结构体成员。比较重要的内容有(注意这里说的地址都是相对虚拟地址):

DT_STRTAB:符号表字符串地址

DT_SYMTAB:符号表地址

DT_JMPREL,DT_PLTRELSZ:跳转重定向表及大小

DT_RELA,DT_RELASZ:重定向表地址及大小

DT_INIT_ARRAY,DT_INIT_ARRAYSZ,DT_FINI_ARRAY,DT_FINI_ARRAYSZ:init_array和fini_array的地址及信息

这些地址信息都是在加载时需要重定向的,所以这里记录重定向后的地址,以便后续修改。

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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
    for(d = si->dynamic; *d; d++){
DEBUG("%5d d = %p, d[0] = 0x%08x d[1] = 0x%08x\n", pid, d, d[0], d[1]);
switch(*d++){
case DT_HASH:
si->nbucket = ((unsigned *) (si->base + *d))[0];
si->nchain = ((unsigned *) (si->base + *d))[1];
si->bucket = (unsigned *) (si->base + *d + 8);
si->chain = (unsigned *) (si->base + *d + 8 + si->nbucket * 4);
break;
case DT_STRTAB:
si->strtab = (const char *) (si->base + *d);
break;
case DT_SYMTAB:
si->symtab = (Elf32_Sym *) (si->base + *d);
break;
#if !defined(ANDROID_SH_LINKER)
case DT_PLTREL:
if(*d != DT_REL) {
DL_ERR("DT_RELA not supported");
goto fail;
}
break;
#endif
#ifdef ANDROID_SH_LINKER
case DT_JMPREL:
si->plt_rela = (Elf32_Rela*) (si->base + *d);
break;
case DT_PLTRELSZ:
si->plt_rela_count = *d / sizeof(Elf32_Rela);
break;
#else
case DT_JMPREL:
si->plt_rel = (Elf32_Rel*) (si->base + *d);
break;
case DT_PLTRELSZ:
si->plt_rel_count = *d / 8;
break;
#endif
case DT_REL:
si->rel = (Elf32_Rel*) (si->base + *d);
break;
case DT_RELSZ:
si->rel_count = *d / 8;
break;
#ifdef ANDROID_SH_LINKER
case DT_RELASZ:
si->rela_count = *d / sizeof(Elf32_Rela);
break;
#endif
case DT_PLTGOT:
/* Save this in case we decide to do lazy binding. We don't yet. */
si->plt_got = (unsigned *)(si->base + *d);
break;
case DT_DEBUG:
// Set the DT_DEBUG entry to the addres of _r_debug for GDB
*d = (int) &_r_debug;
break;
#ifdef ANDROID_SH_LINKER
case DT_RELA:
si->rela = (Elf32_Rela *) (si->base + *d);
break;
#else
case DT_RELA:
DL_ERR("%5d DT_RELA not supported", pid);
goto fail;
#endif
case DT_INIT:
si->init_func = (void (*)(void))(si->base + *d);
DEBUG("%5d %s constructors (init func) found at %p\n",
pid, si->name, si->init_func);
break;
case DT_FINI:
si->fini_func = (void (*)(void))(si->base + *d);
DEBUG("%5d %s destructors (fini func) found at %p\n",
pid, si->name, si->fini_func);
break;
case DT_INIT_ARRAY:
si->init_array = (unsigned *)(si->base + *d);
DEBUG("%5d %s constructors (init_array) found at %p\n",
pid, si->name, si->init_array);
break;
case DT_INIT_ARRAYSZ:
si->init_array_count = ((unsigned)*d) / sizeof(Elf32_Addr);
break;
case DT_FINI_ARRAY:
si->fini_array = (unsigned *)(si->base + *d);
DEBUG("%5d %s destructors (fini_array) found at %p\n",
pid, si->name, si->fini_array);
break;
case DT_FINI_ARRAYSZ:
si->fini_array_count = ((unsigned)*d) / sizeof(Elf32_Addr);
break;
case DT_PREINIT_ARRAY:
si->preinit_array = (unsigned *)(si->base + *d);
DEBUG("%5d %s constructors (preinit_array) found at %p\n",
pid, si->name, si->preinit_array);
break;
case DT_PREINIT_ARRAYSZ:
si->preinit_array_count = ((unsigned)*d) / sizeof(Elf32_Addr);
break;
case DT_TEXTREL:
/* TODO: make use of this. */
/* this means that we might have to write into where the text
* segment was loaded during relocation... Do something with
* it.
*/
DEBUG("%5d Text segment should be writable during relocation.\n",
pid);
break;
}
}

接下来处理动态链接库引入的动态链接库:

找到type为DT_NEEDED的Dyn结构体,使用find_library加载该库。

一开始这个函数名让我很困扰,实际上这就是上文中的find_library,这个函数的作用不过是像他名字一样寻找(已加载)的库,未找到时还要再加载它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for(d = si->dynamic; *d; d += 2) {
if(d[0] == DT_NEEDED){
DEBUG("%5d %s needs %s\n", pid, si->name, si->strtab + d[1]);
soinfo *lsi = find_library(si->strtab + d[1]);
if(lsi == 0) {
strlcpy(tmp_err_buf, linker_get_error(), sizeof(tmp_err_buf));
DL_ERR("%5d could not load needed library '%s' for '%s' (%s)",
pid, si->strtab + d[1], si->name, tmp_err_buf);
goto fail;
}
/* Save the soinfo of the loaded DT_NEEDED library in the payload
of the DT_NEEDED entry itself, so that we can retrieve the
soinfo directly later from the dynamic segment. This is a hack,
but it allows us to map from DT_NEEDED to soinfo efficiently
later on when we resolve relocations, trying to look up a symgol
with dlsym().
*/
d[1] = (unsigned)lsi;
lsi->refcount++;
}
}

接下来是比较重要的重定向部分。我们知道,动态链接库里存放的都是相对虚拟地址,以及外部函数的符号。我们需要修复这些地址,将他改成真正的虚拟地址。

安卓没有dl_resolve这个过程,load的时候JMPREL Relocation Table的过程都要直接加载好,所以还要修改这些地址:

1
2
3
4
5
6
7
8
9
10
11
12
#ifdef ANDROID_SH_LINKER
if(si->plt_rela) {
DEBUG("[ %5d relocating %s plt ]\n", pid, si->name );
if(reloc_library_a(si, si->plt_rela, si->plt_rela_count))
goto fail;
}
if(si->rela) {
DEBUG("[ %5d relocating %s ]\n", pid, si->name );
if(reloc_library_a(si, si->rela, si->rela_count))
goto fail;
}
#endif /* ANDROID_SH_LINKER */

分析reloc_library_a:

reloc_library_a

遍历Elf32_Rela结构体数组。这里用到两个宏获取type和符号表的索引,并计算需要重定向的内容所在的地址

1
2
3
unsigned type = ELF32_R_TYPE(rela->r_info);
unsigned sym = ELF32_R_SYM(rela->r_info);
unsigned reloc = (unsigned)(rela->r_offset + si->base);

如果是符号,获取符号字符串,然后_do_lookup寻找该符号对应的位置。这里使用哈希表实现的存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
        if(sym != 0) {
sym_name = (char *)(strtab + symtab[sym].st_name);
s = _do_lookup(si, sym_name, &base);
if(s == 0) {
DL_ERR("%5d cannot locate '%s'...", pid, sym_name);
return -1;
}
#if 0
if((base == 0) && (si->base != 0)){
/* linking from libraries to main image is bad */
DL_ERR("%5d cannot locate '%s'...",
pid, strtab + symtab[sym].st_name);
return -1;
}
#endif
if ((s->st_shndx == SHN_UNDEF) && (s->st_value != 0)) {
DL_ERR("%5d In '%s', shndx=%d && value=0x%08x. We do not "
"handle this yet", pid, si->name, s->st_shndx,
s->st_value);
return -1;
}
sym_addr = (unsigned)(s->st_value + base);
COUNT_RELOC(RELOC_SYMBOL);
}

最后根据type对内容进行修改,起作用的最重要的一句话为*((unsigned*)reloc) += si->base;,这句换完成了RVA到VA的转换,以及写入外部函数的地址

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
switch(type){
case R_SH_JUMP_SLOT:
COUNT_RELOC(RELOC_ABSOLUTE);
MARK(rela->r_offset);
TRACE_TYPE(RELO, "%5d RELO JMP_SLOT %08x <- %08x %s\n", pid,
reloc, sym_addr, sym_name);
*((unsigned*)reloc) = sym_addr;
break;
case R_SH_GLOB_DAT:
COUNT_RELOC(RELOC_ABSOLUTE);
MARK(rela->r_offset);
TRACE_TYPE(RELO, "%5d RELO GLOB_DAT %08x <- %08x %s\n", pid,
reloc, sym_addr, sym_name);
*((unsigned*)reloc) = sym_addr;
break;
case R_SH_DIR32:
COUNT_RELOC(RELOC_ABSOLUTE);
MARK(rela->r_offset);
TRACE_TYPE(RELO, "%5d RELO DIR32 %08x <- %08x %s\n", pid,
reloc, sym_addr, sym_name);
*((unsigned*)reloc) += sym_addr;
break;
case R_SH_RELATIVE:
COUNT_RELOC(RELOC_RELATIVE);
MARK(rela->r_offset);
if(sym){
DL_ERR("%5d odd RELATIVE form...", pid);
return -1;
}
TRACE_TYPE(RELO, "%5d RELO RELATIVE %08x <- +%08x\n", pid,
reloc, si->base);
*((unsigned*)reloc) += si->base;
break;

default:
DL_ERR("%5d unknown reloc type %d @ %p (%d)",
pid, type, rela, (int) (rela - start));
return -1;
}

link_image的最后结尾,导入调试信息,这里暂时不分析了

1
notify_gdb_of_load(si);

这里是导入的动态链接库的构造函数,调用init_func,init_array。

1
call_constructors(si);

自定义的dlopen与dlsym实现

目标

在这个demo中,我们需要了三个库libhelloworld.so、liblua.so、liblink.so。其中liblink.so是正常从Dalvic调用进来的,已经加载到了内存。libhelloworld.so中依赖liblua.so。libhellowrold.so中的函数helloworld()会调用liblua.so的函数,并且需要执行lua脚本。

我们需要实现自己的dlopen和dlsym,加载libhelloworld.so,调用其中的helloworld,运行任意lua脚本。同时加载的libhelloworld.so不能在maps中显示。由于libhelloworld.so依赖了liblua.so,也需要加载这个so并且不能再maps显示。

思路

理解了源码中linker的实现,自己在实现一个dlopen就不难了。经过分析,maps中之所以会显示动态链接库的路径,是因为第二次map每一个segments的时候是将文件映射到内存,并非匿名映射,所以maps中会显示地址。我们可以参考源码中dlopen的实现,在load_segment的时候不采用文件映射的方法,而是读取文件并写入内存,这样这段内存仍然是匿名映射,就不会显示我们加载的动态链接库了。

需要实现的过程中很多地方需要注意,最重要的一点应该是对于重定向地址的修复,往往会忽略一些内容。对于data段存放的一些也需要重定向。注意考虑RELA和JMPREL中所有的类型。

第二点,如果我们加载的动态链接库也需要加载新的动态链接库,这些库如果原先没加载,我们也要加载它并隐藏,所以要用同样的自己实现的dlopen加载它。目前的方案是,如果在maps中没搜到这个库,统一用my_dlopen加载。

遇到的问题

  1. 忽略了.data段也存放了需要重定向的地址,实际上就是要对重定向表中的每一个类型都进行处理。
  2. 共享库加载共享库的问题。比如我们加载的库A,A还依赖库B,所以也要加载库B,否则对库B的引用都是RVA,无法正常运行。这里处理的时候是从maps中寻找,如果已经加载过这个库,则用libc的dlopen加载(经过分析dlopen的源码,同一个库是不会重复加载的)。如果(操作系统)没有加载过,则使用我们实现的mydlopen去加载这个库。
  3. 编译时遇到的一些问题。主要是不太熟悉Cmake和C++。lua的源代码是用C写的,所以编译出来的符号表是正常的函数符号。而我在写libhelloworld.so时用的是C++,那么定义的函数名会附带符号修饰,那么就不能在C写的.so中找到对应的符号,所以link的时候会失败(比如我们要找一个叫lua_tointegerx的符号,而实际编译出来的符号是_Z14lua_tointegerxP9lua_StateiPi

结果

我们在apk的data目录下存放了一个add.lua脚本:

1
2
3
function add(x,y)
return x+y
end

libhelloworld.so中的helloworld函数负责调用liblua.so的lua函数,运行这个脚本并返回结果,然后在log中打印出来。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void helloworld() {
LOGD("call helloworld.");
LOGD("gettimeofday at .data: %p", fun1);
int sum;
lua_State* L;
L = luaL_newstate();
luaopen_base(L);
luaL_dofile(L, "/data/user/0/fun.apeng.test2/add.lua");
lua_getglobal(L, "add");
lua_pushnumber(L, 1);
lua_pushnumber(L, 2);
lua_call(L, 2, 1);
sum = (int)lua_tointeger(L, -1);
lua_pop(L, 1);
LOGD("lua res:%d", sum);
lua_close(L);
}

运行apk后,查看pid并查看/proc/[pid]/maps,grep一下只有liblinker.so,没有libhelloworld.so和liblua.so。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
sagit:/ # ps
USER PID PPID VSZ RSS WCHAN ADDR S NAME
root 6689 4552 33528 1960 SyS_rt_si+ 0 S sh
root 6693 6689 36004 2312 0 0 R ps
sagit:/ # ps -elf | grep test2
u0_a121 6642 685 1 20:47:52 ? 00:00:00 fun.apeng.test2
root 6695 6689 0 20:48:37 pts/1 00:00:00 grep test2
sagit:/ # cat /proc/6642/maps | grep liblinker.so
75e6f53000-75e7030000 r-xp 00000000 103:01 1310751 /data/app/fun.apeng.test2-VB49mv5fOQMEsLfO7U-EnA==/lib/arm64/liblinker.so
75e7031000-75e7039000 r--p 000dd000 103:01 1310751 /data/app/fun.apeng.test2-VB49mv5fOQMEsLfO7U-EnA==/lib/arm64/liblinker.so
75e7039000-75e703a000 rw-p 000e5000 103:01 1310751 /data/app/fun.apeng.test2-VB49mv5fOQMEsLfO7U-EnA==/lib/arm64/liblinker.so
sagit:/ # cat /proc/6642/maps | grep libhelloworld.so
1|sagit:/ # cat /proc/6642/maps | grep liblua.so
1|sagit:/ #

调用一下helloworld函数,log中正确显示了内容,表明正确调用了helloworld,执行了lua脚本

1
2
3
2020-07-10 15:50:36.433 6642-6642/fun.apeng.test2 D/HELLO: call helloworld.
2020-07-10 15:50:36.433 6642-6642/fun.apeng.test2 D/HELLO: gettimeofday at .data: 0x76d09589fc
2020-07-10 15:50:36.434 6642-6642/fun.apeng.test2 D/HELLO: lua res:3

再次查看maps,并没有发现libhelloworld.so和liblua.so

1
2
3
sagit:/ # cat /proc/6642/maps | grep libhelloworld.so
1|sagit:/ # cat /proc/6642/maps | grep liblua.so
1|sagit:/ #

缺陷

第一个主要缺陷:同一个库可能会重复加载,对于比较复杂的库可能会占用大量内存。在操作系统中保存了加载过的所有库,而我实现的dlopen都是分离的,暂时无法共享大家都加载过什么库。后续可以继续优化,考虑记录下我们加载过的库,再去这些库中寻找。

第二个主要缺陷:新引入的库暂时是从同目录下寻找的,这样如果还要引入一些系统自带的库可能加载不到(比如测试时需要加载libc++_shared.so,就没有成功加载),可以考虑添加为在path中寻找并加载。