在iOS App开发中,程序的链接是由Xcode中自带的LLVM来帮助我们完成的,程序员们也因此更注重业务逻辑的编写。但其实了解链接的原理能让我们对iOS的底层有更深层次的认识,也有助于我们从底层原理方面去解决各种疑难问题。

在京东商城项目中,各个业务模块会被编译成静态库后再一起链接进主工程,而现在的大部分静态插桩的方案都是在编译期进行的,这种方案不适用于京东商城。此时如果想要插桩的话该怎么办呢?

如果我们对链接的原理掌握透彻的话,以上问题就能迎刃而解。下面就让本文从静态链接动态链接两个方面带着大家一起深入到链接的原理及其应用中来。

静态链接

在没有链接之前一个程序的代码就是一个文件,显然是不利于多人合作开发和维护的。为了让代码变得更易开发和维护,代码被分成了若干个模块,每一个.c的代码源文件可以被理解成一个模块,每一个模块独立编译,再把所有编译完的文件链接起来,这个过程就是我们所说的静态链接。

01空间与地址分配

  • 每个单独的文件编译后都会生成一个符号表,静态链接后这些表会被合并成一个全局符号表。

  • 合并的规则是相似段合并、数据段与数据段合并、代码段与代码段合并。

  • 合并后每一个符号的的地址被确定,并写入全局符号表中。

相似段合并空间分配策略02重定位符号

经过空间与地址分配之后代码段中指令用到的符号地址还没有更新,想要确定符号的地址需要用到重定位表。编译后.o文件中需要重定位的符号的相关信息会存入重定位表中。

重定位表可以看作是多个relocation_info组成的数组,一个relocation_info代表了一个需要重定位符号的具体信息。链接器此时知道符号的地址,拿到了需要重定位的位置,就会去完成指令修改的工作。

// 重定位表struct relocation_info {   int32_t  r_address;  /* offset in the section to what is being           relocated */   uint32_t     r_symbolnum:24,  /* symbol index if r_extern == 1 or section           ordinal if r_extern == 0 */  r_pcrel:1,   /* was relocated pc relative already */  r_length:2,  /* 0=byte, 1=word, 2=long, 3=quad */  r_extern:1,  /* does not include value of sym referenced */  r_type:4;  /* if not 0, machine specific relocation type */};

举个例子,代码是在main文件中实现了一个 hook_msgSend的方法,然后在a文件中调用这个hook_msgSend这个方法。a文件编译后的.o文件中的重定位表可以确定符号的位置和需要修改指令的位置。链接之后形成的可执行文件中,分配空间后的函数地址是0x100005F08,可以在下图的符号表中找到。同时a文件里面调用hook_msgSend的指令已经被修改成0x100005F08。如下面3图所示:

重定位表中符号的位置

符号表中的地址

指令跳转的地址03实际应用:对静态库插桩

一个静态库是多个目标文件的集合。静态库链接指某模块里的文件与静态库里的某个模块里的文件链接成可执行文件。

二进制化后的京东商城App在启动优化二进制重排时想要插桩,只能从编译之后的静态库入手,通过修改字符串表中的函数名,并在主工程中创建相应方法,来完成插桩。由于修改了字符串表,静态库链接修改指令时,链接器通过重定位表定位符号时会定位到修改主工程中的函数指针。

在了解了静态链接的原理后,我们的实现是这样的(目标是Hook静态库工程中的 objc_msgSend方法):

1. 通过python或者Shell脚本将静态库的.o文件中objc_msgSend在字符串表中的值改成 hook_msgSend。

2. 在主工程中实现这个hook_msgSend方法。

由于hook_msgSend这个方法在符号表中的位置就是重定位表中需要重定位的objc_msgSend的位置,经过动态链接修改指令之后,原来静态库中的objc_msgSend调用会被修改成主工程中函数实现的地址。基本流程如下图所示:

