iOS汇编教程(八)静态链接中的 Relocation - 静态库链接时是如何保证对变量的相对寻址依然正确的?

2,696 阅读7分钟

系列文章

  1. iOS汇编入门教程(一)ARM64汇编基础
  2. iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码
  3. iOS汇编入门教程(三)汇编中的 Section 与数据存取
  4. iOS汇编教程(四)基于 LLDB 动态调试快速分析系统函数的实现
  5. iOS汇编教程(五)Objc Block 的内存布局和汇编表示
  6. iOS汇编教程(六)CPU 指令重排与内存屏障
  7. iOS汇编教程(七)ARM Exclusive - 互斥锁与读写一致性的底层实现原理

简介

在 iOS 应用开发过程中,我们常常通过静态库方式引用一些闭源三方 SDK,在编译链接时静态库的代码段、数据段和符号表等会被拼接到 App 的主二进制中,在拼接过程中静态库内代码段与数据段的相对位置会发生改变,导致原来代码中的相对寻址不能正确指向链接后产物中的数据,这就需要在链接时根据静态库插入主二进制的情况对代码段进行修正,这一过程被称为 Relocation Fixup,它是保证静态链接后逻辑正确性的关键。

本文将介绍静态库的 Relocation 段,以及静态链接时基于 Relocation Info 对 __TEXT,__text 段进行 Fixup 的过程和原理。

一个例子

下面我们来看一个简单例子,我们先新建一个 iOS 工程命名为 SimpleApp,再新建一个 Static Library 的 Target 命名为 SimpleLib,静态库里只包含一个静态全局变量和一个读取全局变量的函数:

// SimpleLib.m
static int simple_val = 100;
int getSimpleVal() {
    return simple_val;
}

App 的主二进制链接了 SimpleLib,并通过 getSimpleVal 函数读取 SimpleLib 中的静态全局变量:

// ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    extern int getSimpleVal(void);
    printf("the value of simple val is %d\n", getSimpleVal());
}

如果仅仅在 SimpleLib 内部考虑,不考虑编译成静态库供外部使用,simple_val 变量的存取可以通过下面的方式实现:

.section    __TEXT,__text,regular,pure_instructions
.globl	_getSimpleVal
.p2align	2
_getSimpleVal:               
	adrp	x8, _simple_val@PAGE
	add	x8, x8, _simple_val@PAGEOFF
	ldr	w0, [x8]
	ret

.section    __DATA,__data
.p2align	2     
_simple_val:
	.long	100       

链接时的段重排

由于 simple_val 被定义在 __DATA,__data 段,在代码段通过 adrp & add 进行 PC-relative Addressing 来读取变量的值,这是毫无疑问的。

但问题是当 SimpleLib 被链接到 SimpleApp 时,代码段与数据段将会分别进行合并,也就是说 SimpleLib 原来的段之间发生了重排,这会导致 __TEXT,__text 段中的语句与 __DATA,__data 段中的数据之间的相对距离已经发生变化,原来编译出的 adrp & add 指令对已经失效,必须进行修正。

静态库的代码段

上文中的汇编代码是在不考虑编译成静态库的情况下生成的,下面我们将实际的 SimpleLib.a 反汇编,看一下 _getSimpleVal 的代码段内容。

最简单的办法是使用 LLVM 工具链中的 otool,在 Shell 中输入 otool -tv SimpleLib.a,下面是反汇编的结果:

(__TEXT,__text) section
_getSimpleVal:
0000000000000000	adrp	x8, 0
0000000000000004	add	x8, x8, #0x0
0000000000000008	ldr	w0, [x8]
000000000000000c	ret

可以看到 adrp & add 指令对操作的地址都被写成了 0,显然是无法正常取址的,这应该是系统考虑到静态库无法单独工作,因此不必再计算无谓的内部段之间的相对地址,将地址计算这一过程滞后到了链接时,具体的 Fixup 方式我们稍后会详细介绍。

链接后的变化

最后我们来观察一下静态库链接后主二进制中代码段的内容:

(__TEXT,__text) section
_getSimpleVal:
0000000100006a3c	adrp	x8, #0x10000c000
0000000100006a40	add	x8, x8, #0xd88
0000000100006a44	ldr	w0, [x8]
0000000100006a48	ret

可以看到这时的 adrp & add 已经被 Fixup 成了真实地址 0x10000cd88,为了验证地址的正确性,我们可以再通过 otool -dv 获取到 __DATA,__data 段的内容:

000000010000cd80	00000000 00000000 00000064

