5、iOS强化 --- 链接与符号(补充内容)

上一篇文章4、iOS强化 --- 链接与符号(Symbol)中我们提到了链接
链接的本质就是把多个目标文件组合成一个文件
但是有一些地方说的不够详细,这里我们做一下补充。

首先我们知道,在生成.o目标文件的过程中,链接器 (llvm-ld) 并没有被执行,这个过程就是编译的过程。
那么llvm-ld在哪里执行呢?
答案:在最终生成Mach-O文件的过工程中被执行。
过程如下:

image.png

1、多个目标文件合并
2、符号表(包括重定位符号表)合并成一张表
3、生成一个Mach-O可执行文件

注意⚠️ :上面从生成.o到生成Mach-O这整一个过程才是一个完整的链接过程

  • 链接有又分为静态链接动态链接

静态链接

  • 静态链接:将多个目标文件合并成一个可执行文件。这个过程中会把多个目标文件相同性质的段合并到一起。比如a.ob.o合并成可执行文件ab;其过程就是将a.o里面的代码段和b.o里面的代码段合并到一起,数据段同理等等。

    image.png

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

    image.png

动态链接

既然有了静态链接,那么动态链接又是怎么回事呢?
通过上面我们知道,静态链接是将多个目标文件合并成一个可执行文件,如果遇到静态库那就将静态库也合并进去。这样在实际开发中就会有一个问题:

比如:我们开发中经常会用到的Foundation框架(Apple提供的),如果采用静态链接的方式,那么市面上所有用到它的APP都要在自己的Mach-O文件中集成它。试想一下,一旦该库出现问题,那所有用到它的APP都要从新集成,从新上架。这样做不仅让APP的体积变大,而且还极不方便。
因此,动态链接应运而生。
只需要在手机端存放Foundation框架,当APP运行的时候,发现使用到了Foundation,这时候再去链接,加载到内存中。这样就方便很多了。即使Foundation出问题,那么直接更新Foundation就可以了,并不需要同时更新APP。

动态链接是在运行时由dyld(动态链接器)动态加载的。

  • 静态链接 与 动态链接 的加载过程
    image.png

可执行文件的调用过程

  • 1、调用fork函数,创建一个process
  • 2、调用execve(程序加载器) 或其衍生函数,在该进程上加载,执行我们的Mach-O文件
    当我们调用execve(程序加载器)内核实际上在执行以下操作:
    ①:将文件加载到内存
    ②:开始分析Mach-O中的mach_header,以确认它是有效的Mach-O文件

如何查看链接器的参数

在终端使用下面的命令可以查看链接器的参数

man ld

image.png

使用/ + 要查找的关键字即可进行检索。
n代表向下检索,N代表向上检索。


Symbol Table

下面我们来讲一下符号(Symbol) 以及符号表(Symbol Table)
⚠️⚠️⚠️ 请大家结合3、iOS强化 --- Mach-O 文件这篇文章来阅读下面的内容。

  • Mach-O中通过两个load commands:
    1、LC_SYMTAB:当前Mach-O
    2、LC_DYSYMTAB:描述动态链接器使用其他的Symbol Table信息。
    用来描述Symbol Table的大小和位置,以及其他元数据。
LC_SYMTAB

用来描述该文件的符号表。不论是静态链接器还是动态链接器在链接此文件时,都要使用该load command。调试器也可以使用该load command找到调试信息。

  • symtab_command
    定义LC_SYMTAB加载命令的具体属性。
    /usr/include/mach-o/loader.h中的定义:
/*
 * 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 */
};
  • nlist
    定义符号的具体表示含义:
/*
 * Format of a symbol table entry of a Mach-O file for 32-bit architectures.
 * Modified from the BSD format.  The modifications from the original format
 * were changing n_other (an unused field) to n_sect and the addition of the
 * N_SECT type.  These modifications are required to support symbols in a larger
 * number of sections not just the three sections (text, data and bss) in a BSD
 * file.
 */
