7、iOS强化 --- 静态链接(详解)

之前我们在讲链接与符号的时候提到了静态链接动态链接,本章我们来详细的梳理一下静态链接
接下来我们用实例来讲解一下:代码如下:

// a.c 文件
extern int global_var;

void func(int a);

int main(int argc, const char * argv[]) {
    int a = 99;
    func(a + global_var);
    return 0;
}
// b.c 文件
int global_var = 1;

void func(int a) {
    global_var = a;
}

首先我们生成a.o & b.o

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

接着将a.o & b.o合并生成可执行文件:

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

⚠️ 注意:这里生成的两个目标文件都是基于arm64架构。a.o & b.o通过静态链接后生成Mach-O文件ab。(其实这里基于arm64架构,链接的过程也是有动态库(系统库)参与的,这里我们只讨论静态链接。)

image.png

这里我们先认识两个概念:模块 & 符号
对于符号在前面的文章中已经做了介绍,这里我们就再简单的讲一下:

名字 解释
模块 可以理解为一个源代码文件就是一个模块,比如上面的a.c & b.c。我们在实际的开发中,一般来讲一个类在一个源文件上,就形成了一个模块。模块化的好处就是易于复用和维护,再一点就是在编译的时候,未改动的模块不用从新编译,直接用之前编译好的缓存就行。
符号 简单理解就是函数名和变量名,比如上面的mainglobal_varfunc

空间和地址分配

相似段合并
  • 静态链接:将多个目标文件合并成一个可执行文件。
    在这个过程中,把多个目标文件里面相同性质的段合并到一起。
    比如我们来合并a.o & b.o,生成ab(Mach-O);在合并的过程中,a.o & b.o里面的数据段一起合并成ab里面的数据段;同理,数据段也是一样的。

两步链接
  • 第一步:空间与地址分配
    扫描所有的输入目标文件,并且获得他们各个段的长度属性位置,将输入目标文件中的符号表中所有的符号定义符号引用收集起来,统一放到一个全局符号表中。这一步中,链接器能够获得所有的输入目标文件的段的长度,将它们合并,计算出输出文件中各个段合并后的长度位置,并建立映射关系。

  • 第二步:符号解析和重定位
    使用上面第一步收集到的信息,读取输入文件中段的数据、重定位信息,并且进行符号解析重定位,调整代码中的地址等。

    重定位
    a模块使用了global_varfunc两个符号,那么怎么知道这两个符号的地址呢?
    我们来看一下a.o文件:

    image.png

    global_var(地址0x28) 和 func(地址0x3C)的地址都是假地址。编译器暂时用假地址替代,把真正的地址计算工作留给链接器。通过前面的空间与地址分配可以得知,链接器在完成地址与空间分配之后,就可以确定所有符号的虚拟地址了。也就是说,链接器可以根据符号的地址对每个需要重定位的指令进行地址修正。
    我们再来看一下ab可执行文件:
    image.png

    可以看到global_var (地址:0x100007000,指向data段,值为1) 和 func(地址: 0x100007F90,指向func函数地址)都是真实的地址。

    重定位表
    链接器是如何知道a模块里面有哪些指令被调整,这些指令该如何调整。
    这是因为:在a.o里面,有一个重定位表,专门保存这些与重定位相关的信息。而且每个sectionsection_64headerreloff(重定位表里的偏移) 和 nreloc(需要重定位的符号的数量),让链接器知道a模块里面的哪个section里的指令需要调整。

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 */
};

重定位表可以可以认为是一个数组,数组里的元素为结构体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 */
};

r_addressr_length可以让我们知道要重定位的字节;r_symbolnum(当为外部符号时)是符号的index。
我们看一Relocation里面的内容:

image.png

下面我们去Symbol Table里面找对应的符号:
image.png

通过上面可以看到,a.o中的重定位表(Relocation)记录了符号_func_global_var,两个符号需要重定位。并且给出了两个符号在代码段中的位置,和指向符号表(Symbol Table)index,链接的时候(a.o里面有这两个符号的引用,b.o里面有这两个符号的定义,一起合并到全局符号表里面),在全局符号表中可以找到这两个符号的虚拟内存地址和其它信息,就可以完成重定向工作了。

上面说道r_symbolnum(当为外部符号) 是符号表的index,我们再来看一下加载命令:符号表

/*
 * The symtab_command contains the offsets and sizes of the link-edit 4.3BSD
 * "stab" style symbol table information as described in the header files
 *  and .
 */
struct symtab_command {
    // 共有属性。指明当前描述的加载命令,当前被设置为LC_SYMTAB
    uint32_t    cmd;        /* LC_SYMTAB */
    // 共有属性。指明加载命令的大小,当前被设置为sizeof(struct symtab_command)
    uint32_t    cmdsize;    /* sizeof(struct symtab_command) */
    // 表示从文件开始到 symbol table 所在位置的偏移量。symbol table用[nlist]来表示
    uint32_t    symoff;        /* symbol table offset */
    // 符号表内符号的数量
    uint32_t    nsyms;        /* number of symbol table entries */
    // 表示从文件开始到 string table 所在位置的偏移量
    uint32_t    stroff;        /* string table offset */
    // 表示string table大小(以byte为单位)
    uint32_t    strsize;    /* string table size in bytes */
};

加载命令的前两个参数是cmd&cmdsize;符号表加载命令的symoff&nsyms告诉链接器符号表的位置(偏移)和个数;stroffstrsize告诉字符串表的位置和大小。这个我们在5、iOS强化 --- 链接与符号(补充内容)有提到过。

符号表也是一个数组,里面的元素是结构体nlist_64():

/*
 * This is the symbol table entry structure for 64-bit architectures.
 */
struct nlist_64 {
    union {
        uint32_t  n_strx; /* index into the string table */
    } n_un;
    uint8_t n_type;        /* type flag, see below */
    uint8_t n_sect;        /* section number or NO_SECT */
    uint16_t n_desc;       /* see  */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};

n_strx代表字符串标的index,可以找到符号对应的字符串;
n_sect代表第几个section
n_value代表符号的地址值。

image.png

符号解析
为什么要链接呢?
因为一个模块(a模块)可能引用了其它模块(b模块)的符号,所以需要把多有的模块(.o目标文件)链接在一起。
重定位就是:链接器会去查找由所有输入的.o目标文件的符号组成的全局符号表,找到相应的符号后进行重定位。

⚠️ 其中两个常见的错误:
1、ld: dumplicate symbols:多个目标文件里面有相同的符号,导致全局符号表出现多个一样的符号。
2、Undefined symbols:需要重定位的符号,在全局符号表里面没有找到(一个符号:有引用,未定义)。

静态库链接

一个静态库可以简单看做一个目标文件的合集,也就是多个目标文件经过压缩合并形成的一个文件。
而静态库链接,就是将自己的目标文件与静态库里面的某个模块(用到的一个或多个目标文件)链接成可执行文件。

静态库一般包含多个目标文件,可是链接器在链接静态库的时候是以目标文件为单位的。假设我们把所有的函数放在一个目标文件里面,而我们只用到了一个函数,此时却把很多没有用到的函数一起链接到了可执行文件里面。

静态库链接示意图:


image.png

参考文档:https://juejin.cn/post/6844903912198127623

你可能感兴趣的:(7、iOS强化 --- 静态链接(详解))