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

2021-03-11  本文已影响0人  Jax_YD

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

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

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

动态链接的基本实现

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

名字 解释
dyld the dynamic link editor。动态链接器
dylib 动态链接库,也叫做共享对象
// 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 */
};
/*
 * 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 
};
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)。

动态链接实例

extern char *global_var;

void print(char *str);

int main(int argc, const char * argv[]) {
    print(global_var);
    return 0;
}
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需要被修改的部分(对地址的引用),按照是否跨模块分为两类;按照引用方式又可以分为两类:函数调用 & 数据访问。这样总共可以分为4类:

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

延迟绑定

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

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

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

image.png
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项。

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之后,我们就可以去符号表里面找到对应的符号。

_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

上一篇下一篇

猜你喜欢

热点阅读