android linker加载和链接机制

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

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;
}

这个函数执行流程为:

  1. 使用find_load_library函数在已经加载的动态链接库链表里面查找该动态库,如果找到了,就返回该动态库的soinfo,否则第二步
  2. 此时,说明指定的动态链接库还没有被加载,就使用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函数的执行过程可以概括如下:

  1. 使用open_library函数打开指定so文件
  2. 创建ElfReader类对象,并通过该对象的load方法,读取elf文件头,然后通过分析elf文件来加载各个segments
  3. 使用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
都有如下属性:

  1. p_offset:段在文件的偏移地址
  2. p_filesz:段的大小
  3. p_memsz :段在内存中占据的大小(通常大于p_filesz)
  4. p_vaddr:段的虚拟地址
  5. p_flags:段的标记(可读,可写,可执行)

当前,我们忽略p_paddr和p_align成员,可以加载的segments能在虚拟地址范围(p_vaddr……p_vaddr+p_memsz)以列表的形式展现,其中有如下几个规则:

  1. 各个segments的虚拟地址范围不可重叠
  2. 如果一个segments的p_filesz小于p_memsz,那么两者之间的额外数据将被初始化为0
  3. segment的虚拟地址范围的起始地址不是必须在某一页的边界,两个不同的segments的起始地址可以在同一页,在这种情况下,该页继承后一segment的映射标记(mapping flags)
  4. 每一个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 = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寻梦&之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值