一个iOS程序员的自我修养(三)Mach-O文件静态链接

一个iOS程序员的自我修养(三)Mach-O文件静态链接

IOS小彩虹2021-08-21 2:04:47180A+A-

上文分析了 Mach-O 文件的整体结构,那么 Mach-O 文件是怎么来的呢?其中一个重要的过程就是静态链接,链接器将所有输入的 “.o” 文件打包输出可执行文件,可以简单理解这个可执行文件就是 Mach-O 文件,因为本篇主要分析静态链接,所以暂且理解为静态链接后生成了最终的可执行文件。

假设我们只有两个模块,“a.c” 和 “b.c”,它们的代码定义如下:

/* a.c */
extern int shared;
int main() {
    int a = 100;
    swap(&a, &shared);
}

/* b.c */
int shared = 1;
void swap( int* a, int* b) {
    *a ^= *b ^= *a ^= *b;
}

以 x86 架构为例,首先使用 clang 命令将 “a.c” 和 “b.c” 分别编译成目标文件 “a.o” 和 “b.o”:

clang -fmodules -c a.c b.c -o a.o b.o

经过编译后,我们就得到了 a.o b.o 这两个目标文件。从代码可以看到,b.c 中共定义了两个全局符号,“shared” 和 “swap”,a.c 里面定义了一个全局符号 “main”,a.c 里面引用到了 b.c 里面的 “shared” 和 “swap”,接下来要做的就是把 a.o 和 b.o 两个目标文件链接成 “ab” 可执行文件。

函数和变量被统称为符号。

空间与地址分配

链接器会先扫描所有输入的目标文件,获取它们各个段的长度、属性和位置,将所有的符号表收集起来统一放到一个全局符号表。这一步链接器将所有目标文件进行段合并,计算出合并后的长度和位置,建立映射关系。

实际上目标文件与可执行文件 Mach-O 结构一致。

符号解析与重定位

在分析符号解析与重定位之前先看看 a.o 里面是怎么使用两个外部符号 “shared” 和 “swap” 的: 利用 MachOView 工具可以看到 a.o 的代码段反汇编结果:

最左边的那列是每条在虚拟内存中的偏移量,每一行代表了一条指令。红框标出的就是两个引用了 “shared” 和 “swap” 的位置,其中 shared 使用 mov 指令,这条指令占用了 3 个字节,swap 调用使用的是 call 指令,其中的 0x488B35 和 0xE8 操作码都是近址相对位移调用指令,后面的四个字节就是被调用函数相对于调用指令的下一条指令的偏移量。实际上 0x1E3 和 0x1F5 存放的只是 “shared” 和 “swap” 的临时假地址,因为编译器在编译的时候并不知道它们的真正地址。编译器将这两条指令的地址暂时用 0x00000000 代替着。链接器在空间与地址分配后就可以确定所有符号的虚拟地址了,之后对每个需要重定位的指令进行修正。下面将 a.o b.o 链接成可执行文件 ab:

clang a.o b.o -o ab

再对 ab 进行反汇编看一下链接后的代码段和 a.o 对比一下变化:

经过修整后,“shared” 和 “swap” 的地址分别为 0x000000A1 和 0x0000000F (小端模式),以 swap 为例,这个 call 指令是一条近址相对位移调用指令,它后面跟的是相对于下一条指令 xor 的偏移量,也就是 0xF71 + 0x0F 求和结果正好是 0xF80,0xF80 刚好是 swap 函数的地址。

重定位表

那么链接器怎么知道哪些指令需要被调整呢?事实上在目标文件中有个重定位表,专门保存与重定位相关的符号,它被定义在了目标文件的 Relocations 段中。a.o 的 Relocations 段定义如下:

每个要被重定位的地方叫做一个重定位入口,可以看到 a.o 里面在 __TEXT,__text 段有两个重定位入口,对照前面 a.o 的反汇编分析,这里的 0x1D 和 0xB 正好是代码段中的 call 指令和 mov 指令的地址部分。

重定位表可以理解为是一个装有重定位入口的数组,重定位入口的结构体被定义在了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 */
};

两个重要的字段 r_addressr_symbolnum,r_address 对应该符号在该段中的偏移,通过 r_address 就可以找到要重定位的位置,r_symbolnum 对应该符号在符号表中的下标,通过 r_symbolnum 就可以找到该符号在符号表中的位置。

符号解析

通常观念里,之所以要链接是因为目标文件中用到的符号被定义在了其他目标文件中,所以要将他们链接起来,例如我们直接链接 “a.o” 链接器发现 shared 和 swap 两个符号未被定义,没有办法完成链接工作:

Undefined symbols for architecture x86_64:
  "_shared", referenced from:
      _main in a.o
  "_swap", referenced from:
      _main in a.o
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)

这也是编写程序遇到最多的错误之一,就是链接时符号未定义。从程序员的角度来看,符号解析占据了链接过程的主要内容。

其实重定位过程也伴随着符号解析过程,每个目标文件都可能定义一些符号或者引用到定义在其他目标文件中的符号,例如 a.o 引用到了 b.o 的 “shared” 和 “swap”。符号都被定义在了符号表数组中,它的结构体定义在 mach-o 下的 loader.h 中:

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 <mach-o/stab.h> */
    uint64_t n_value;      /* value of this symbol (or stab offset) */
};
  • n_strx:字符串表中的下标。
  • n_sect:第几个 section。
  • n_value:符号地址。

比如 a.o 的符号表:

可以看到除了 main 函数定义在了代码段之外,其他符号都是 N_UNDF 类型,即 “undefined” 类型,实际上这种未定义的符号都能够在重定位表中找到。在链接器扫描完所有的输入文件后,这些未定义的符号都应该能够在全局符号表中找到,否则链接器就会报符号未定义错误。可通过下图对比出链接前后,符号表中“shared”和“swap”符号的变化:

目标文件 a.o 中未定义的类型在链接成可执行文件 ab 后,未定义的部分就都变得有值了。

重定位

重定位时,重定位表中的每个重定位入口的 r_address 都是对一个符号的引用,当链接器对引用的某个符号进行重定位时,它就要确定这个目标符号的地址,这时候就会通过重定位入口的下标 r_symbolnum 去全局符号表中查找这个符号,找到后再将这个符号的地址按照一定的规则(例如相对位移调用指令的方式),回填入调用该符号的位置,这样一个重定位过程就结束了。

静态插桩 hook objc_msgSend 分析

所谓静态插桩就是在静态链接期间实现 objc_msgSend 方法替换。具体实现方案就是在主工程用汇编的方式实现 hook_msgSend 函数,再将静态库中字符串表中的 objc_msgSend 替换为 hook_msgSend,例如替换某个 Pod 库中的 objc_msgSend,用来监控 OC 方法调用。

字符串表

每个目标文件或者说静态库里面都有专门用来为符号表服务的字符串表,存储着比如段名、变量名、函数名等。因为字符串的长度是不定的,所以没有像符号那样的结构体来表示它。所有的字符串都被集中起来存放到一个表中,然后使用字符串在表中的偏移来引用字符串,符号表正是通过这个偏移 n_strx 值来索引到它的符号名称。

我们还是以 ab 可执行文件的 main 函数为例,通过 MachOView 分析下符号表通过索引查找对应符号名的过程:

红框内的值就是符号表中的 n_strx,符号表中 main 符号在字符串表中的偏移为 0x16,换算成十进制是 22。然后在看一下字符串表中的结构: 红框内的 16 进制翻译成 ASCII 正好是 _main 字符串,在字符串表中的偏移也正好是 22 位,这也对应上了在符号表中的偏移。

因为 main 函数并非定义在外部,所以它的 value 是有值的,如果是一个外部函数,例如 objc_msgSend 这个 value 会是 0,因为 objc_msgSend 是属于 runtime 的库函数,这是一个动态库,动态库中函数地址的确定是在动态链接的时候进行绑定的,关于动态链接后面章节会讲到。在生成可执行文件 Mach-O 时 objc_msgSend 的真实地址是未知的,在静态链接的过程不会对这个符号进行重定位。如果在主工程和静态库链接前,将 objc_msgSend 修改为 hook_msgSend 字符串,链接后符号表中的 value 就变成了 hook_msgSend 函数的地址。

因为静态库本身就是一组目标文件的集合,静态库与库之间在链接的过程与目标文件之间的链接并无二异。通过上面的分析我们可以知道,当目标文件也就是 .o 文件引用了外部符号后,这些外部符号在全局符号表中的状态都是 N_UNDF 类型,并且同时会在重定位表中增加这个符号的重定位入口。 在空间与地址分配后,符号表中的未知符号在虚拟地址中的偏移也就随之确定了。在这之后会遍历所有的重定位入口,对需要重定位的位置进行修正,也就是将调用 objc_msgSend 指令的地方都修正为对 hook_msgSend 的调用。

关于具体代码实现可以参考这个开源工具: KKMagicHook

参考

《程序员的自我修养》

juejin.cn/post/684490…

github.com/maniackk/KK…

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们