struct nlist {
    // 表示该符号在 string table 的索引
    union {
#ifndef __LP64__
        // 在 Mach-O 中不使用该字段
        char *n_name;    /* for use when in-core */
#endif
        // 索引
        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 */
    int16_t n_desc;        /* see  */
    uint32_t n_value;    /* value of this symbol (or stab offset) */
};
  • n_type
    1字节,通过四位掩码保存数据:
名字 含义
N_STAB(0xe0) 如果当前的n_type包含这3位中的任何一位,则该符号为调试符号表(stab)。在这种情况下,整个n_type字段将被解释为stab value。请参阅 /usr/include/mach-o/stab.h ,以获取有效的stab value
N_PEXR(0x10) 如果当前的n_type包含此位。则将此符号标记为**私有外部符号
__private_extern__(visibility=hidden) 只在程序内可引用和访问。当文件通过静态链接器链接的时候,不要讲其转换成静态符号(可以通过ld-keep_private_externs关闭静态链接器的这种行为)。
N_TYPE(0x0e) 如果当前的n_type包含此位。则使用预先定义的符号类型。
N_EXT(0x01) 如果当前的n_type包含此位。则此符号为外部符号;该符号在该文件外部定义或在该文件中定义,但可以在其他文件中使用。
  • N_TYPE
    N_TYPE字段的值包含:
名字 含义
N_UNDF(0x0) 该符号未定义。未定义符号是在当前模块中引用,但是被定义在其他模块中的符号。n_sect字段设置为NO_SECT
N_ABS(0x2) 该符号是绝对符号。链接器不会更改绝对符号的值。n_sect字段设置为NO_SECT
N_SECT(0xe) 该符号在n_sect中指定的段号中定义。
N_PBUD(0xc) 该符号未定义,镜像使用该符号的与绑定值。n_sect字段设置问NO_SECT
N_INDR(0xa) 该符号定义为与另一个符号相同。n_value字段是string table中的索引,用于指定另一个符号的名称。链接该符号时,此符号和另一个符号都具有相投的定义类型和值。
#define N_GSYM      0x20    /* 全局符号 global symbol: name,,NO_SECT,type,0 */
#define N_FNAME     0x22    /* procedure name (f77 kludge): name,,NO_SECT,0,0 */
#define N_FUN       0x24    /* 方法/函数 procedure: name,,n_sect,linenumber,address */
#define N_STSYM     0x26    /* 静态符号 static symbol: name,,n_sect,type,address */
#define N_LCSYM     0x28    /* .lcomm symbol: name,,n_sect,type,address */
#define N_BNSYM     0x2e    /* begin nsect sym: 0,,n_sect,0,address */
#define N_AST       0x32    /* AST file path: name,,NO_SECT,0,0 */
#define N_OPT       0x3c    /* emitted with gcc2_compiled and in gcc source */
#define N_RSYM      0x40    /* 寄存器符号 register sym: name,,NO_SECT,type,register */
#define N_SLINE     0x44    /* 代码行数 src line: 0,,n_sect,linenumber,address */
#define N_ENSYM     0x4e    /* nsect符号结束 end nsect sym: 0,,n_sect,0,address */
#define N_SSYM      0x60    /* 结构体符号 structure elt: name,,NO_SECT,type,struct_offset */
#define N_SO        0x64    /* 源码名称 source file name: name,,n_sect,0,address */
#define N_OSO       0x66    /* 目标代码名称 object file name: name,,(see below),0,st_mtime */
/*   historically N_OSO set n_sect to 0. The N_OSO
 *   n_sect may instead hold the low byte of the
 *   cpusubtype value from the Mach-O header. */
#define N_LSYM      0x80    /* 本地符号 local sym: name,,NO_SECT,type,offset */
#define N_BINCL     0x82    /* include file 开始 include file beginning: name,,NO_SECT,0,sum */
#define N_SOL       0x84    /* include file 名称 #included file name: name,,n_sect,0,address */
#define N_PARAMS    0x86    /* 编译器参数 compiler parameters: name,,NO_SECT,0,0 */
#define N_VERSION   0x88    /* 编译器版本 compiler version: name,,NO_SECT,0,0 */
#define N_OLEVEL    0x8A    /* 编译器 -O 级别 compiler -O level: name,,NO_SECT,0,0 */
#define N_PSYM      0xa0    /* 参数 parameter: name,,NO_SECT,type,offset */
#define N_EINCL     0xa2    /* include file 结束 include file end: name,,NO_SECT,0,0 */
#define N_ENTRY     0xa4    /* alternate entry: name,,n_sect,linenumber,address */
#define N_LBRAC     0xc0    /* 左括号 left bracket: 0,,NO_SECT,nesting level,address */
#define N_EXCL      0xc2    /* deleted include file: name,,NO_SECT,0,sum */
#define N_RBRAC     0xe0    /* 右括号 right bracket: 0,,NO_SECT,nesting level,address */
#define N_BCOMM     0xe2    /* 通配符开始 begin common: name,,NO_SECT,0,0 */
#define N_ECOMM     0xe4    /* 通配符结束 end common: name,,n_sect,0,0 */
#define N_ECOML     0xe8    /* end common (local name): 0,,n_sect,0,address */
#define N_LENG      0xfe    /* second stab entry with length information */

/*
 * for the berkeley pascal compiler, pc(1):
 */
#define N_PC        0x30    /* global pascal symbol: name,,NO_SECT,subtype,line */
  • n_sect
    整数,用来在指定编号的section中找到此符号;如果在该image的任何部分都找不到该符号,则为NO_SECT。根据sectionLC_SEGMENT加载命令中出现的顺序,这些section从1开始连续编号。
  • n_desc
    16_bit值,用来描述非调试符号。低三位使用REFERENCE_TYPE
名字 含义
REFERENCE_FLAG_UNDEFINED_NON_LAZY(0x0) 该符号是外部非延迟(数据)符号的引用。
REFERENCE_FLAG_UNDEFINED_LAZY(0x1) 该符号是外部延迟性符号(即对函数调用)的引用。
REFERENCE_FLAG_DEFINED(0x2) 该符号在该模块中定义。
REFERENCE_FLAG_PRIVATE_DEFINED(0x3) 该符号在该模块中定义,但是仅对共享库中的模块可见。
REFERENCE_FLAG_PRIVATE_UNDEFINED_NON_LAZY(0x4) 该符号在该文件的另一个模块中定义,是非延迟加载(数据)符号,并且仅对该共享库中的模块可见。
REFERENCE_FLAG_PRIVATE_UNDEFINED_LAZY(0x5) 该符号在该文件的另一个模块中定义,是延迟加载(函数)符号,仅对该共享库中的模块可见。
REFERENCED_DYNAMICALLY(0x10) 定义的符号必须是使用在动态加载器中(例如dlsymNSLookupSymbolInImage)。而不是普通的未定义符号引用。strip使用该位来避免删除那些必须存在的符号(如果该符号设置了该位,则strip不会剥离它)。
N_DESC_DISCARDED(0x20) 在完全链接的image在运行时,动态链接器有可能会使用此符号。不要在完全链接的image中设置此位
N_NO_DEAD_STRIP(0x20) 定义在可重定位目标文件(类型为MH_OBJECT)中的符号设置时,指示静态链接器不对该符号进行dead-strip(死代码剥离)。(⚠️ 注意:与N_DESC_DISCARDED(0x20)用于两个不同的目的。)
N_WEAK_REF(0x40) 表示此未定义符号是弱引用。如果动态链接器找不到该符号的定义,则将其符号地址设置为0。静态链接器会将此符号设置弱链接标志。
N_WEAK_DEF(0x80) 表示此符号为若定义符号。如果静态链接器或动态链接器为此符号找到另一个(非弱)定义,则弱定义将被忽略。只能将合并部分中的符号标记为弱定义。

如果该文件是二级命名two_level namespace image(即如果mach_header中设置了MH_TWOEVEL标志),则n_desc的高8位表示定义此未定义符号的库的编号。使用宏GET_LIBRARY_ORDINAL来获取此值,或者使用宏SET_LIBRARY_ORDINAL来设置此值。0指定当前的image
1到253根据文件中LC_LOAD_DYLIB命令的顺序表明库号。254用于需要动态查找的未定义符号(仅在OS X v10.3及以上版本中支持)。
255用来指定可执行image
对于flat namespace images,高8位必须是0。

two_levelnamespace & flat_namespace

上面提到了一级命名空间和二级命名空间,这里我们来看一下到底是什么意思。
链接器默认采用二级命名空间,也就是除了会记录符号名称,还会记录符号属于哪个Mach-O的。比如会记录下来_NSLog来自Foundation
那这样做有什么作用呢?
采用二级命名空间之后,还拿_NSLog来说,我们引入了Foundation,已经存在了_NSLog。但是我们可以在自己的工程中定义一个名字一样的符号。而且不会冲突。

你可能感兴趣的:(5、iOS强化 --- 链接与符号(补充内容))