美文网首页
8、iOS强化 --- 动态链接(详解)

8、iOS强化 --- 动态链接(详解)

作者: Jax_YD | 来源:发表于2021-03-11 09:36 被阅读0次

我们在5、iOS强化 --- 链接与符号(补充内容)里面提到了动态链接,在这里我们再详细的探讨一下,动态链接到底是怎么链接的。

  • 动态链接的基本思想就是:把程序的模块分割开来,不是通过静态链接在一起,而且推迟到程序运行的时候链接在一起。

还记的我们之前举的例子吗?
比如:我们开发中经常会用到的Foundation框架(Apple提供的),如果采用静态链接的方式,那么市面上所有用到它的APP都要在自己的Mach-O文件中集成它。试想一下,一旦该库出现问题,那所有用到它的APP都要从新集成,从新上架。这样做不仅让APP的体积变大,而且还极不方便。

接下来我们来看一下动态链接是怎么实现的。

动态链接的基本实现

首先我们来了解两个名词:

名字 解释
dyld the dynamic link editor。动态链接器
dylib 动态链接库,也叫做共享对象
  • 静态链接动态链接都是把程序分割成一个个独立的模块,但是不同的是:
    1、静态链接是运行前就用ld链接器链接成一个完整的程序。
    2、动态链接是程序主模块被加载的时候,对应的Mach-O文件里面有dyld加载命令,通过这个dyld去寻找依赖的dylib(⚠️ Mach-O有动态链接加载命令),把dylib加载到内存(如果对应的dylib不在内存),然后将程序中所有未决议的符号绑定到相应的dylib中,并进行重定位工作。
    dyld & dylib加载命令如下:

    image.png
  • dylinker_command

// dyld 加载命令
struct dylinker_command {
    uint32_t    cmd;        /* LC_ID_DYLINKER, LC_LOAD_DYLINKER or
                       LC_DYLD_ENVIRONMENT */
    uint32_t    cmdsize;    /* includes pathname string */
    union lc_str    name;        /* dynamic linker's path name */
};
  • lc_str
    dyld加载命令中
    1、offsetsizeof(cmd)+sizeof(cmdsize)+sizeof(offset)=12
    2、ptr 表示dyld的路径
    则上面表示偏移12位置是dyld的路径。
    在加载命令中,假如有字符串,那就都用lc_str表示,lc_str仅仅告诉去相对于加载命令头部(cmd)多少的偏移位置去取字符串,这个字符串都是放在加载命令结构体最后。
/*
 * A variable length string in a load command is represented by an lc_str
 * union.  The strings are stored just after the load command structure and
 * the offset is from the start of the load command structure.  The size
 * of the string is reflected in the cmdsize field of the load command.
 * Once again any padded bytes to bring the cmdsize field to a multiple
 * of 4 bytes must be zero.
 */
union lc_str {
    uint32_t    offset; /* offset to the string */
#ifndef __LP64__
    char        *ptr;   /* pointer to the string */
#endif 
};
  • dylib加载命令
struct dylib_command {
    uint32_t    cmd;        /* LC_ID_DYLIB, LC_LOAD_{,WEAK_}DYLIB,
                       LC_REEXPORT_DYLIB */
    uint32_t    cmdsize;    /* includes pathname string */
    struct dylib    dylib;      /* the library identification */
};
struct dylib {
    union lc_str  name;         /* library's path name */
    uint32_t timestamp;         /* library's build time stamp */
    uint32_t current_version;       /* library's current version number */
    uint32_t compatibility_version; /* library's compatibility vers number*/
};

想必这里大家都主要到了,dyld&dylib加载命令的最后都是lc_str,大家仔细阅读lc_str的注释部分。
大致意思就是说:name放在加载命令的结尾,name放到加载命令结构体时候,偏移量是从加载命令结构体的开头开始的。并且加载命令是4字节的倍数,不够的话用0填充(必须是0)。

动态链接实例

  • a.c文件
extern char *global_var;

void print(char *str);

