Android Linker詳解


Android Linker詳解

本文目的

Unidbg在對So進行模擬執行的時候,需要先將So文件加載到內存,配置So的進程映像,然后使用CPU模擬器(Unicorn、Dynamic等)對So進行模擬執行。本文的目的是為了徹底搞懂So文件是如何加載到內存的,以及加載進內存之后做了什么,史無巨細,握住方向盤

Linker入口

我們在Android程序中,往往會使用到JNI編程來加快某些算法的運行或增加APP的逆向難度。當然這不是Android的新特性,它是Java自帶的本地編程接口,可以使我們的Java程序能夠調用本地語言。

當我們在Android程序想使用本地編譯的So庫,第一步就是要將So加載進來對吧,Android Studio創建C/C++ Native模板的時候,它會在我們的MainActivity類中加這么一段代碼

static{
    System.loadLibrary("native-lib");
}

這句代碼的作用就是將So加載進來供Android程序來使用,所以以此為入口,開始分析

http://androidxref.com/4.4.4_r1/xref/libcore/luni/src/main/java/java/lang/System.java#525

public static void loadLibrary(String libName) {
    Runtime.getRuntime().loadLibrary(libName, VMStack.getCallingClassLoader());
}

又調用了Runtime類的loadLibrary,第二個參數為調用類的ClassLoader

http://androidxref.com/4.4.4_r1/xref/libcore/luni/src/main/java/java/lang/Runtime.java#354

void loadLibrary(String libraryName, ClassLoader loader) {
    if (loader != null) {
        //調用findLibrary通過名稱("native-lib")尋找真實庫文件名
        String filename = loader.findLibrary(libraryName);
        if (filename == null) {
            throw new UnsatisfiedLinkError("...");
        }
        //如果找到了,進行加載
        String error = doLoad(filename, loader);
        if (error != null) {
            throw new UnsatisfiedLinkError(error);
        }
        return;
    }

    String filename = System.mapLibraryName(libraryName);
    List<String> candidates = new ArrayList<String>();
    String lastError = null;
    for (String directory : mLibPaths) {
        String candidate = directory + filename;
        candidates.add(candidate);

        if (IoUtils.canOpenReadOnly(candidate)) {
            //這里是還有其他的路徑來搜索我們的庫文件名,都是調用doLoad方法
            String error = doLoad(candidate, loader);
            if (error == null) {
                return; // We successfully loaded the library. Job done.
            }
            lastError = error;
        }
    }

    if (lastError != null) {
        throw new UnsatisfiedLinkError(lastError);
    }
    throw new UnsatisfiedLinkError("Library " + libraryName + " not found; tried " + candidates);
}

接着看doLoad方法

http://androidxref.com/4.4.4_r1/xref/libcore/luni/src/main/java/java/lang/Runtime.java#393

private String doLoad(String name, ClassLoader loader) {
    String ldLibraryPath = null;
    if (loader != null && loader instanceof BaseDexClassLoader) {
        //如果是BaseDexClassLoader,獲取系統so的路徑
        ldLibraryPath = ((BaseDexClassLoader) loader).getLdLibraryPath();
    }
    synchronized (this) {
        //繼續調用nativeLoad,還加了同步鎖
        return nativeLoad(name, loader, ldLibraryPath);
    }
}

http://androidxref.com/4.4.4_r1/xref/libcore/luni/src/main/java/java/lang/Runtime.java#426

private static native String nativeLoad(String filename, ClassLoader loader, String ldLibraryPath);

繼續往下分析,找到nativeLoad對應的C層函數

http://androidxref.com/4.4.4_r1/xref/art/runtime/native/java_lang_Runtime.cc#43

static jstring Runtime_nativeLoad(JNIEnv* env, jclass, jstring javaFilename, jobject javaLoader, jstring javaLdLibraryPath) {
  //...各種檢查
  mirror::ClassLoader* classLoader = soa.Decode<mirror::ClassLoader*>(javaLoader);
  std::string detail;
  JavaVMExt* vm = Runtime::Current()->GetJavaVM();
  bool success = vm->LoadNativeLibrary(filename.c_str(), classLoader, detail);
  if (success) {
    return NULL;
  }

  // Don't let a pending exception from JNI_OnLoad cause a CheckJNI issue with NewStringUTF.
  env->ExceptionClear();
  return env->NewStringUTF(detail.c_str());
}

最關鍵的函數是vm->LoadNativeLibrary,繼續往下跟

