文章目录
So的加载和启动
handle=dlopen(pathName,PTLD_LAZY);//获得指定文件的句柄,这个handle是soinfo*
dlsym(handle,"JNI_OnLoad");//获取该文件的JNI_OnLoad函数的地址
android加载共享库关键为dlopen函数:
void* dlopen(const char* filename, int flags) {
ScopedPthreadMutexLocker locker(&gDlMutex);
soinfo* result = do_dlopen(filename, flags);
if (result == NULL) {
__bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
return NULL;
}
return result;
}
此函数通过调研do_dlopen返回一个动态链接库的句柄,该句柄为一个soinfo结构体。
soinfo* do_dlopen(const char* name, int flags) {
if ((flags & ~(RTLD_NOW|RTLD_LAZY|RTLD_LOCAL|RTLD_GLOBAL)) != 0) {
DL_ERR("invalid flags to dlopen: %x", flags);
return NULL;
}
set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
soinfo* si = find_library(name); //查找动态链接库
if (si != NULL) {
si->CallConstructors();
}
set_soinfo_pool_protection(PROT_READ);
return si;
}
紧接着往下跟find_library:
static soinfo* find_library(const char* name) {
soinfo* si = find_library_internal(name);
if (si != NULL) {
si->ref_count++;
}
return si;
}
继续跟
static soinfo* find_library_internal(const char* name) {
……..
soinfo* si = find_loaded_library(name); //首先查看这个so是否已经加载,如果已经加载,就返回该so的soinfo
if (si != NULL) {
if (si->flags & FLAG_LINKED) {
return si;
}
DL_ERR("OOPS: recursive link to \"%s\"", si->name);
return NULL;
}
TRACE("[ '%s' has not been loaded yet. Locating...]", name);
si = load_library(name); //说明该so没有被加载,就调用此函数进行加载
if (si == NULL) {
return NULL;
}
// At this point we know that whatever is loaded @ base is a valid ELF
// shared library whose segments are properly mapped in.
TRACE("[ find_library_internal base=%p size=%zu name='%s' ]",
reinterpret_cast<void*>(si->base), si->size, si->name);
if (!soinfo_link_image(si)) {
//加载完so后,根据si的反馈进行链接。会在第3节进行详细分析
munmap(reinterpret_cast<void*>(si->base), si->size);
soinfo_free(si);
return NULL;
}
return si;
}
这个函数执行流程为:
- 使用find_load_library函数在已经加载的动态链接库链表里面查找该动态库,如果找到了,就返回该动态库的soinfo,否则第二步
- 此时,说明指定的动态链接库还没有被加载,就使用load_library函数来加载该动态库。
load_library函数是整个so加载过程中的比较重要的,创建了动态链接库的句柄:
static soinfo *load_library(const char * name){
//open the file
int fd=open_library(name);
if (fd == -1) {
DL_ERR("library \"%s\" not found", name);
return NULL;
}
// Read the ELF header and load the segments.
ElfReader elf_reader(name, fd);
if (!elf_reader.Load()) {
return NULL;
}
const char* bname = strrchr(name, '/');
soinfo* si = soinfo_alloc(bname ? bname + 1 : name);
if (si == NULL) {
return NULL;
}
si->base = elf_reader.load_start();
si->size = elf_reader.load_size();
si->load_bias = elf_reader.load_bias();
si->flags = 0;
si->entry = 0; //入口函数设为null
si->dynamic = NULL;
si->phnum = elf_reader.phdr_count();
si->phdr = elf_reader.loaded_phdr();
return si;
}
load_library函数的执行过程可以概括如下:
- 使用open_library函数打开指定so文件
- 创建ElfReader类对象,并通过该对象的load方法,读取elf文件头,然后通过分析elf文件来加载各个segments
- 使用soinfo_alloc函数分配soinfo结构体,并为这个结构体中的各个成员赋值
So文件的读取与加载工作
linker使用Elf类的load函数完成so文件的分析工作,该类的源代码在linker_phdr.cpp中,Load函数代码:
bool ElfReader::Load() {
return ReadElfHeader() &&
VerifyElfHeader() &&
ReadProgramHeader() &&
ReserveAddressSpace() &&
LoadSegments() &&
FindPhdr();
}
此函数依次调用ReadRlfHeader,verifyHeader,ReadProgramHeader(),ReserveAddressSpace()等等函数。
我们需要知道android系统加载segments的机制:
一个ELF文件的程序头表包含一个或多个PT_LOAD segments,这些segments标志ELF文件需要被映射到进程空间中的区域,每一个加载的segment
都有如下属性:
- p_offset:段在文件的偏移地址
- p_filesz:段的大小
- p_memsz :段在内存中占据的大小(通常大于p_filesz)
- p_vaddr:段的虚拟地址
- p_flags:段的标记(可读,可写,可执行)
当前,我们忽略p_paddr和p_align成员,可以加载的segments能在虚拟地址范围(p_vaddr……p_vaddr+p_memsz)以列表的形式展现,其中有如下几个规则:
- 各个segments的虚拟地址范围不可重叠
- 如果一个segments的p_filesz小于p_memsz,那么两者之间的额外数据将被初始化为0
- segment的虚拟地址范围的起始地址不是必须在某一页的边界,两个不同的segments的起始地址可以在同一页,在这种情况下,该页继承后一segment的映射标记(mapping flags)
- 每一个segment实际加载的地址并非p_vaddr,而是由加载起决定将第一个segment加载到内存中的哪个位置,然后剩下segments就以第一个segment为参照物,进行加载,比如:
下面两个是loadable segments的信息:
[ offset:0, filesz:0x4000, memsz:0x4000, vaddr:0x30000 ],
[ offset:0x4000, filesz:0x2000, memsz:0x8000, vaddr:0x40000 ],
相当于这两个segments的虚拟地址范围分别为:
0x30000...0x34000
0x40000...0x48000
如果加载器决定将第一个segment加载到0xa0000000的话(通过后面分析会知道,这个加载地址是在加载程序头部表的时候由系统定的),那么它们的实际虚拟机地址范围就是:
0xa0030000...0xa0034000
0xa0040000...0xa0048000
所有的segments 的实际加载开始地址与其vaddr的偏差值是固定的(0xa0030000 – 0x30000 = 0xa0040000 – 0x40000)
但是,在实际情况下,segments的地址并不是每一页的边界初开始的,考虑到我们只能在页面边界进行内存映射,因此,这就意味着加载地址的偏差bias应当按照如下方式进行计算:
load_bias = phdr0_load_address - PAGE_START(phdr0->p_vaddr)
(#define PAGE_START(x) ((x) & PAGE_MASK)
PAGE_MASK的值一般为0xfffff000。)
所以第一个segment的load_bias=0xa0030000 – 0x30000&0xfffff000 = 0xa00000000
这里phdr0_load_address必须以某一页的边界为起始地址,所以该segments的真正内容的开始地址为:
#!bash
phdr0_load_address + PAGE_OFFSET(phdr0->p_vaddr)
(#define PAGE_OFFSET(x) ((x) & ~PAGE_MASK) 就是x & 0xfff)
注意:ELF要求如下条件,以满足mmap正常工作:
PAGE_OFFSET(phdr0->p_vaddr) == PAGE_OFFSET(phdr0->p_offset)
每一个loadable segments的p_vaddr都必须上load_bias,其和就是该segments在内存中的实际开始地址
ReadProgramHeader
理清android加载segments的机制,来看linker中的实际代码:
#!cpp
bool ElfReader::ReadProgramHeader() {
phdr_num_ = header_.e_phnum;
……..
ElfW(Addr) page_min =

文章详细介绍了Android系统中动态链接库(so)的加载过程,包括dlopen函数、soinfo结构体、ElfReader类的使用,以及动态链接库的加载、读取、链接机制。重点分析了动态链接库如何通过dlopen加载,如何通过ElfReader读取ELF文件头和程序头表,以及动态节区的处理。最后,文章提到了执行so文件时的构造函数调用。
最低0.47元/天 解锁文章
971

被折叠的 条评论
为什么被折叠?