可以看到 cd80 中存储的 0x64 正是 simple_val 的初值 100,除了这种方式,我们还可以通过 MachOView 查看二进制的符号表来直观验证变量地址。

由此可见,静态库在连接时会对其代码段中涉及到 PC-relative Addressing 的部分进行 Fixup,那么链接器是如何知道哪些代码需要修正,以及如何完成修正呢?

静态链接中的 Fixup

Relocation 段

我们通过 MachOView 打开 SimpleLib.a 可以发现在静态库的二进制中包含了一个名为 Relocation 的 Section,打开它的内容如下:

根据 mach-o/reloc.h 中的结构体定义:

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

我们可以知道每个 Relocation Info 的长度为 8 字节,前 4 字节指明了要 Fixup 的段偏移,后 4 个字节指明了 Fixup 方式,例如上图中的例子,指明了要对 __TEXT,__text 中偏移量为 +0 和 +4 的语句进行 Fixup,且 Fixup 对应的目标符号在符号表中的索引号为 #1,我们打开符号表可以看到,#1 正式 simple_val 这个变量的符号。

修复MachOView 的显示问题

有可能你在自己电脑上用 MachOView 查看 Relocation 段时发现内容缺失,这似乎是 MachOView 的一个 bug,这里有一个简单的修复办法,但只能保证你正常查看 Symbol Index 和 Fixup Address,其他功能不一定正常,因此只是个临时方案

打开 MachOView 工程,搜索方法 createReloc64Node,可以看到在方法的开头对 relocation_info->r_address 进行了解析,随后的复杂解析可能由于系统升级等原因导致了兼容异常,我们可以在 if (relocation_info->r_extern) 语句前插入对 Symbol Index 的解析并 continue 来临时解决 Relocation 段的查看问题:

[node.details appendRow:[NSString stringWithFormat:@"%.8lX", range.location]
                           :lastReadHex
                           :@"Address"
                           :[NSString stringWithFormat:@"0x%qX", relocation_info->r_address + baseAddress]];

// read the second half of the entry
[dataController read_uint32:range lastReadHex:&lastReadHex];
  
/** INSERT CODE BEGAN */
[node.details appendRow:[NSString stringWithFormat:@"%.8lX", range.location]
                                                  :lastReadHex
                                                  :@"Symbol Index"
                                                  :[NSString stringWithFormat:@"%d", relocation_info->r_symbolnum]];
  
continue;
/** INSERT CODE END */

//========================================================================
if (relocation_info->r_extern) {
// omit codes ...

基于 Relocation 段的 Fixup

Relocation 中的关键信息主要有三部分:段、段偏移和对应的符号。

定位段的 Relocation Info

Load Commands 中每个段都有其对应的 Relocation Offset & Number of Relocations 来声明段是否有对应的 Relocation Fixup,例如 __TEXT,__text 段的 Section Header 内容如下:

它指明了代码段的 Fixup 位于二进制偏移量 2980 处,转化为十六进制为 0xBA4,需要注意这个值需要加上 SimpleLib.o 的基础偏移量 0xC8,最后结果为 0xBA4 + 0xC8 = 0xC6C,与上文中给出的代码段 Relocation 偏移量一致。

执行 Relocation Fixup

每一个 Relocation Entry 对应的是相应段中的一个单元,以代码段为例,每一个 Relocation Entry 对应的是一条指令,我们上面的取值包含两条指令:

0000000000000000	adrp	x8, 0
0000000000000004	add	x8, x8, #0x0

因此需要对代码段 +0 和 +4 两处进行 Fixup,因此包含了 2 个 Relocation Entry 如下图所示:

这两条 Relocation Info 指明了代码段 +0 和 +4 两条指令是对 #1 符号的寻址操作,需要在静态链接时对他们进行 Fixup,Fixup 过程大致为重新计算链接后数据段中 #1 符号的新地址,并将其的高 21 和低 12 位拆开,分别对 adrp 和 add 指令进行 Fixup,如果你对 Fixup 的具体过程感兴趣,可以去 LLVM 源码中查找链接的相关逻辑阅读。

总结

由于静态链接时需要将静态库的各段拆分合并到主二进制中,导致了需要对静态库内 PC-relative Addressing 进行 Relocation,Relocation Info 被包含在静态库内,在链接时链接器会根据 Relocation Info 来 Fixup 拼接到主二进制的静态库代码。同样的在动态库链接时也有运行时的 Rebase & Bind 过程,将在后面的文章中介绍。