精华内容
下载资源
问答
  • linker
    2021-10-15 14:16:16

    本文目的

    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的动态链接。如果觉得本篇文章对您有用,可以扫码加入我们的星球,不定期分享各种最新的技术
    在这里插入图片描述

    更多相关内容
  • linker

    2021-03-27 10:09:43
    React + Node.js URL缩短器
  • The GNU linker.pdf

    2021-10-08 15:55:47
    ld accepts Linker Command Language files written in a superset of AT&T’s Link Editor Command Language syntax, to provide explicit and total control over the linking process. This version of ld uses ...
  • Linker

    2021-03-27 15:46:18
    连结器 用于管理任务,笔记等的应用。类似Evernote的东西。 技术: 微服务架构:Spring Boot 2.4,Spring Cloud 2020,Resilience4j,Feign; 安全:Spring Security和令牌; 讯息:Kafka 数据库:Liquibase,...
  • 将Android Linker部分源码单独提取出来,可用ndk-build编译修改,用于学习ELF格式,及Linker加载链接过程
  • S32DS for ARM 2018.R1 链接文件(linker file)学习笔记 在S32DS for ARM 2018.R1自带的例程中有两个链接文件,分别为S32K1xx_flash.ld和S32K1xx_ram.ld,前者针对的是程序在flash中运行的链接文件,后者是在ram中...
  • linkers and loaders
  • Visual-linker:Gnu Linker

    2021-04-08 12:21:13
    可视链接器 Gnu Linker
  • Linker 9.0

    2016-11-14 19:59:43
    Linker 9.0
  • 易语言静态编译所需要的连接器,VC98linker。,
  • GNU-linker.pdf

    2020-07-16 09:41:07
    GNU链接器的手册,最新版 V2.34,gnu linker manual version 2.34
  • 易语言编译连接器 vc6linker vc7linker vc8linker vc9linker vc2010linker
  • set-dynamic-linker.md

    2020-12-18 14:10:56
    Set the name of the dynamic linker. This is only meaningful when generating dynamically linked ELF executables. The default dynamic linker is normally correct; don't use this unless you know wh
  • ARM LINKER

    2018-10-13 12:11:37
    ARM LINKER ARM_ld.pdf
  • 今天编写一个简单的代码,报错了: template class matrix final { public: matrix(uint32_t size): SIZE(size) { _data = static_cast(malloc(SIZE * sizeof(T*))); for (uint32_t i = 0;...
  • 弃用 -> gulp-inject 现在已经弃用了。 请使用很棒的gulp-inject ... var linker = require ( 'gulp-linker' ) , // Read templates gulp . src ( 'templates/*.html' ) // Link the JavaScript . pipe
  • api ' me.twocities:linker:0.0.5 ' kapt ' me.twocities:linker-compiler:0.0.5 ' } 用法 Linker有两个部分:批注和LinkResolver 注解 @活动链接 使用注解@Link指示尊重哪个URI: @Link( " link://product/...
  • Plain Text Linker-crx插件

    2021-04-02 23:34:23
    也可用于firefox:https://addons.mozilla.org/firefox/addon/plain-text-linker/ 双击一块纯文本时,纯文本链接器试图确定您单击的内容是否可以是URL,并尝试打开它。 请查看链接的主页以获取更多完整信息。 可以...
  • 该存储库存储tvm_linker实用程序的源代码。 它需要TON智能合约的TVM( )汇编源代码,对其进行编译和链接,添加标准选择器和运行时,并将其存储到二进制TVC文件中。 而且,它可以通过模拟TON交易的计算阶段立即执行...
  • VC98linker 纯净原版

    2019-02-22 20:42:18
    通用易语言链接器 VC98linker 使用本工具可解决无法静态编译的问题 静态编译时提示:无法定位链接器!请检查 tools\link.ini 中的配置是否正确。 静态连接失败
  • The GNU Linker V2.19.51

    2019-02-20 22:36:13
    此版本修正了 《Using ld》(The GNU Linker (ld version 2) v2.14版本)里的错误,同时新增了一些内容,更易于理解. The GNU Linker (ld) 官方说明文档 (Sourcery G++ Lite 2010q1-188) Version 2.19.51.
  • TASKING linker 常见问题

    2018-10-15 11:28:27
    TASKING 编译器的连接器( linker)使用上的 常见问题及解答
  • 易语言编程生成EXE文件必备工具
  • 假链接 ...项目介绍 修改Android链接器以提供加载模块和plt钩子功能。请检查详细原理, 支援的Android Android版本: Android 5.0 Android 11 +。 支持指令: x86 , x86_64 , arm , arm64 建造 ...
  • Linker 7.0

    2016-11-14 19:54:38
    Linker 7.0
  • vc14linker

    2018-04-17 19:05:34
    vc14linker2016编译器vc14linker2016编译器vc14linker2016编译器
  • linker and loader

    2018-08-28 10:52:54
    linker and loader 英文版!
  • 概述用于链接针对运行现代 Linux 操作系统的 x86_64 平台的二进制文件的纯 C++ 工具
  • Linker 8.0

    2016-11-14 19:56:32
    Linker 8.0

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 114,167
精华内容 45,666
关键字:

linker

友情链接: ISO_11898-3-2006.pdf.zip