int main(int argc, const char * argv[]) {
    print(global_var);
    return 0;
}
  • b.c文件
extern char *global_var;

void print(char *str);

int main(int argc, const char * argv[]) {
    print(global_var);
    return 0;
}

我们将b.c文件包装成dylib动态库,然后a文件与动态库合并成main可执行文件:
1、编译a.c,生成a.o

xcrun -sdk iphoneos clang -c a.c -o a.o -target arm64-apple-ios12.2

2、编译b.c 生成libPrint.dylib

xcrun -sdk iphoneos clang -fPIC -shared b.c -o libPrint.dylib -target arm64-apple-ios12.2

3、链接main.o&libPrint.dylib,生成main可执行文件

xcrun -sdk iphoneos clang a.o -o main -L . -l Print -target arm64-apple-ios12.2

// -target arm64-apple-ios12.2 ==> 运行的目标版本号iOS12.2
// -l Print ==> 链接libPrint.dylib
// -L . ==> libPrint.dylib在当前路径寻找(.代表当前路径)
image.png
⚠️ 上面说过动态链接静态链接的区别就是链接的时机推迟到程序被加载的时候。但是上面第3步将目标文件main.o链接成可执行文件的时候,还是用到了动态库libPrint.dylib
我们来看一下main可执行文件:
image.png
我们在7、iOS强化 --- 静态链接(详解)讲过,再经过重定位之后,静态链接的话,此时已经\color{red}{知道符号地址}。但是通过上图可以看到,动态链接的话,此时是\color{red}{不知道符号地址}。、

动态链接的情况下,在链接的时候让链接器(dyld)知道这两个符号来自dylib,只需要给这两个符号做标记就可以了,而不是此刻进行绑定和重定位。生成的main可执行文件知道这两个符号来自dylib,做了标记。等到main被加载的时候,再把这两个符号绑定到dylib中,并进行重定位。

PIC(position-independent code 地址无关代码)

  • 产生地址无关代码的原因
    dylib在编译的时候,是不知道自己在进程中的虚拟内存地址的。因为dylib可以被多个进程共享。
    例子:现在有一个dylib作为共享对象(标记为Z),同时被两个进程(A 和 B) 共享。假如A进程可以在空闲地址0x1000-0x2000存放共享对象Z,但是B进程的0x1000-0x2000已经被主模块占用了,只有空闲地址0x3000-0x4000可以存放共享对象Z。
    那么Z对象里面有一个函数,此时在A进程中的虚拟内存地址就是0x10f4,在B进程中的虚拟内存地址就是0x30f4。这样的话机器指令就不能包含绝对地址了(动态库代码段所有进程共享;可修改的数据段,每个进程有一个副本,私有的)。

  • PIC原理
    PIC就是为了解决dylib的代码段能被共享的问题。
    PIC:把指令中那些需要被修改的部分剥离出来,跟数据部分放在一起,这样指令部分就可以保持不变,而数据部分是每个进程都有一个副本。

dylib需要被修改的部分(对地址的引用),按照是否跨模块分为两类;按照引用方式又可以分为两类:函数调用 & 数据访问。这样总共可以分为4类:

dylib需要被修改的部分,分类情况
1、模块内部的函数调用、跳转等。
2、模块内部的数据访问,比如模块中定义的全局变量,静态变量等。
3、模块外部的数据访问、跳转等。(如:a.dylib 调用 b.dylib中的函数)
4、模块外部的数据访问。(访问其他模块模块中定义的全局变量)

延迟绑定

  • 延迟绑定的基本思想跟iOS的objc_msgSend基本一样的,都是第一调用函数的时候,去查找函数的地址。而不是程序启动的时候,先把所有的地址查找好。

模块外部的函数数据的放完,都是通过got来间接寻址的。程序被加载的时候,动态链接器要进行一次链接工作,比如加载依赖的模块,修改got里面的地址(符号查找、地址重定位)等工作,减慢了程序的启动速度。

举个例子:我们在开发中引入Foundation动态库,但是并不是说库里面的所有函数我们都会用到。如果我们在程序启动的时候就去绑定所有的符号地址,显然是不合理的。

