android linker加载和链接机制

news2024/11/24 17:15:24

文章目录

    • So的加载和启动
    • So文件的读取与加载工作
      • ReadProgramHeader
      • ReserveAddressSpace
      • LoadSegments
      • FindPhdr
    • so 的链接机制
      • 动态节区
    • 执行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 = PAGE_START(header_.e_phoff);
  ElfW(Addr) page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(ElfW(Phdr))));
  ElfW(Addr) page_offset = PAGE_OFFSET(header_.e_phoff);

  phdr_size_ = page_max - page_min;

  void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);
  ……..
  phdr_mmap_ = mmap_result;
  phdr_table_ = reinterpret_cast<ElfW(Phdr)*>(reinterpret_cast<char*>(mmap_result) + page_offset);
  return true;
}
  1. 首先读取elf文件的程序头部表项数据phdr_num
  2. 然后分别获区程序头部表在页边界对齐后的起始地址page_min,结束地址page_max和偏移地址page_offset,并根据page_max与page_start计算出程序头部表占据的页面大小phdr_size
  3. 再以只读模式建立一个私有映射,该映射将elf文件中偏移值page_min,大小为phdr_size的区域映射到内存中,将映射后的内存地址赋给phdr_mmap_,简单一句话:将程序头部表映射到内存中,并将内存地址赋值;
  4. reinterpret_cast<new_type>(expression),这是c++中的强制类型转换符,类似于(new_type*)(expression),这里我们对上面部分代码加以解释:

首先reinterpret_cast<char*>(mmap_result):经void型指针mmap_result强制转换成char型;然后reinterpret_cast<char*>(mmap_result) + page_offset:char*型指针+page_offset,表示指向程序头部表真正开始的地方;最后再将其转换成ELFW(Phdr)*型指针,显然phdr_table_指向程序头部表开始的地址

ReserveAddressSpace

/*预备一块足够大的虚拟地址范围,用来加载所有可加载的segments.我们可以通过mmap创建一个带有PROT_NONE属性的私有匿名内存映射。PROT_NONE表示页不可访问,匿名映射表示映射区不与任何文件关联(要求fd为-1),私有映射表示对该映射区域的写入操作会产生一个映射文件的复制,对此区域做的任何修改够不会写会原来的文件*/
bool ElfReader::ReserveAddressSpace() {
  ElfW(Addr) min_vaddr;
  load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
  ……..
  uint8_t* addr = reinterpret_cast<uint8_t*>(min_vaddr);
  int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;
  void* start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);
  ……..
  load_start_ = start;
  load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;
  return true;
}

此函数就是返回ELF文件中包含的可加载segments总共需要占用的空间大小,并设置其最小虚拟地址的值(是页对齐的)。值得注意的是,原函数有4个参数,但是在ReserveAddressSpace中调用该函数时只传递了3个参数,忽略了out_max_vaddr,感觉主要是已知了out_max_vaddr及两者的差值load_size,所以可以通过out_min_vaddr+load_size来求得out_max_vaddr。

回到ReserverAddressSpace函数,求得load_size之后,就需要为这些segments分配足够的内存空间。这里需要注意的是mmap的第一个参数并非null,而是addr,这就表示将映射区间开始地址放在进程的addr地址处(一般不会成功,而是由系统自动分配,所以可以看作是null),mmap返回实际映射后的内存开始地址start,显然load_bias_=start-addr就是实际映射内存地址同linker期望的映射地址的误差值,后面的操作中,linker就可以通过p_vaddr+load_bias_来获取某一segements在内存中的开始地址了

LoadSegments

现在就开始加载ELF文件中的可加载segments:

bool ElfReader::LoadSegments() {
  for (size_t i = 0; i < phdr_num_; ++i) {
    const ElfW(Phdr)* phdr = &phdr_table_[i];

    if (phdr->p_type != PT_LOAD) {
      continue;
    }

    // Segment addresses in memory.
    ElfW(Addr) seg_start = phdr->p_vaddr + load_bias_;
    ElfW(Addr) seg_end   = seg_start + phdr->p_memsz;

    ElfW(Addr) seg_page_start = PAGE_START(seg_start);
    ElfW(Addr) seg_page_end   = PAGE_END(seg_end);

    ElfW(Addr) seg_file_end   = seg_start + phdr->p_filesz;
    // File offsets.
    ElfW(Addr) file_start = phdr->p_offset;
    ElfW(Addr) file_end   = file_start + phdr->p_filesz;

    ElfW(Addr) file_page_start = PAGE_START(file_start);
    ElfW(Addr) file_length = file_end - file_page_start;

    if (file_length != 0) {
      void* seg_addr = mmap(reinterpret_cast<void*>(seg_page_start),
                            file_length, //是以文件大小为参照,而非内存大小
                            PFLAGS_TO_PROT(phdr->p_flags),
                            MAP_FIXED|MAP_PRIVATE,
                            fd_,
                            file_page_start);
      if (seg_addr == MAP_FAILED) {
        DL_ERR("couldn't map \"%s\" segment %zd: %s", name_, i, strerror(errno));
        return false;
      }
    }

    /*如果segments可写,并且该segments的实际结束地址不在某一页的边界的话,就将该segments实际结束地址到此页的边界之间的内存全置为0*/
    if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
      memset(reinterpret_cast<void*>(seg_file_end), 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
    }

    seg_file_end = PAGE_END(seg_file_end);

    // seg_file_end is now the first page address after the file
    // content. If seg_end is larger, we need to zero anything
    // between them. This is done by using a private anonymous
    // map for all extra pages.
    if (seg_page_end > seg_file_end) {
      void* zeromap = mmap(reinterpret_cast<void*>(seg_file_end),
                           seg_page_end - seg_file_end,
                           PFLAGS_TO_PROT(phdr->p_flags),
                           MAP_FIXED|MAP_ANONYMOUS|MAP_PRIVATE,
                           -1,
                           0);
      if (zeromap == MAP_FAILED) {
        DL_ERR("couldn't zero fill \"%s\" gap: %s", name_, strerror(errno));
        return false;
      }
    }
  }
  return true;
}

此部分功能很简单,就是将ELF中的可加载segments依次映射到内存中,并进行一些辅助扫尾工作

FindPhdr

返回程序头部表在内存中的地址,这与phdr_table_是不同的,后者是一个临时的,在so被重定位之前会为释放的变量:

bool ElfReader::FindPhdr() {
  const ElfW(Phdr)* phdr_limit = phdr_table_ + phdr_num_;

  //如果段类型是 PT_PHDR, 那么我们就直接使用该段的地址.
  for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
    if (phdr->p_type == PT_PHDR) {
      return CheckPhdr(load_bias_ + phdr->p_vaddr);
    }
  }

  //否则,我们就检查第一个可加载段。如果该段的文件偏移值为0,那么就表示它是以ELF头开始的,我们就可以通过它来找到程序头表加载到内存的地址(虽然过程有点繁琐)。
  for (const ElfW(Phdr)* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
    if (phdr->p_type == PT_LOAD) {
      if (phdr->p_offset == 0) {
        ElfW(Addr)  elf_addr = load_bias_ + phdr->p_vaddr;
        const ElfW(Ehdr)* ehdr = reinterpret_cast<const ElfW(Ehdr)*>(elf_addr);
        ElfW(Addr)  offset = ehdr->e_phoff;
        return CheckPhdr((ElfW(Addr))ehdr + offset);
      }
      break;
    }
  }

  DL_ERR("can't find loaded phdr for \"%s\"", name_);
  return false;
}

要理解这段代码,我们需要知道PT_PHDR所表示的意义,指定程序头表在文件及程序内存影像中的位置和大小,此段类型不能在一个文件中多次出现, 此外,仅当程序头表时程序内存映像的一部分时,才可以出现此段,此类型(如果存在)必须位于任何可装入段的各项前面。请参见:程序的解释程序。
至此so文件的读取,加载工作就分析完毕了,我们可以发现,Android对so的加载操作只是以段为单位,跟section完全没有关系,另外,通过查看verifyElfheader的代码,我们还可以发现,android系统仅仅对ELF文件头的e_ident,e_type,e_version,e_machine进行验证(当然,e_phnum也是不能错的)所以,这就解释了为什么有些加壳so文件头的section相关字段可以任意修改,系统也不会报错了。

so 的链接机制

动态节区