http://androidxref.com/4.4.4_r1/xref/art/runtime/jni_internal.cc#3120

bool JavaVMExt::LoadNativeLibrary(const std::string& path, ClassLoader* class_loader,
                                  std::string& detail) {
    //...
    self->TransitionFromRunnableToSuspended(kWaitingForJniOnLoad);
    //調用dlopen加載So,並返回一個handle句柄
    void* handle = dlopen(path.empty() ? NULL : path.c_str(), RTLD_LAZY);
    self->TransitionFromSuspendedToRunnable();

    VLOG(jni) << "[Call to dlopen(\"" << path << "\", RTLD_LAZY) returned " << handle << "]";
    //...
    //此時如果So加載正常,會調用dlsym查找JNI_OnLoad符號,並執行
    void* sym = dlsym(handle, "JNI_OnLoad");
    if (sym == NULL) {
        VLOG(jni) << "[No JNI_OnLoad found in \"" << path << "\"]";
        was_successful = true;
    } else {
    }
    typedef int (*JNI_OnLoadFn)(JavaVM*, void*);
    JNI_OnLoadFn jni_on_load = reinterpret_cast<JNI_OnLoadFn>(sym);
    ClassLoader* old_class_loader = self->GetClassLoaderOverride();
    self->SetClassLoaderOverride(class_loader);
    int version = 0;
    {
        ScopedThreadStateChange tsc(self, kNative);
        VLOG(jni) << "[Calling JNI_OnLoad in \"" << path << "\"]";
        //在這里調用JNIOnload
        version = (*jni_on_load)(this, NULL);
    }
    //...
    library->SetResult(was_successful);
    return was_successful;
}

我們分析上面的函數知道,此函數主要做了兩件事

  • 調用dlopen加載So
  • 查找So中的JNI_OnLoad函數,並執行
    繼續往下分析dlopen

http://androidxref.com/4.4.4_r1/xref/bionic/linker/dlfcn.cpp#63

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

So的裝載

http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#823

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

分析到這里,終於進入Linker部分了,上面的篇幅我們由System.loadLibrary()方法,找到了Linker的do_dlopen函數,這個函數就可以說是Linker開始加載的地方了。這個函數主要做了兩件事

  • 調用函數find_library,返回soinfo。soinfo就是so被加載到內存的一個代表,存放了內存中so的信息
  • 調用soinfo的CallConstructors函數,做了一些初始化操作(Iint、init.array)

繼續分析find_library

http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#785

static soinfo* find_library(const char* name) {
  soinfo* si = find_library_internal(name);
  if (si != NULL) {
    si->ref_count++;
  }
  return si;
}

這個函數的作用很簡單

  • 調用find_library_internal
  • so的引用計數+1

繼續分析find_library_internal函數

http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#751

static soinfo* find_library_internal(const char* name) {
  if (name == NULL) {
    return somain;
  }
  //尋找已經加載過的so,當我們的so被加載完成后,會放到已加載列表,再次調用System.loadLibrary的時候不需要進行二次加載
  soinfo* si = find_loaded_library(name);
  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);
  //如果沒有被加載過,就調用load_library進行加載
  si = load_library(name);
  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("[ init_library base=0x%08x sz=0x%08x name='%s' ]",
        si->base, si->size, si->name);
  // so被加載后,進行鏈接
  if (!soinfo_link_image(si)) {
    munmap(reinterpret_cast<void*>(si->base), si->size);
    soinfo_free(si);
    return NULL;
  }

  return si;
}

這個函數主要做了3個事情:

  • 判斷想要加載的so是否已經被加載過
  • 如果沒有被加載過,調用load_library進行加載
  • 加載完成后,調用soinfo_link_image函數進行鏈接
    也就體現了我們So裝載的主要兩個步驟
  • So的裝載
  • So的鏈接
    在上面我們還有一個調用soinfo的CallConstructors函數,這個也可以作為第三個
  • So的初始化

那么我們假設我們的So是第一次進行加載,繼續分析load_library函數,看看linker如何裝載我們的So

http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker.cpp#702

static soinfo* load_library(const char* name) {
    // 打開So文件,拿到文件描述符fd
    int fd = open_library(name);
    if (fd == -1) {
        DL_ERR("library \"%s\" not found", name);
        return NULL;
    }

    //創建ElfReader對象,並調用Load方法
    ElfReader elf_reader(name, fd);
    if (!elf_reader.Load()) {
        return NULL;
    }
    //生成soinfo,並根據elf_reader的結果進行賦值
    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;
    si->dynamic = NULL;
    si->phnum = elf_reader.phdr_count();
    si->phdr = elf_reader.loaded_phdr();
    return si;
}

