-
-
Android Linker详解
-
发表于: 2021-10-15 13:55 7470
-
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类中加这么一段代码
1 2 3 | 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
1 2 3 | 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
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 | 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
1 2 3 4 5 6 7 8 9 10 11 | 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
1 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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
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 | 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
1 2 3 4 5 6 7 8 9 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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
1 2 3 4 5 6 7 | 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
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 | 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
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 | 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
1 2 3 4 5 6 7 8 | bool ElfReader::Load() { return ReadElfHeader() && VerifyElfHeader() && ReadProgramHeader() && ReserveAddressSpace() && LoadSegments() && FindPhdr(); } |
Load函数分别调用了6个函数
- ReadElfHeader 读取ElfHeader
- VerifyElfHeader 验证ElfHeader
- ReadProgramHeader 读取程序头表
- ReserveAddressSpace 准备地址空间
- LoadSegments 加载段
- FindPhdr 寻找Phdr段
从函数名直译,我们也能知道一个大概。下面我们来分析这6个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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; } |
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 | 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; } |
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 | 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; } |
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 | 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; } |
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 | 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; } |
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 | / / 上面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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | / / 这个函数看看就可以,就是在找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的动态链接。如果觉得本篇文章对您有用,可以扫码加入我们的星球,不定期分享各种最新的技术
赞赏
- [原创]Base64 编码原理 && 实现 23389
- [原创]Lsposed 技术原理探讨 && 基本安装使用 26341
- [原创]Unidbg-Linker部分源码分析(下) 28647
- [原创]Unidbg-Linker部分源码分析(上) 26136