如果一个目标文件参与动态链接,它的程序头部表将包含类型的PT_DYNAMIC的元素,此段包含.dynamic节区(这个节区是一个数组)。该节区采用一个特殊符号_DYNAMIC来标记,其中包含如下结构的数组:

typedef struct { 
Elf32_Sword d_tag; 
union { 
Elf32_Word d_val; 
Elf32_Addr d_ptr; 
} d_un; 
} Elf32_Dyn; 
extern Elf32_Dyn _DYNAMIC[]; //注意这里是一个数组
/*注意:
对每个这种类型的对象,d_tag控制d_un的解释含义: 
d_val 此 Elf32_Word 对象表示一个整数值,可以有多种解释。
d_ptr 此 Elf32_Addr 对象代表程序的虚拟地址。
关于d_tag的值、该值的意义,及其与d_un的关系,可查看ELF.PDF  p24。 */

该Elf32_Dyn数组就是soinfo结构体中的dynamic成员,在上面介绍的load_library函数中发现,si->dynamic被赋值为null,这就说明,在加载阶段是不需要此值的,只有在链接阶段才需要,android的动态库的链接工作是由linker完成,主要代码就是在linker.cpp的soinfo_link_image(find_library_internal方法中调用),此函数的代码相当多,我们来分块分析;

/*in function soinfo_link_image */    
    /*抽取动态节区*/
    size_t dynamic_count;
    ElfW(Word) dynamic_flags;
    /*这里的si->dynamic 为ElfW(Dyn)指针,就是上面提到的Elf32_Dyn _DYNAMIC[]*/
    phdr_table_get_dynamic_section(phdr, phnum, base, &si->dynamic,
                                   &dynamic_count, &dynamic_flags);

此函数很简单

/*返回ELF文件中的dynamic节区在内存中的地址和大小,如果没有该节区就返回null
 * Input:
 *   phdr_table  -> program header table
 *   phdr_count  -> number of entries in tables
 *   load_bias   -> load bias
 * Output:
 *   dynamic       -> address of table in memory (NULL on failure).
 *   dynamic_count -> number of items in table (0 on failure).
 *   dynamic_flags -> protection flags for section (unset on failure)
*/
void phdr_table_get_dynamic_section(const ElfW(Phdr)* phdr_table, size_t phdr_count,
                                    ElfW(Addr) load_bias,
                                    ElfW(Dyn)** dynamic, size_t* dynamic_count, ElfW(Word)* dynamic_flags) {
  const ElfW(Phdr)* phdr = phdr_table;
  const ElfW(Phdr)* phdr_limit = phdr + phdr_count;

  for (phdr = phdr_table; phdr < phdr_limit; phdr++) {
    if (phdr->p_type != PT_DYNAMIC) {
      continue;
    }

    *dynamic = reinterpret_cast<ElfW(Dyn)*>(load_bias + phdr->p_vaddr);
    if (dynamic_count) {
      *dynamic_count = (unsigned)(phdr->p_memsz / 8);
      //这里需要解释下,在2.2.1中我们介绍了Elf32_Dyn的结构,它占8字节。而PT_DYNAMIC段就是存放着Elf32_Dyn数组,所以dynamic_count的值就是该段的memsz/8。
    }
    if (dynamic_flags) {
      *dynamic_flags = phdr->p_flags; 
    }
    return;
  }
  *dynamic = NULL;
  if (dynamic_count) {
    *dynamic_count = 0;
  }
}

成功获取dynamic节区信息,我们就可以根据该节区中的Elf32_Dyn数组来进行so链接操作了,我们需要从dynamic节区中抽取有用的信息,linker采取遍历dynamic数组的方式,根据每个元素的flag()进行相应的处理:

/*in function soinfo_link_image */ 
    // 从动态dynamic节区中抽取有用信息
    uint32_t needed_count = 0;

    //开始从头遍历dyn数组,根据数组中个元素的标记进行相应的处理
    for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) { //标记为 DT_NULL 的项目标注了整个 _DYNAMIC 数组的末端,因此以它为结尾标志。 
        ........
        switch (d->d_tag) {
        case DT_HASH:
            ........
            break;
        case DT_STRTAB:
            si->strtab = reinterpret_cast<const char*>(base + d->d_un.d_ptr);
            break;
        case DT_SYMTAB:
            si->symtab = reinterpret_cast<ElfW(Sym)*>(base + d->d_un.d_ptr);
            break;
        case DT_JMPREL:
#if defined(USE_RELA)
            si->plt_rela = reinterpret_cast<ElfW(Rela)*>(base + d->d_un.d_ptr);
#else
            si->plt_rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);
#endif
            break;
        case DT_PLTRELSZ:
#if defined(USE_RELA)
            si->plt_rela_count = d->d_un.d_val / sizeof(ElfW(Rela));
#else
            si->plt_rel_count = d->d_un.d_val / sizeof(ElfW(Rel));
#endif
            break;
#if defined(__mips__)
        case DT_PLTGOT:
            // Used by mips and mips64.
            si->plt_got = reinterpret_cast<ElfW(Addr)**>(base + d->d_un.d_ptr);
            break;
#endif
         ........
#if defined(USE_RELA)
         case DT_RELA:
            si->rela = reinterpret_cast<ElfW(Rela)*>(base + d->d_un.d_ptr);
            break;
         case DT_RELASZ:
            si->rela_count = d->d_un.d_val / sizeof(ElfW(Rela));
            break;
        case DT_REL:
            DL_ERR("unsupported DT_REL in \"%s\"", si->name);
            return false;
        case DT_RELSZ:
            DL_ERR("unsupported DT_RELSZ in \"%s\"", si->name);
            return false;
#else
        case DT_REL:
            si->rel = reinterpret_cast<ElfW(Rel)*>(base + d->d_un.d_ptr);
            break;
        case DT_RELSZ:
            si->rel_count = d->d_un.d_val / sizeof(ElfW(Rel));
            break;
         case DT_RELA:
            DL_ERR("unsupported DT_RELA in \"%s\"", si->name);
            return false;
#endif
        case DT_INIT: //只有可执行文件才有此节区
            si->init_func = reinterpret_cast<linker_function_t>(base + d->d_un.d_ptr);
            DEBUG("%s constructors (DT_INIT) found at %p", si->name, si->init_func);
            break;
        case DT_FINI:
            si->fini_func = reinterpret_cast<linker_function_t>(base + d->d_un.d_ptr);
            DEBUG("%s destructors (DT_FINI) found at %p", si->name, si->fini_func);
            break;
        case DT_INIT_ARRAY:
            si->init_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
            DEBUG("%s constructors (DT_INIT_ARRAY) found at %p", si->name, si->init_array);
            break;
        case DT_INIT_ARRAYSZ:
            si->init_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr));
            break;
        case DT_FINI_ARRAY:
            si->fini_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
            DEBUG("%s destructors (DT_FINI_ARRAY) found at %p", si->name, si->fini_array);
            break;
        case DT_FINI_ARRAYSZ: 
            si->fini_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr));
            break;
        case DT_PREINIT_ARRAY:
            si->preinit_array = reinterpret_cast<linker_function_t*>(base + d->d_un.d_ptr);
            DEBUG("%s constructors (DT_PREINIT_ARRAY) found at %p", si->name, si->preinit_array);
            break;
        case DT_PREINIT_ARRAYSZ:
            si->preinit_array_count = ((unsigned)d->d_un.d_val) / sizeof(ElfW(Addr));
            break;
        case DT_TEXTREL:
#if defined(__LP64__)
            DL_ERR("text relocations (DT_TEXTREL) found in 64-bit ELF file \"%s\"", si->name);
            return false;
#else
            si->has_text_relocations = true;
            break;
#endif
        case DT_SYMBOLIC:
            si->has_DT_SYMBOLIC = true;
            break;
        case DT_NEEDED:
            ++needed_count;
            break;
        case DT_FLAGS:
            if (d->d_un.d_val & DF_TEXTREL) {
                ........
                si->has_text_relocations = true;
            }
            if (d->d_un.d_val & DF_SYMBOLIC) {
                si->has_DT_SYMBOLIC = true;
            }
            break;
#if defined(__mips__)
        case DT_STRSZ:
        case DT_SYMENT:
        case DT_RELENT:
             break;
        case DT_MIPS_RLD_MAP:
            // Set the DT_MIPS_RLD_MAP entry to the address of _r_debug for GDB.
            {
              r_debug** dp = reinterpret_cast<r_debug**>(base + d->d_un.d_ptr);
              *dp = &_r_debug;
            }
            break;
        case DT_MIPS_RLD_VERSION:
        case DT_MIPS_FLAGS:
        case DT_MIPS_BASE_ADDRESS:
        case DT_MIPS_UNREFEXTNO:
            break;

        case DT_MIPS_SYMTABNO:
            si->mips_symtabno = d->d_un.d_val;
            break;

        case DT_MIPS_LOCAL_GOTNO:
            si->mips_local_gotno = d->d_un.d_val;
            break;

        case DT_MIPS_GOTSYM:
            si->mips_gotsym = d->d_un.d_val;
            break;
#endif

        default:
            DEBUG("Unused DT entry: type %p arg %p",
                  reinterpret_cast<void*>(d->d_tag), reinterpret_cast<void*>(d->d_un.d_val));
            break;
        }
    }

完成dynamic数组的遍历后,就说明我们已经获取了其中的有用信息了,那么现在就需要根据这些信息进行处理:

/*in function soinfo_link_image */ 

    //再检测一遍,这种做法总是明智的
    if (relocating_linker && needed_count != 0) {
        DL_ERR("linker cannot have DT_NEEDED dependencies on other libraries");
        return false;
    }
    if (si->nbucket == 0) {
        DL_ERR("empty/missing DT_HASH in \"%s\" (built with --hash-style=gnu?)", si->name);
        return false;
    }
    if (si->strtab == 0) {
        DL_ERR("empty/missing DT_STRTAB in \"%s\"", si->name);
        return false;
    }
    if (si->symtab == 0) {
        DL_ERR("empty/missing DT_SYMTAB in \"%s\"", si->name);
        return false;
    }

    // If this is the main executable, then load all of the libraries from LD_PRELOAD now.
    //如果是main可执行文件,那么就根据LD_PRELOAD信息来加载所有相关的库
    //这里面涉及到的gLdPreloadNames变量,我们知道在前面的整个分析过程中均没有涉及,这是因为,对于可执行文件而言,它的起始函数并不是dlopen,而是系统内核的execv函数,通过层层调用之后才会执行到linker的linker_init_post_ralocation函数,在这个函数中调用parse_LD_PRELOAD函数完成 gLdPreloadNames变量的赋值
    if (si->flags & FLAG_EXE) {
        memset(gLdPreloads, 0, sizeof(gLdPreloads));
        size_t preload_count = 0;
        for (size_t i = 0; gLdPreloadNames[i] != NULL; i++) {
            soinfo* lsi = find_library(gLdPreloadNames[i]);
            if (lsi != NULL) {
                gLdPreloads[preload_count++] = lsi;
            } else {
                ........
            }
        }
    }

    //分配一个soinfo*[]指针数组,用于存放本so库需要的外部so库的soinfo指针
    soinfo** needed = reinterpret_cast<soinfo**>(alloca((1 + needed_count) * sizeof(soinfo*)));
    soinfo** pneeded = needed;
    //依次获取dynamic数组中定义的每一个外部so库soinfo
    for (ElfW(Dyn)* d = si->dynamic; d->d_tag != DT_NULL; ++d) {
        if (d->d_tag == DT_NEEDED) {
            const char* library_name = si->strtab + d->d_un.d_val; //根据index值获取所需库的名字
            DEBUG("%s needs %s", si->name, library_name);
            soinfo* lsi = find_library(library_name);  //获取该库的soinfo
            if (lsi == NULL) {
                ........
            }
            *pneeded++ = lsi;
        }
    }
    *pneeded = NULL; 

#if !defined(__LP64__)
    if (si->has_text_relocations) {
        // Make segments writable to allow text relocations to work properly. We will later call
        // phdr_table_protect_segments() after all of them are applied and all constructors are run.
        DL_WARN("%s has text relocations. This is wasting memory and prevents "
                "security hardening. Please fix.", si->name);
        if (phdr_table_unprotect_segments(si->phdr, si->phnum, si->load_bias) < 0) {
            DL_ERR("can't unprotect loadable segments for \"%s\": %s",
                   si->name, strerror(errno));
            return false;
        }
    }