那么我們主要來分析elf_reader.Load()函數

http://androidxref.com/4.4.4_r1/xref/bionic/linker/linker_phdr.cpp#134

bool ElfReader::Load() {
  return ReadElfHeader() &&
         VerifyElfHeader() &&
         ReadProgramHeader() &&
         ReserveAddressSpace() &&
         LoadSegments() &&
         FindPhdr();
}

Load函數分別調用了6個函數

  • ReadElfHeader 讀取ElfHeader
  • VerifyElfHeader 驗證ElfHeader
  • ReadProgramHeader 讀取程序頭表
  • ReserveAddressSpace 准備地址空間
  • LoadSegments 加載段
  • FindPhdr 尋找Phdr段

從函數名直譯,我們也能知道一個大概。下面我們來分析這6個函數

bool ElfReader::ReadElfHeader() {
  //從我們打開的So文件中,讀取header長度的內容賦值到header_
  ssize_t rc = TEMP_FAILURE_RETRY(read(fd_, &header_, sizeof(header_)));
  if (rc < 0) {
    DL_ERR("can't read file \"%s\": %s", name_, strerror(errno));
    return false;
  }
  if (rc != sizeof(header_)) {
    DL_ERR("\"%s\" is too small to be an ELF executable", name_);
    return false;
  }
  return true;
}
bool ElfReader::VerifyElfHeader() {
  //校驗魔數
  if (header_.e_ident[EI_MAG0] != ELFMAG0 ||
      header_.e_ident[EI_MAG1] != ELFMAG1 ||
      header_.e_ident[EI_MAG2] != ELFMAG2 ||
      header_.e_ident[EI_MAG3] != ELFMAG3) {
    DL_ERR("\"%s\" has bad ELF magic", name_);
    return false;
  }
  //因為分析的是Android4.4源碼,所以它必須是一個32位的So
  if (header_.e_ident[EI_CLASS] != ELFCLASS32) {
    DL_ERR("\"%s\" not 32-bit: %d", name_, header_.e_ident[EI_CLASS]);
    return false;
  }
  //必須為小端字節序
  if (header_.e_ident[EI_DATA] != ELFDATA2LSB) {
    DL_ERR("\"%s\" not little-endian: %d", name_, header_.e_ident[EI_DATA]);
    return false;
  }
  //必須為ET_DYN,也就是我們的Shared Object So文件
  if (header_.e_type != ET_DYN) {
    DL_ERR("\"%s\" has unexpected e_type: %d", name_, header_.e_type);
    return false;
  }
  // version當前版本,這個一般都是EV_CURRENT(1)
  if (header_.e_version != EV_CURRENT) {
    DL_ERR("\"%s\" has unexpected e_version: %d", name_, header_.e_version);
    return false;
  }
  // 校驗e_machine
  if (header_.e_machine !=
#ifdef ANDROID_ARM_LINKER
      EM_ARM
#elif defined(ANDROID_MIPS_LINKER)
      EM_MIPS
#elif defined(ANDROID_X86_LINKER)
      EM_386
#endif
  ) {
    DL_ERR("\"%s\" has unexpected e_machine: %d", name_, header_.e_machine);
    return false;
  }
  return true;
}
bool ElfReader::ReadProgramHeader() {
  //e_phnum 為我們So的程序頭表(段)數,后面我們都叫段,是一個意思
  phdr_num_ = header_.e_phnum;

  // Like the kernel, we only accept program header tables that
  // are smaller than 64KiB.
  if (phdr_num_ < 1 || phdr_num_ > 65536/sizeof(Elf32_Phdr)) {
    DL_ERR("\"%s\" has invalid e_phnum: %d", name_, phdr_num_);
    return false;
  }
  //e_phoff 為我們段表在文件中的偏移, 然后進行內存頁對其
  Elf32_Addr page_min = PAGE_START(header_.e_phoff);
  Elf32_Addr page_max = PAGE_END(header_.e_phoff + (phdr_num_ * sizeof(Elf32_Phdr)));
  Elf32_Addr page_offset = PAGE_OFFSET(header_.e_phoff);
  // 在Linux中內存讀寫都是以頁為單位,所以上面按照存放段的位置,算出了需要的頁大小
  // page_offset就是段在該頁的一個偏移

  phdr_size_ = page_max - page_min;
  //將該包含段表的頁映射到內存
  void* mmap_result = mmap(NULL, phdr_size_, PROT_READ, MAP_PRIVATE, fd_, page_min);
  if (mmap_result == MAP_FAILED) {
    DL_ERR("\"%s\" phdr mmap failed: %s", name_, strerror(errno));
    return false;
  }

  phdr_mmap_ = mmap_result;
  // phdr_table_ 就指向了段表在內存中的起始位置
  phdr_table_ = reinterpret_cast<Elf32_Phdr*>(reinterpret_cast<char*>(mmap_result) + page_offset);
  return true;
}
bool ElfReader::ReserveAddressSpace() {
  //此時段表已經被加載到內存
  Elf32_Addr min_vaddr;
  //先獲取該So的load_size,也就是需要加載的大小,先看下面對該函數的解釋
  load_size_ = phdr_table_get_load_size(phdr_table_, phdr_num_, &min_vaddr);
  if (load_size_ == 0) {
    DL_ERR("\"%s\" has no loadable segments", name_);
    return false;
  }

  uint8_t* addr = reinterpret_cast<uint8_t*>(min_vaddr);
  int mmap_flags = MAP_PRIVATE | MAP_ANONYMOUS;
  //根據PT_LOAD段的指示,匿名映射一塊足夠裝下我們So的內存
  void* start = mmap(addr, load_size_, PROT_NONE, mmap_flags, -1, 0);
  if (start == MAP_FAILED) {
    DL_ERR("couldn't reserve %d bytes of address space for \"%s\"", load_size_, name_);
    return false;
  }
  load_start_ = start;
  //這里需要注意一下,start為我們映射出來的那塊內存的起始地址,按理說它就是我們So加載的一個起始地址
  //那這里又計算了一個load_bias_是什么意思呢?
  //So文件並沒有對p_vaddr有特殊要求,所以它可以是任意地址,如果它指定了一個最小的虛擬地址不為0
  //那么文件中的關於地址的引用就是根據它指定的虛擬地址來的
  //所以我們在后面進行對地址修正的時候,就要計算 start - min_addr來得到正確的值
  //所以這里計算了load_bias_, 后面關於地址引用的地方,我們都用這個load_bias_就可以了
  //舉個例子:假設一個So中的PT_LOAD段指定的最小虛擬地址min_vaddr = 0x100
  //那么如果這個So中的一個函數中引用了一個地址為0x300地方的字符串
  //那這個字符串在實際文件中的偏移就是0x200 = 0x300 - 0x100
  //當So加載到內存中,需要對這個函數中的引用做重定位的時候,就應該這樣計算
  //start + 0x300 - 0x100 <==> start - 0x100  + 0x300 
  //每次在計算的時候都要-0x100,所以這里就計算了一個load_bias_ = start - 0x100
  //后面直接用這個load_bias_ + 0x300(地址引用偏移) 就可以了
  load_bias_ = reinterpret_cast<uint8_t*>(start) - addr;
  return true;
}
size_t phdr_table_get_load_size(const Elf32_Phdr* phdr_table,
                                size_t phdr_count,
                                Elf32_Addr* out_min_vaddr,
                                Elf32_Addr* out_max_vaddr)
{
    Elf32_Addr min_vaddr = 0xFFFFFFFFU;
    Elf32_Addr max_vaddr = 0x00000000U;

    bool found_pt_load = false;
    //遍歷我們的段表
    for (size_t i = 0; i < phdr_count; ++i) {
        const Elf32_Phdr* phdr = &phdr_table[i];
        //只處理PT_LOAD段,因為PT_LOAD段說明了我們的So應該怎么加載
        if (phdr->p_type != PT_LOAD) {
            continue;
        }
        found_pt_load = true;
        //遍歷所有的PT_LOAD段,尋找So指定的最小的一個虛擬地址
        if (phdr->p_vaddr < min_vaddr) {
            min_vaddr = phdr->p_vaddr;
        }
        //遍歷所有的PT_LOAD段,尋找So指定的要加載到內存的最大的一個虛擬地址
        if (phdr->p_vaddr + phdr->p_memsz > max_vaddr) {
            max_vaddr = phdr->p_vaddr + phdr->p_memsz;
        }
    }
    if (!found_pt_load) {
        min_vaddr = 0x00000000U;
    }
    //So文件並沒有對p_vaddr有特殊要求,所以這里需要頁對齊
    min_vaddr = PAGE_START(min_vaddr);
    max_vaddr = PAGE_END(max_vaddr);

    if (out_min_vaddr != NULL) {
        *out_min_vaddr = min_vaddr;
    }
    if (out_max_vaddr != NULL) {
        *out_max_vaddr = max_vaddr;
    }
    //最大-最小拿到該So加載到內存的一個size
    return max_vaddr - min_vaddr;
}