这时候就用到了延迟绑定的技术。
Mach-O中,因为模块间的数据访问很少(模块间如果提供了很多全局变量给其他模块使用,那耦合度太大了,所以这种情况很少见),所以外部数据地址都放到got(也叫做Non-Lazy Symbol Pointers)数据段,非惰性的,动态链接阶段就寻找好所有数据符号的地址。
而模块间函数调用就很频繁,这里就用延迟绑定的技术,将外部函数地址放到la_symbol_ptr(Lazy Symbol Pointers)数据段,惰性的,程序第一次调用这个函数的时候,才去寻找函数地址,然后将地址写入到这个数据段。

image.png
image.png
  • 这里我们再引入一个加载命令dysymtab_command间接符号表
    间接符号表我们在4、iOS强化 --- 链接与符号(Symbol)里面讲过,它里面保存着我们当前可执行文件使用的其他动态库里面的导出符号。我们来看一下间接符号表:
    image.png
struct dysymtab_command {
    uint32_t cmd;    /* LC_DYSYMTAB */
    uint32_t cmdsize;    /* sizeof(struct dysymtab_command) */
    ...
    uint32_t indirectsymoff; /* file offset to the indirect symbol table */ 到间接符号表的偏移量,可以确定位置
    uint32_t nindirectsyms;  /* number of indirect symbol table entries */ 间接符号表里面符号的个数
    ...
}

dysymtab_command可看做指向一个数组,里面的元素是整型数字。
例如:dysymtab[0] == 2 ,代表间接符号表第0项对应的符号,在符号表中的第2项。

  • 还有一个结构体需要说明一下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 */
};

大家可以看到结构体中有一个reserved1,在got数据段的section_64里面,reserved1代表got里面的符号在间接符号表(IndirectSymbolTable)的起始Index
结合上面的间接符号表的含义,我们可以得出这样一个逻辑:
1、假设A代表间接符号表里面的元素,则 A = IndirectSymbolTable[got.section_64.reserved1]
2、拿到A之后,我们就可以去符号表里面找到对应的符号。

  • 下面我们按照上面的逻辑来寻找一下_global_var
    image.png
  • 下面我们再来寻找一下_print
    1、我们在la_sumbol_ptr数据段里面找到_print:
    image.png
    可以看到_print对应了一个地址7FAC
    我们在讲3、iOS强化 --- Mach-O 文件有提到过下面一句话:
    image.png
    那么接下来我们去__stub_helper找一下对应的元素
    2、查看__stub_helper
    image.png
    这里面是一些汇编指令,可能有的同学看起来会有点吃力,不过没关系。
    首先我们要明确一点,__la_symbol_ptr表里面的指针,就是指向这里,这一点是没有问题的。
    接着我们会发现一行注释:literal pool symbol address:dyld_stub_binder。那么我们就去找一下dyld_stub_binder:
    image.png

_print的寻址过程,总结一下就是:
1、先从la_symbol_ptr找到对应指针;
2、然后去__stub_helper找到相应的地址(建议大家去理解一下这里面的汇编代码);
3、跳到相应的地址,此时到了got数据段。

⚠️ 我们在上面提到过,got里面存放的是外部数据符号。但是在动态链接的时候,会重定位dylddyld_stub_binder函数地址,放在这里。
dyld_stub_binder是一个寻址外部函数地址的函数,所以必须提前重定位好。
第一次调用print函数的时候,会调用dyld_stub_binder函数去寻找地址,找到之后把print的地址写入到la_symbol_ptr数据段,替换到对应的指针(这里面是结尾是7FAC的地址),然后调用print函数。
之后再调用print函数的时候,就不用去寻址了,直接就可以调用,因为函数地址已经被写入了la_symbol_ptr数据段。


参考文档:https://juejin.cn/post/6844903922654511112#comment

相关文章

网友评论

      本文标题:8、iOS强化 --- 动态链接(详解)

      本文链接:https://www.haomeiwen.com/subject/khjwqltx.html