#endif

#if defined(USE_RELA)
    if (si->plt_rela != NULL) {
        DEBUG("[ relocating %s plt ]\n", si->name);
        if (soinfo_relocate(si, si->plt_rela, si->plt_rela_count, needed)) {
            return false;
        }
    }
    if (si->rela != NULL) {
        DEBUG("[ relocating %s ]\n", si->name);
        if (soinfo_relocate(si, si->rela, si->rela_count, needed)) {
            return false;
        }
    }
#else
    if (si->plt_rel != NULL) {
        DEBUG("[ relocating %s plt ]", si->name);
        if (soinfo_relocate(si, si->plt_rel, si->plt_rel_count, needed)) {
            return false;
        }
    }
    if (si->rel != NULL) {
        DEBUG("[ relocating %s ]", si->name);
        if (soinfo_relocate(si, si->rel, si->rel_count, needed)) {
            return false;
        }
    }
#endif

#if defined(__mips__)
    if (!mips_relocate_got(si, needed)) {
        return false;
    }
#endif

    si->flags |= FLAG_LINKED;
    DEBUG("[ finished linking %s ]", si->name);

#if !defined(__LP64__)
    if (si->has_text_relocations) {
        // All relocations are done, we can protect our segments back to read-only.
        if (phdr_table_protect_segments(si->phdr, si->phnum, si->load_bias) < 0) {
            DL_ERR("can't protect segments for \"%s\": %s",
                   si->name, strerror(errno));
            return false;
        }
    }
#endif

    /* We can also turn on GNU RELRO protection */
    if (phdr_table_protect_gnu_relro(si->phdr, si->phnum, si->load_bias) < 0) {
        DL_ERR("can't enable GNU RELRO protection for \"%s\": %s",
               si->name, strerror(errno));
        return false;
    }

    notify_gdb_of_load(si);
    return true;
}

执行so文件

上面的find_library_internal函数中的soinfo_link_image函数执行后就返回到上层函数find_library中,然后进一步返回到do_dlopen函数:

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

如果获取的si不为空,就说明so的加载和链接操作正确完成,那么就可以执行so的初始化构造函数了:

void soinfo::CallConstructors() {
  ........
  // DT_INIT should be called before DT_INIT_ARRAY if both are present.
  //如果文件含有.init和.init_array节区的话,就先执行.init节区的代码再执行.init_array节区的代码
  CallFunction("DT_INIT", init_func);  
  CallArray("DT_INIT_ARRAY", init_array, init_array_count, false);
}

由于我们只分析so库,所以只需要关心CallArray(“DT_INIT_ARRAY”, init_array, init_array_count, false)函数即可:

void soinfo::CallArray(const char* array_name UNUSED, linker_function_t* functions, size_t count, bool reverse) {
  ........
  //这里的recerse变量用于指定.init_array中的函数是由前到后执行还是由后到前执行。默认是由前到后
  int begin = reverse ? (count - 1) : 0;
  int end = reverse ? -1 : count;
  int step = reverse ? -1 : 1;

  for (int i = begin; i != end; i += step) {
    TRACE("[ %s[%d] == %p ]", array_name, i, functions[i]);
    CallFunction("function", functions[i]); //依次调用init_array中的函数。
  }
 ........
}

这里需要对init_array节区的结构和作用加以说明
首先是init_array节区的数据结构。该节中包含指针,这些指针指向了一些初始化代码。这些初始化代码一般是在main函数之前执行的。在C++程序中,这些代码用来运行静态构造函数。另外一个用途就是有时候用来初始化C库中的一些IO系统。使用IDA查看具有init_array节区的so库文件就可以找到如下数据:在这里插入图片描述
这里共三个函数指针,每个指针指向一个函数地址。值得注意的是,上图中每个函数指针的值都加了1,这是因为地址的最后1位置1表明需要使得处理器由ARM转为Thumb状态来处理Thumb指令。将目标地址处的代码解释为Thumb代码来执行。

然后再来看CallFunction的具体实现:

void soinfo::CallFunction(const char* function_name UNUSED, linker_function_t function) {
  //如果函数地址为空或者为-1就直接退出。
  if (function == NULL || reinterpret_cast<uintptr_t>(function) == static_cast<uintptr_t>(-1)) {
    return;
  }
  ........
  function(); //执行该指针所指定的函数
  // The function may have called dlopen(3) or dlclose(3), so we need to ensure our data structures
  // are still writable. This happens with our debug malloc (see http://b/7941716).
  set_soinfo_pool_protection(PROT_READ | PROT_WRITE);
}

至此,整个Android so的linker机制就分析完毕了!

原文地址:

https://wooyun.js.org/drops/Android%20Linker%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0.html

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/752411.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

SpringAMQP - 消息传输时,如何提高性能?解决 SQL 注入问题?

目录 一、问题背景 二、从消息转化器根源解决问题 1.引入依赖 2.在服务生产者和消费者中都重新定义一个 MessageConverter&#xff0c;注入到 Spring 容器中 一、问题背景 在SpringAMQP的发送方法中&#xff0c;接收消息的类型是Object&#xff0c;也就是说我们可以发送任意…

【程序人生】如何在工作中保持稳定的情绪?

前言 在工作中保持稳定的情绪是现代生活中一个备受关注的话题。随着职场压力和工作挑战的增加&#xff0c;我们常常发现自己情绪波动不定&#xff0c;甚至受到负面情绪的困扰。然而&#xff0c;保持稳定的情绪对于我们的工作效率、人际关系和整体幸福感都至关重要。 无论你是…

GPT-4揭秘:从科学突破到宇宙探索,大模型如何为人类谋福祉?

最近一段时间&#xff0c;人工智能领域似乎在上演一场密切相关的三幕戏。从OpenAI的GPT-4&#xff0c;到LeanDojo的开源平台&#xff0c;再到Elon Musk的xAI&#xff0c;人工智能的最新发展进程仿佛正在向我们揭示未来的模样。让我们深入探讨一下这些最新的科技发展。 GPT-4&a…

个人信息保护影响评估,推动个人信息保护“关口前移”

2021 年 11 月 1 日&#xff0c;《个人信息保护法》&#xff08;以下简称《个保法》&#xff09;的正式施行&#xff0c;可以说在我国个人信息保护法治建设具有里程碑意义。《个保法》内容具备系统性、针对性和可操作性特点&#xff0c;规范了个人信息处理活动&#xff0c;明确…

ubuntu安装软件包提示【未安装软件包 deepin-elf-verify】【已解决】

文章目录 背景原因分析步骤1 解压文件2 删除依赖3 重新打包软件 转载请标明出处&#xff1a; https://bigmaning.blog.csdn.net/article/details/131713280 本文出自:【BigManing的博客】 背景 在ubuntu系统上安装一个deb文件&#xff0c;执行命令后&#xff0c;报错如下 sud…

【C++】STL之string功能及模拟实现

目录 前沿 一、标准库中的string类 二、string类的常用接口说明 1、string类对象的常见构造 2、string类对象的容量操作 3、string类对象的访问及遍历操作 4、string类对象的修改操作 5、string类非成员函数 6、vs下string结构的说明 三、string类的模拟实现 1、构造函数 2…

MySQL(备份还原索引视图入门)