我们选择采用汇编来实现hook_msgSend函数。objc_msgSend本身是不定参数的。虽然不定参数可以使用va_list来解决,但是 objc_msgSend 方法中不是传统意义上的可变参数,可以参考这篇文章苹果为什么要修改 objc_msgSend 的原型。具体实现方式已经有非常多的文章了,这里就不细讲了。

动态链接

静态链接会将目标文件合并成一个可执行文件,当有多个应用程序需要使用同一个静态库的时候,每个程序都要链接一次这个静态库,每个程序的可执行文件中都包含了这个静态库中的代码,这种方式造成了空间的浪费。为了弥补这个不足,就有了动态链接。将一些通用的库比如Foundation;UIKit这些系统库独立出来,不通过静态链接,而推迟到程序运行时链接在一起。

01PIC (position independ code)

  • 为什么要有PIC: 由于虚拟地址(ASLR)的缘故每次加载程序的时候程序在内存中的基地址都是不一样的,并且虚拟内存也有可能被其他程序占用,所以只有当前进程可以确定动态库的内存地址。这样设计动态库的时候所有的指令就需要在运行时动态的修改。

  • PIC解决的问题:  动态库将指令中需要修改的部分独立出来,放进了数据部分,数据部分每个进程都会保留一个副本,程序可以动态修改这个部分,而指令部分是不变的。这样就做到了PIC。

02动态符号绑定

在静态链接中,当可执行程序内部需要调用外部动态库的函数时,会先在data段建立指针变量,用来存储动态库的函数地址,这些指针就是将来需要被动态修改的的部分。machO被加载之后 ,这些指针变量中的值就会被替换成动态库中实现的地址,dyld确定了这些地址之后对动态库中的指令进行修改,这个过程就是动态符号绑定。

前面提到了当需要动态链接的时候,会在machO的data段中建立函数指针,这些指针最终会被替换成真正的动态库里函数的地址,修改指令的过程和静态链接类似,就是找到地址之后对相应的指令进行修改。

在讲动态链接的具体实现前,这里把本文中所需要用到的machO加载命令的结构先列出来,以供参考查阅。

// machO 的头struct mach_header_64 {  uint32_t  magic;    /* mach magic number identifier */  cpu_type_t  cputype;  /* cpu specifier */  cpu_subtype_t  cpusubtype;  /* machine specifier */  uint32_t  filetype;  /* type of file */  uint32_t  ncmds;    /* number of load commands */  uint32_t  sizeofcmds;  /* the size of all the load commands */  uint32_t  flags;    /* flags */  uint32_t  reserved;  /* reserved */};// 间接符号表 struct dysymtab_command {  ...     uint32_t indirectsymoff; /* file offset to the indirect symbol table */  间接符号表的位置    uint32_t nindirectsyms;  /* number of indirect symbol table entries */ 间接符号表中符号的个数};  // 符号表加载命令struct symtab_command {   uint32_t  cmd;    /* LC_SYMTAB */  uint32_t  cmdsize;  /* sizeof(struct symtab_command) */  uint32_t  symoff;    /* symbol table offset */  uint32_t  nsyms;    /* number of symbol table entries */  uint32_t  stroff;    /* string table offset */  uint32_t  strsize;  /* string table size in bytes */};// 段加载命令struct segment_command_64 { /* for 64-bit architectures */  uint32_t  cmd;    /* LC_SEGMENT_64 */  uint32_t  cmdsize;  /* includes sizeof section_64 structs */  char    segname[16];  /* segment name */  uint64_t  vmaddr;    /* memory address of this segment */  uint64_t  vmsize;    /* memory size of this segment */  uint64_t  fileoff;  /* file offset of this segment */  uint64_t  filesize;  /* amount to map from the file */  vm_prot_t  maxprot;  /* maximum VM protection */  vm_prot_t  initprot;  /* initial VM protection */  uint32_t  nsects;    /* number of sections in segment */  uint32_t  flags;    /* flags */};// 段里面的section_64 的加载命令struct section_64 { /* for 64-bit architectures */  char    sectname[16];  /* name of this section */  char    segname[16];  /* segment this section goes in */  uint64_t  addr;    /* memory address of this section */  uint64_t  size;    /* size in bytes of this section */  uint32_t  offset;    /* file offset of this section */  uint32_t  align;    /* section alignment (power of 2) */  uint32_t  reloff;    /* file offset of relocation entries */  uint32_t  nreloc;    /* number of relocation entries */  uint32_t  flags;    /* flags (section type and attributes)*/  uint32_t  reserved1;  /* reserved (for offset or index) */  uint32_t  reserved2;  /* reserved (for count or sizeof) */  uint32_t  reserved3;  /* reserved */};

(1)直接符号绑定(GOT表) 

所有的全局变量引用的地址都会被存放在GOT表中,模块间使用到全局符号的时候,全局符号的地址会被绑定到GOT表中。

(2)延迟绑定(PLT) 

由于函数的调用比较复杂,为了提升性能,动态链接采用了(PLT)的思想,简单来说就是在函数第一次调用之后再进行符号绑定。这个实现在Linux中是将got表一分为二,一张叫.got ,另一张叫.got.plt, 其中.got存放全局变量引用的地址,.got.plt存放函数引用的地址, 而在iOS中也有相应的这两张表,这两张表分别如下:

  • Non-Lazy Symbol Pointers 非懒加载符号表,内部存储全局变量的符号

  • Lazy Symbol Pointers 懒加载符号表,内部存储函数的符号

03寻找地址修改指针

当知道了懒加载和非懒加载两张表(下文简称GOT表)中存放的是外部符号的指针之后,就需对GOT里面的指针进行重新绑定。GOT中存放的都是指针,我们不知道这些指针到底代表的是什么符号,还有这些符号来自哪个动态库。如何定位这这些符号呢?

在上文我们讲的静态链接中,静态链接生成的符号表中存有符号的信息,如果这个符号是动态库中的,那么在符号表中我们就能得到该符号来自哪个库,以及名字是什么。关键的问题在于如何从GOT表映射到符号表呢?

通过查找machO的加载命令,我们可以看到GOT表是如何被加载的section_64这个加载命令加载的。在加载section_64加载命令的结构体中,有一个reserved1的保留字段, 加载GOT表的时候这个section_64命令中的reserved1保存的就是该表对应的符号在间接符号表中的初始位置。而间接符号表中存放的值是符号表中符号的index。见下图:

间接符号表可以通过上方的dysymtab_command加载命令找到,符号表可以通过symtab_command加载命令找到。

通过got中的reserved1字段确定了符号在间接符号表的位置后,找到其中存放的数据是117(16进制),这个117就是符号在符号表中的位置。

然后在符号表中通过刚才的117这个index就可以找到相对应的符号,这个符号中标明了符号的名字是NSLog,如图该符号来自Foundation这个动态库。

这个时候我们知道了got表里的指针来自哪里,并且叫什么,动态库本身就是dyld加载的,dyld此刻肯定清楚这个动态库的真实地址在哪里,dyld在内存中找到动态库并从中找到动态库中符号的虚拟地址,最后将找到的地址绑定到got表里的指针上去,这样就完成了重新绑定的过程。

我们总结一下动态链接符号绑定几张表的查找流程,见下图: (其中黄色代表我们要查找的表,蓝色代表相应表的加载命令)

04动态链接的应用

了解了动态链接符号绑定的原理,我们看一下fishhook(https://github.com/facebook/fishhook)是如何利用动态链接来实现hook的。fishhook的工作就是将GOT表里的指针替换成我们的自己的,就可以实现hook了。而正好GOT表处在data段,data段是可读可写的。

我们先来看一下fishhook的使用方法:

void myLog(NSString *format, ...){    printf("mylog");}static void (*orginLLog)(NSString *format, ...);+(void)load{  struct rebinding r1 = {"NSLog", myLog, (void *)&orginLLog};  struct rebinding rebind[1] = {r1}  rebind_symbols(rebind, 2);}

用法很简单,定义一个和原函数类型相同的函数指针,然后定义一下自己的函数实现, 再加上函数名字的字符串,构成一个结构体传给rebind_symbols就行了。但是思考一下, fishhook怎么直接通过一个字符串就定位到函数的呢?

通过上面动态链接的过程发现绑定流程是: got--->indirect间接符号表--->符号表,那要改got里的指针肯定是反向查找,从符号表开始:符号表--->indirect间接符号表--->got, 所以fishhook也一定是走的这个流程,既然fishhook的切入点是一个字符串,那么从一个字符串到符号表,这中间一定有一个联系方式。其实符号表中存有符号在字符串表中的起始位置,那这个时候我们的查找链就诞生了,也就是:String Table--->符号表--->indirect间接符号表--->got表。 

整个寻找过程与上面动态链接寻找的过程基本类似,只是多了一张字符串表用来定位。fishhook的所有代码也是基于这个流程来进行符号查找的。

(1)构建rebinding链表 

1. 在rebind_symbols中遇到的第一个调用是prepend\_rebindings,该函数的作用是构建一个链表_rebindings_head,链表中每一个元素是我们需要重新绑定的符号信息。

2. 使用_dyld_register_func_for_add_image,为每个镜像注册回调函数,注册之后每个镜像加载完之后都会调用_rebind_symbols_for_image函数。

(2)寻找各个表的地址:_rebind_symbols_for_image 

rebind_symbols_for_image所做的事情:

1. 遍历所有加载命令找到符号表加载命令的地址symtab_cmd

2. 遍历所有加载命令找到间接符号表加载命令的地址dysymtab_cmd

3. 计算基地址

4. 通过symtab_cmd找到符号表在内存中的地址,并加上基地址得到真实地址

5. 通过dysymtab_cmd找到间接符号表在内存中的地址,并加上基地址得到真实地址

6. 遍历所有加载命令找到got表的加载命令section64的地址

7. 把上面所有找到的地址传给下面要讲的perform_rebinding_with_section函数

(3)执行绑定:perform_rebinding_with_section 

前面我们提过了secion64里的reserved1字段存储了符号在间接符号表里的起始index,下面的代码就会用到这个 perform_rebinding_with_section所做的事情:

1. 根据reserved字段获得got表在间接符号表中的初始位置

2. 创建一个指针指向got表的首地址

3. 通过间接符号表中存的index 找到符号表中对应符号的位置,再从符号表中存的字符表位置在字符表中找到对应字符

4. 遍历我们传过来的需要绑定的符号,找到相应的符号,进行替换工作

Fishhook 中几张表查找的过程如下图所示:

总结

本文中主要讲的是链接的相关内容,但其实关于编译与链接这个大的课题,还是有很多地方可以去深入探究,比如动态链接中是如何寻找动态库中函数的真实地址的(本文着重讲的是找到地址后是如何替换指针的),比如有关符号strip的概念,动态库的手动加载,符号决议等,这些都是我们可以去深入研究的方向。

了解底层原理有助于我们更好的去解决实际问题,而不是在不断重复的debug中捶胸顿足,也有助于我们从更宽阔的角度来思考程序的优化和效率的提升。希望这篇文章能给大家不一样的感受,底层原理其实离我们并不遥远,只要耐心去学习,就能有所收获。

作者:平台业务研发部-首页团队:陈国强、曹金果、张大鹏、王启启

ios 静态库合成_iOS链接原理解析与应用实践相关推荐

  1. ios 静态库合成_iOS : 静态库(.framework)合并

    如果写了一个Framework,根据Build时选择的机器类型,会分为模拟器Framework和真机Framework,两者是不能混用的. 此时可以通过配置一个Run Script,在Script中使 ...

  2. ios 静态库合成_iOS生成静态库方法-iOS集成静态库-iOS合并静态库

    在iOS的开发过程中,我们常常用到第三方的库.尤其是QQ.百度地图.广告等. 那么,如何制作自己的库文件呢? 如果,将自己写的功能类编译成库文件,分发给其他人来使用呢? 静态库的优点 编译静态库的好处 ...

  3. ios 静态库冲突的解决办法

    参考:http://www.cnblogs.com/machao/p/5288460.html ios 静态库冲突的解决办法 最近在做一个 iOS 的 cocos2d-x 项目接入新浪微博 SDK 的 ...

  4. ios开发 c语言打包.a文件,【转】IOS静态库a文件制作流程

    原文网址:http://www.jianshu.com/p/3439598ea61f 1.新建Cocoa Touch Static Library工程 新建工程 2.Xcode的参数设置 " ...

  5. iOS静态库和动态库

    iOS静态库和动态库 静态库和动态库是什么,以及它们的区别,详细介绍可参考博文:iOS里的动态库和静态库,里面讲的很详细. 静态库动态库的区别 内容来源自:iOS动态库与静态库 静态库和动态库是相对编 ...

  6. ios静态库和代码同名_使用一个代码库开始制作NativeScript iOS和Android应用程序

    ios静态库和代码同名 Users can choose whatever operating system they prefer, but every operating system use d ...

  7. 编译-C++支持iOS静态库的脚本学习

    这是一个编译C++库,支持iOS静态库的一个脚本.仅供研究学习的. #!/bin/bashPLATFORMPATH="/Applications/Xcode.app/Contents/Dev ...

  8. iOS静态库.a文件制作和导入使用

    iOS静态库.a文件制作: 1.新建Cocoa Touch Static Library工程 新建工程 - 选择iOS-FrameWork&Libary,选择 Cocoa Touch Stat ...

  9. 《ClickHouse原理解析与应用实践》读书笔记(2)

    开始学习<ClickHouse原理解析与应用实践>,写博客作读书笔记. 本文全部内容都来自于书中内容,个人提炼. 第一章  ->  第二章: <ClickHouse原理解析与应 ...

最新文章

  1. 算法-----三数之和等于0
  2. C++中 #include与#include
  3. 解决docker中/etc/default/docker配置DOCKER_OPTS 失效问题
  4. python捕获摄像头帧_Xuggler教程:帧捕获和视频创建
  5. 用好这个新功能,报表数据安全瞬间提升一个等级!
  6. Unity AssetBundle 踩坑记录
  7. MaxRects纹理合并算法as3实现
  8. 【导入篇】Robotics:Perception课程_导入篇、四周课程内容、week 1st Perspective Projection
  9. SwitchNAT 测试
  10. java 贝叶斯抠图_毕业论文(设计)基于贝叶斯算法的自动抠图程序设计与实现.doc...
  11. 非线性回归-最小二乘法
  12. 不怕新歌有多嗨,就怕老歌带DJ,Python批量对DJ歌曲进行下载
  13. Redis Java客户端的选择
  14. 读书笔记:云计算概念、技术和架构
  15. 一个实用的在线文档格式转换器
  16. 使用JavaReport制作Web报表与图形 入门示例
  17. Mathon广告过滤规则发现
  18. java复合语句与条件语句
  19. matlab基本知识(入门)
  20. mysql 计算两个日期之间的工作日天数

热门文章

  1. 【VMware vSAN 6.6】8.2.合规性:我们有软硬件项目解决方案
  2. Exchange如何将邮件转发给外部邮件地址
  3. Keepalived+LVS+Nginx+DRBD+Heartbeat+Zabbix集群架构
  4. 基于Android5.0的Camera Framework源码分析 (三)
  5. CSS3 稳固而知新: 居中
  6. FZU 1686 神龙的难题(DLX反复覆盖)
  7. Android问题-DelphiXE5编义时提示找不到“连接器(arm-linux-androideabi-ld.exe)
  8. React学习笔记7:React使用注意事项
  9. 高效排序算法(快排序)
  10. 秒懂上线必不可少的安全测试!