之前我们在讲链接与符号的时候提到了静态链接和动态链接,本章我们来详细的梳理一下静态链接
接下来我们用实例来讲解一下:代码如下:
// 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
架构,链接的过程也是有动态库(系统库)参与的,这里我们只讨论静态链接。)
这里我们先认识两个概念:模块
& 符号
:
对于符号
在前面的文章中已经做了介绍,这里我们就再简单的讲一下:
名字 | 解释 |
---|---|
模块 |
可以理解为一个源代码文件就是一个模块,比如上面的a.c & b.c 。我们在实际的开发中,一般来讲一个类在一个源文件上,就形成了一个模块。模块化的好处就是易于复用和维护,再一点就是在编译的时候,未改动的模块不用从新编译,直接用之前编译好的缓存就行。 |
符号 | 简单理解就是函数名和变量名,比如上面的main 、global_var 、func |
空间和地址分配
相似段合并
- 静态链接:将多个目标文件合并成一个可执行文件。
在这个过程中,把多个目标文件里面相同性质的段合并到一起。
比如我们来合并a.o
&b.o
,生成ab
(Mach-O);在合并的过程中,a.o
&b.o
里面的数据段一起合并成ab
里面的数据段;同理,数据段也是一样的。
两步链接
第一步:空间与地址分配
扫描所有的输入目标文件,并且获得他们各个段的长度、属性和位置,将输入目标文件中的符号表中所有的符号定义
和符号引用
收集起来,统一放到一个全局符号表中。这一步中,链接器能够获得所有的输入目标文件的段的长度,将它们合并,计算出输出文件中各个段合并后的长度和位置,并建立映射关系。-
第二步:符号解析和重定位
使用上面第一步收集到的信息,读取输入文件中段的数据、重定位信息,并且进行符号解析和重定位,调整代码中的地址等。重定位
a
模块使用了global_var
和func
两个符号,那么怎么知道这两个符号的地址呢?
我们来看一下a.o
文件:
global_var
(地址0x28) 和func
(地址0x3C)的地址都是假地址。编译器暂时用假地址替代,把真正的地址计算工作留给链接器。通过前面的空间与地址分配可以得知,链接器在完成地址与空间分配之后,就可以确定所有符号的虚拟地址了。也就是说,链接器可以根据符号的地址对每个需要重定位
的指令进行地址修正。
我们再来看一下ab
可执行文件:
可以看到global_var
(地址:0x100007000,指向data
段,值为1) 和func
(地址: 0x100007F90,指向func
函数地址)都是真实的地址。重定位表
链接器是如何知道a
模块里面有哪些指令被调整,这些指令该如何调整。
这是因为:在a.o
里面,有一个重定位表
,专门保存这些与重定位相关的信息。而且每个section
的section_64
的header
的reloff
(重定位表里的偏移) 和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_address
和 r_length
可以让我们知道要重定位的字节;r_symbolnum
(当为外部符号时)是符号的index。
我们看一Relocation
里面的内容:
下面我们去
Symbol Table
里面找对应的符号:
通过上面可以看到,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
告诉链接器符号表的位置(偏移)和个数;stroff
和strsize
告诉字符串表的位置和大小。这个我们在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
代表符号的地址值。
符号解析
为什么要链接呢?
因为一个模块(a
模块)可能引用了其它模块(b
模块)的符号,所以需要把多有的模块(.o
目标文件)链接在一起。
重定位就是:链接器会去查找由所有输入的.o
目标文件的符号组成的全局符号表,找到相应的符号后进行重定位。
⚠️ 其中两个常见的错误:
1、ld: dumplicate symbols
:多个目标文件里面有相同的符号,导致全局符号表出现多个一样的符号。
2、Undefined symbols
:需要重定位的符号,在全局符号表里面没有找到(一个符号:有引用,未定义)。
静态库链接
一个静态库可以简单看做一个目标文件的合集,也就是多个目标文件经过压缩合并形成的一个文件。
而静态库链接,就是将自己的目标文件与静态库里面的某个模块(用到的一个或多个目标文件)链接成可执行文件。
静态库一般包含多个目标文件,可是链接器在链接静态库的时候是以目标文件为单位的。假设我们把所有的函数放在一个目标文件里面,而我们只用到了一个函数,此时却把很多没有用到的函数一起链接到了可执行文件里面。
静态库链接示意图:
参考文档:https://juejin.cn/post/6844903912198127623