文章目录 第一节 备份和还原1、题目2、题目作答 第二节 索引1.题目2.题目作答 第三节 视图1 题目2 题目作答 第一节 备份和还原 1、题目 CREATE DATABASE beifen;use beifen;CREATE TABLE books(bk_id INT NOT NULL PRIMARY KEY,bk_title VARCHAR(50) NOT NULL,copyright YEA…

django框架向DRF框架演变过程详解

一、Django框架实现项目查询接口 主要知识点&#xff1a; Django框架视图函数 1、在 Django 项目中创建一个应用&#xff08;如果还没有创建&#xff09;&#xff1a; python manage.py startapp projects 2、在项目的 models.py 文件中定义项目模型 from django.db impor…

视频融合平台EasyCVR登录后通道数据及菜单栏页面显示异常的排查与解决

EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。 有用…

只需三步实现Gateway结合Sentinel实现无侵入网关限流,注意避坑!

前言&#xff1a;本文基于您已有基础的可运行的微服务系统&#xff0c;使用了Sping Cloud Alibaba&#xff0c;Gateway,Nacos等&#xff1b;目标实现网关流控类型的限流。 顾名思义限流用于在高并发场景下限制请求流量的进入&#xff0c;保护系统不被冲垮。阿里巴巴的开源senti…

数据库慢查询优化

数据库慢查询优化 1.分析慢查询原因 分析导致慢查询的原因是数据库性能优化的关键步骤之一。下面是一些常见的方法和工具&#xff0c;可以帮助你确定慢查询的原因&#xff1a; 慢查询日志&#xff1a; 开启慢查询日志&#xff0c;允许数据库记录执行时间超过阈值的查询语句。…

百度ERNIE 3.0——中文情感分析实战

目录 前言一、百度ERNIE 3.0二、使用ERNIE 3.0中文预训练模型进行句子级别的情感分析2-1、环境2-2、数据集加载2-3、加载预训练模型和分词器2-4、基于预训练模型的数据处理2-5、数据训练和评估2-6、模型验证2-7、情感分析结果的预测以及保存 三、自定义个人案例3-1、如何自定义…

数据库语句

文章目录 数据库语句SQL语言分类MySQL中6种常见的约束1.DDL1.1 创建新的数据库1.2 创建新的表1.3 删除指定的数据表 2.DML管理表2.1 插入数据2.2 修改&#xff08;更新数据&#xff09;2.3 在数据表中删除指定的数据 3.DQL查询数据记录4.DCL4.1 修改表名和表结构4.2 扩展表结构…

Node.js详解(四):连接MongoDB

文章目录 一、安装MongoDB访问驱动二、连接数据库三、添加数据四、添加多条数据五、修改数据六、查询数据1、查询单条记录2、查询多条记录 七、删除数据八、完整示例代码1、路由 Api 接口&#xff1a;2、运行结果&#xff1a; MongoDB 对许多平台都提供驱动可以访问数据库&…

前端vue入门(纯代码)31_route-link的repalce属性

如果夜里十二点我还回你消息&#xff0c;那么意味着什么&#xff0c;意味着我是真的很喜欢玩手机。 【29.Vue Router--router-link的replace属性】 <router-link>的replace属性 replace属性的作用是&#xff1a;控制路由跳转时操作浏览器历史记录的模式。【当我们从一个…

城市内涝监测设备-内涝监测终端

随着我国城市化发展迅速、全球极端天气现象频发带来的暴雨天气增多&#xff0c;汛期暴雨引发道路低洼处、立交桥底、隧道、涵洞等城市 内涝时有发生&#xff0c;甚至开启城市看海模式&#xff0c;对交通、电力、通讯等造成了严重的影响和破坏&#xff0c;严重时造成人民生命、财…

放弃使用Merge,开心拥抱Rebase!

1. 引言 大家好&#xff0c;我是比特桃。Git 作为现在最流行的版本管理工具&#xff0c;想必大家在开发过程中都会使用。由于 Git 中很多操作默认是采用 Merge 进行的&#xff0c;并且相对也不容易出错&#xff0c;所以很多人都会使用 Merge 来进行合并代码。但Rebase 作为 Gi…

官宣!菁英实习生计划启动,百度大模型团队诚邀你的加入

大模型风起&#xff0c;人才需求涌 在这个充满变革的时代&#xff0c;我们见证了AI的快速发展。从“阿尔法狗”击败世界围棋冠军&#xff0c;到生成式大模型以势不可挡的浪潮席卷全球&#xff0c;掀起人类社会一场眩晕式变革。新技术、新工具、新的生产力正在改变经济活动各环…

小红书运营推广

大家好&#xff0c;我是权知星球&#xff0c;今天给大家分享一下小红手运营推广的一些经验&#xff0c;希望能给大家运营小红书带来一些帮助。 这篇文章虽然是基于小红书的运营写的&#xff0c;但新媒体的东西都是相通的&#xff0c;相信这篇文章对运营其他媒体的同学也会有所…

抓包工具Fiddler:fiddler的介绍及安装

Fiddler简介 Fiddler是比较好用的web代理调试工具之一&#xff0c;它能记录并检查所有客户端与服务端的HTTP/HTTPS请求&#xff0c;能够设置断点&#xff0c;篡改及伪造Request/Response的数据&#xff0c;修改hosts&#xff0c;限制网速&#xff0c;http请求性能统计&#xff…