//上面ReserveAddressSpace函數,只是開辟了一塊足夠的內存,並沒有內容
//這個函數就在填充內容
bool ElfReader::LoadSegments() {
  for (size_t i = 0; i < phdr_num_; ++i) {
    const Elf32_Phdr* phdr = &phdr_table_[i];
    //還是在遍歷每一個PT_LOAD段
    if (phdr->p_type != PT_LOAD) {
      continue;
    }

    // 計算該PT_LOAD段在內存中的開始地址和結束地址
    Elf32_Addr seg_start = phdr->p_vaddr + load_bias_;
    Elf32_Addr seg_end   = seg_start + phdr->p_memsz;

    Elf32_Addr seg_page_start = PAGE_START(seg_start);
    Elf32_Addr seg_page_end   = PAGE_END(seg_end);
    //計算該PT_LOAD段在內存中對應文件的結束位置
    Elf32_Addr seg_file_end   = seg_start + phdr->p_filesz;

    //文件中的偏移
    Elf32_Addr file_start = phdr->p_offset;
    Elf32_Addr file_end   = file_start + phdr->p_filesz;

    Elf32_Addr file_page_start = PAGE_START(file_start);
    Elf32_Addr file_length = file_end - file_page_start;

    if (file_length != 0) {
      //將該PT_LOAD段的實際內容頁對齊后映射到內存中
      void* seg_addr = mmap((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 %d: %s", name_, i, strerror(errno));
        return false;
      }
    }

    //如果該段的權限可寫且該段指定的文件大小並不是頁邊界對齊的,就要對頁內沒有文件與之對應的區域置0
    if ((phdr->p_flags & PF_W) != 0 && PAGE_OFFSET(seg_file_end) > 0) {
      memset((void*)seg_file_end, 0, PAGE_SIZE - PAGE_OFFSET(seg_file_end));
    }

    seg_file_end = PAGE_END(seg_file_end);

    // 如果該段指定的內存大小超出了文件映射的頁面,就要對多出的頁進行匿名映射
    // 防止出現Bus error的情況
    if (seg_page_end > seg_file_end) {
      void* zeromap = mmap((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;
}
//這個函數看看就可以,就是在找PT_PHDR段並檢查,此段指定了段表本身的位置和大小
bool ElfReader::FindPhdr() {
  const Elf32_Phdr* phdr_limit = phdr_table_ + phdr_num_;

  // If there is a PT_PHDR, use it directly.
  for (const Elf32_Phdr* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
    if (phdr->p_type == PT_PHDR) {
      return CheckPhdr(load_bias_ + phdr->p_vaddr);
    }
  }
  for (const Elf32_Phdr* phdr = phdr_table_; phdr < phdr_limit; ++phdr) {
    if (phdr->p_type == PT_LOAD) {
      if (phdr->p_offset == 0) {
        Elf32_Addr  elf_addr = load_bias_ + phdr->p_vaddr;
        const Elf32_Ehdr* ehdr = (const Elf32_Ehdr*)(void*)elf_addr;
        Elf32_Addr  offset = ehdr->e_phoff;
        return CheckPhdr((Elf32_Addr)ehdr + offset);
      }
      break;
    }
  }
  DL_ERR("can't find loaded phdr for \"%s\"", name_);
  return false;
}

至此 So的裝載部分就分析完了

總結

總結一下So的裝載就是根據So的文件信息,先讀入So的頭部信息,並進行驗證。然后找到段表的位置,遍歷段表的每一個段,根據PT_LOAD段指定的信息將So進行裝載,如果我們要模擬這個過程,只需要注意一下細節就可以了。相對於So的裝載,更難的部分是So的動態鏈接,我們另起一篇文章來講解So的動態鏈接。如果覺得本篇文章對您有用,可以掃碼加入我們的星球,不定期分享各種最新的技術


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM