0

    iOS链接原理解析与应用实践

    2023.08.10 | admin | 155次围观

    在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在启动优化二进制重排时想要插桩,只能从编译之后的静态库入手,通过修改字符串表中的函数名,并在主工程中创建相应方法qq空间蓝色链接生成,来完成插桩。由于修改了字符串表,静态库链接修改指令时,链接器通过重定位表定位符号时会定位到修改主工程中的函数指针。

    在了解了静态链接的原理后,我们的实现是这样的(目标是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 方法中不是传统意义上的可变参数,可以参考这篇文章。具体实现方式已经有非常多的文章了,这里就不细讲了。

    动态链接

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

    01

    PIC (position independ code)

    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中也有相应的这两张表,这两张表分别如下:

    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就可以找到相对应的符号,这个符号中标明了符号的名字是NSLogqq空间蓝色链接生成,如图该符号来自Foundation这个动态库。

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

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

    04

    动态链接的应用

    了解了动态链接符号绑定的原理,我们看一下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中捶胸顿足,也有助于我们从更宽阔的角度来思考程序的优化和效率的提升。希望这篇文章能给大家不一样的感受,底层原理其实离我们并不遥远,只要耐心去学习,就能有所收获。

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    发表评论