上一篇文章4、iOS强化 --- 链接与符号(Symbol)中我们提到了链接
链接的本质就是把多个目标文件组合成一个文件
但是有一些地方说的不够详细,这里我们做一下补充。
首先我们知道,在生成.o
目标文件的过程中,链接器 (llvm-ld) 并没有被执行,这个过程就是编译的过程。
那么llvm-ld
在哪里执行呢?
答案:在最终生成Mach-O
文件的过工程中被执行。
过程如下:
1、多个目标文件合并
2、符号表(包括重定位符号表)合并成一张表
3、生成一个Mach-O
可执行文件
注意⚠️ :上面从生成.o
到生成Mach-O
这整一个过程才是一个完整的链接过程
- 链接有又分为静态链接 和 动态链接
静态链接
-
静态链接:将多个目标文件合并成一个可执行文件。这个过程中会把多个目标文件相同性质的段合并到一起。比如
a.o
和b.o
合并成可执行文件ab
;其过程就是将a.o
里面的代码段和b.o
里面的代码段合并到一起,数据段同理等等。
-
静态库链接
一个静态库可以简单看做一个目标文件的合集,也就是多个目标文件经过压缩合并形成的一个文件。
而静态库链接,就是将自己的目标文件与静态库里面的某个模块(用到的一个或多个目标文件)链接成可执行文件。
动态链接
既然有了静态链接,那么动态链接又是怎么回事呢?
通过上面我们知道,静态链接是将多个目标文件合并成一个可执行文件,如果遇到静态库那就将静态库也合并进去。这样在实际开发中就会有一个问题:
比如:我们开发中经常会用到的Foundation
框架(Apple提供的),如果采用静态链接的方式,那么市面上所有用到它的APP都要在自己的Mach-O
文件中集成它。试想一下,一旦该库出现问题,那所有用到它的APP都要从新集成,从新上架。这样做不仅让APP的体积变大,而且还极不方便。
因此,动态链接应运而生。
只需要在手机端存放Foundation
框架,当APP运行的时候,发现使用到了Foundation
,这时候再去链接,加载到内存中。这样就方便很多了。即使Foundation
出问题,那么直接更新Foundation
就可以了,并不需要同时更新APP。
动态链接是在运行时由dyld(动态链接器)
动态加载的。
- 静态链接 与 动态链接 的加载过程
可执行文件的调用过程
- 1、调用
fork
函数,创建一个process
- 2、调用
execve(程序加载器)
或其衍生函数,在该进程上加载,执行我们的Mach-O
文件
当我们调用execve(程序加载器)
内核实际上在执行以下操作:
①:将文件加载到内存
②:开始分析Mach-O
中的mach_header
,以确认它是有效的Mach-O文件
如何查看链接器的参数
在终端使用下面的命令可以查看链接器的参数
man ld
使用
/
+ 要查找的关键字
即可进行检索。
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
。根据section
在LC_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) |
定义的符号必须是使用在动态加载器中(例如dlsym 和NSLookupSymbolInImage )。而不是普通的未定义符号引用。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
。但是我们可以在自己的工程中定义一个名字一样的符号。而且不会冲突。