我正在参与「启航方案」

最近在三刷《程序员的自我涵养:链接、装载与库》,为了加深关于相关常识的了解,我又阅读了 fishhook 的源码。本文希望从程序的链接原理出发,详细介绍 fishhook 的规划原理,学习其间的规划思维。

概述

Fishhook 是 Facebook 开源的一款面向 iOS/macOS 渠道的 符号动态重绑定 东西,答应开发者在运转时修正 Mach-O 中的符号(函数),然后完结 动态库 的函数 hook 才能。

Fishhook 供给了两个用于符号重绑定的接口,分别是:

int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
int rebind_symbols_image(void *header,
                         intptr_t slide,
                         struct rebinding rebindings[],
                         size_t rebindings_nel);

其间,rebind_symbols 能够在一切动态库规模内进行符号重绑定,而 rebind_symbols_image 则限制了动态库的规模,只能指定某一个动态库。

这里,咱们先预设几个问题,后面会逐渐进行回答:

  • 问题一:fishhook 是在什么时候完结函数 hook 的?
  • 问题二:fishhook 为什么只支撑 hook 动态库函数?

为了能介绍清楚 fishhook 的完结原理,本文我将要点介绍程序的链接原理,包含:静态链接、动态链接。其间,触及到的术语和概念首要是依据 ELF 可履行文件(或方针文件),在真实介绍 fishhook 的原理时,我会将 Mach-O 中的术语与 ELF 进行比较和映射,然后达到一个举一反三的作用。

可履行文件格局

在介绍链接原理之前,咱们有必要先了解一下可履行文件(方针文件)的根本格局,不同的渠道有着不同的格局,分别是:

  • 关于 Windows 渠道,其采用的是 PE(Portable Executable) 格局
  • 关于 Linux 渠道,其采用的是 ELF(Executable Linkable Format) 格局
  • 关于 iOS/macOS 渠道,其采用的是 Mach-O(Mach Object) 格局

虽然不同渠道的可履行文件格局不同,可是它们的组织结构和规则是根本相似的。如下图所示,不同格局的可履行文件根本都包含如下几个部分:

  • 文件头
  • segment 表
  • section 表
  • section 数据

如何从链接原理的角度理解 fishhook 的设计思路?

文件头用于描绘可履行文件的元信息,包含:文件类型、系统版别、segment 表的方位和巨细、section 表的方位和巨细等等。Section 表实质上是一个索引表,其存储了每一个 section 的元信息,比方对应 section 在文件中的方位和巨细。至于 section,它是可履行文件的根本组成单元,常见 section 有:.text.data.bss.symtab.strtab 等。

那么 segment 表的作用又是什么呢?

section 与 segment

事实上,两者的差异首要在于:section 用于描绘可履行文件的静态存储布局,segment 用于描绘可履行文件的装载内存布局。

咱们知道可履行文件是以 section 为根本单元存储的,section 的类型十分多,如:.data.text.rodata 等。假如,咱们的可履行文件中有两个 section,分别是 .init.text,两者的巨细分别是 3500B 和 4100B。假设系统的页面巨细为 4KB,咱们来分别看一下依据 section 装载和依据 segment 装载的内存占用状况。

下图右部所示为依据 section 装载的内存占用状况,其间 .init 单独占用一个页,且页没有全部运用;.text 会单独占用两个页,且第二页绝大多数内存空间没有运用,一共糟蹋内存 3 x 4KB – 3500B – 4100B = 4688B。

下图左部所示为依据 segment 装载的内存占用状况,.text 占用了两个页,且与 .init 同享了一个页,一共糟蹋内存 2 x 4KB – 3500B – 4100B = 592B。

如何从链接原理的角度理解 fishhook 的设计思路?

很显然,相比于依据 section 装载,依据 segment 装载关于内存占用的优化十分显着,内存碎片更少。在实践中,程序在装载时会将相同权限的 section 兼并在一个 segment 中,比方:.init.text 都兼并成为可读可履行权限的 segment,作为代码段;可读可写的 section 兼并在为一个 segment,作为数据段。

程序的链接原理

链接(Linking) 的实质是把多个方针文件相互拼接到一同,使得函数调用、变量拜访等指令能够找到正确的内存地址。然而,这一切都是围绕着 符号(Symbol) 完结的。

那么究竟什么是符号?举个比如,方针文件 B 调用了方针文件 A 中的函数 foo。对此,咱们以为方针文件 A 界说了函数 foo,方针文件 B 引证了函数 foo。在链接进程中,咱们将函数和变量统称为 符号(Symbol),函数名和和变量名统称为 符号名(Symbol Name)。因而,咱们也能够以为方针文件 A 包含了函数 foo符号界说(Symbol Definition),方针文件 B 包含了函数 foo符号引证(Symbol Reference)

这时候问题来了,链接进程是怎么依据符号完结对二进制指令中内存地址的批改呢?对此,咱们能够先来了解一下静态链接。

静态链接

静态链接会在编译期将多个方针文件兼并为一个可履行文件。因而,里面包含了一切的符号、重定位项、字符串等。

在编译进程中,编译器会为每一个变量或函数生成一个符号项,符号项包含的信息首要有:

  • 符号名:即一个指向字符串表的索引,比方:字符串 foo 在字符串表中的偏移量
  • 符号类型:类型有许多,比方:大局符号、部分符号、未界说符号等。
  • 符号值符号界说 的内存地址,用于批改二进制指令中的内存地址。这个地址批改的进程被称为 重定位

此外,编译器还会为每个变量引证或函数引证生成一个重定位项。由于每一个重定位项记录了每一次关于符号的引证,因而,咱们能够将其称为符号引证项。这样也就构成了符号界说和符号引证的一对多联系,究竟,咱们能够在不同的当地引证同一个变量或函数。

依据如下示意图,静态链接的整体作业原理大约能够分为以下三个进程:

  • 依据重定位项中的符号索引,去符号表找到对应的符号项,并获取到对应符号的符号值,即内存地址。
  • 依据重定位项中的重定位地址,找到代码段中对应的字节地址,将其批改为进程一获取到的内存地址。
  • 遍历重定位表中的一切重定位项,重复进程一和进程二。

如何从链接原理的角度理解 fishhook 的设计思路?

由于静态链接时,程序所依靠的一切方针文件都已经兼并在了一个可履行文件中,因而几乎不存在符号项中的符号值(内存地址)不确认的状况,对此,静态链接器只需求依据重定位表进行重定位即可。这其实便是咱们常说『静态链接的要点是重定位』的原因。

动态链接

动态链接的根本思维是 将程序按照模块拆分红各个独立的部分,在运转时将它们链接在一同构成一个完结的程序,而不是像静态链接相同在编译时把一切的模块都链接成一个独立的可履行文件。因而,动态链接能够有用处理静态链接存在的 内存空间糟蹋程序更新困难 的问题。

那么关于动态链接,咱们是否能够直接采用静态链接的做法呢?这种方案理论上能够,但却不是最优解,由于静态链接会修正代码段,咱们很难让同享方针在被屡次重定位之后也能持续安全安稳的运转。

举一个比如,如下所示,一个动态同享方针 X 内部会引证外部的一个变量 a。当程序 A 与动态同享方针 X 完结重定位后,X 代码段中的某个指令的访存地址可能是一个值;当程序 B 与动态同享方针 X 完结重定位后,X 代码段中同方位的访存地址可能会被修正成另一个值。这时候,必然会出现其他程序无法正常履行的状况。

如何从链接原理的角度理解 fishhook 的设计思路?

关于怎么处理多进程之间的重定位抵触问题,咱们能够引证一句经典名言来描绘动态链接的处理方案。

计算机领域中的任何问题都能够经过添加一个中心层来处理

当然,在具体的完结中,动态链接依据链接的机遇,还能够分为 装载时链接(Load-Time Linking)推迟链接(Lazy Linking)。两者的完结思路只要略微的差异,下面咱们将分别进行介绍。

装载时链接

下图所示为装载时链接的作业原理示意图。关于同享方针而言,其代码段会被多个进程所同享,因而不能直接在代码段中进行重定位,修正内存地址。考虑到多进程同享方针时,同享方针会为每个进程拷贝一份数据段,支撑修正。因而,一种称为 地址无关代码(PIC,Position-Independent Code) 的技能诞生了,其根本思维是:在编译时装备 PIC 编译选项,将指令部分中需求被修正的部分分离出来,跟数据部分放在一同。这样指令部分能够坚持不变,而数据部分能够在每个进程中有一个独立的副本。

关于 PIC 技能,代码运转性能会比静态链接要差一点。由于指令在拜访外部变量或外部函数时,必须先经过指针去数据段找到对应的方位,再从中取出真实的内存地址,很显然多了一次直接操作,损耗了性能。

如何从链接原理的角度理解 fishhook 的设计思路?

在装载前,同享方针 X 的符号表中的外部符号 bar 的内存地址是未界说的。可是,程序 A 的符号表中的符号 bar 的内存地址是确认的(由于符号 bar 的符号界说坐落程序 A 中)。因而,在装载时咱们就能够决议出同享方针 X 的外部符号 bar 的地址。这个进程,咱们称之为 装载时绑定(Load-Time Binding)装载时符号绑定(Load-Time Symbol Binding)

当外部符号 bar 的内存地址绑定完结后,咱们就能够进行后续的重定位了。其进程和静态链接的重定位相似,首要包含以下几步:

  • 依据动态重定位项中的符号索引,去动态符号表中找到对应的符号项,并获取对应符号的符号值,即装载时绑定的内存地址。
  • 依据动态重定位项中的重定位地址,找到 数据段 中对应的字节地址,将其批改为进程一获取到的内存地址。
  • 遍历动态重定位表中的一切重定位项,重复进程一和进程二。

在 PIC 技能中,编译器会在数据段中为每一个符号存储一个占位桩(stub),用于存储符号的真实内存地址。这些占位桩组成了一个表,咱们称之为 大局偏移表(GOT,Global Offset Table)

综上述能够看出,装载时链接包含了两个重要的进程,分别是装载时绑定和重定位。虽然中心多了一步直接索引内存地址,损耗了一些性能,可是程序的灵活性和复用性供给了许多。

推迟链接

考虑到程序运转的部分性,实践上在进程生命周期中许多变量或函数并不会被调用。所以,诞生了推迟链接技能,能够支撑进程只在第一次调用符号时才进行链接。

下图所示为推迟链接的作业原理示意图,实质上与装载时链接差不多,首要差异在于:装载时链接在数据段中运用了 GOT 存储符号地址,推迟链接则在数据段中运用了 进程链接表(PLT,Procedure Linkage Table) 存储符号地址。当 PLT 表项中符号的内存地址未决议时,PLT 表项中的占位桩(stub)存储的是一段代码的地址。当这段代码完结符号绑定和重定位后,会将符号的真实内存地址回填到占位桩中,掩盖默许的代码地址,然后完结仅在第一次调用符号时才进行链接。

如何从链接原理的角度理解 fishhook 的设计思路?

推迟链接的关键是怎么完结在第一次调用符号时进行链接,这个进程包含了 推迟绑定(Lazy Binding) 和重定位。关于 PLT 的存储,许多方针文件会将其存储在命名为 got.plt 的 section 中,Mach-O 和 ELF 都是如此,这一点需求注意。

Fishhook 完结原理

中心思维

上述介绍了程序的链接原理,尤其是有了解了动态链接之后,如果你细想考虑一下,很简单就能想到 fishhook 的规划思维。

下图展示了 fishhook 的规划思维,十分简单巧妙,中心思维便是 将方针符号(函数)对应的 GOT 表项或 PLT 表项中存储的符号值(内存地址),替换成 hook 函数的内存地址。经过这种办法,无论是装载时链接还是推迟链接,咱们都能够完结对动态同享库函数的 hook。

如何从链接原理的角度理解 fishhook 的设计思路?

下面,咱们来介绍一下 fishhook 完结细节中与 Mach-O 的相关概念。

Non-lazy Symbol Pointer & Lazy Symbol Pointer

如下所示为《Mach-O Programming Topics》中对两者的解释:

Non-lazy symbol references are resolved (bound to their definitions) by the dynamic linker when a module is loaded.A non-lazy symbol reference is essentially a symbol pointer—a pointer-sized piece of data. The compiler generates non-lazy symbol references for data symbols or function addresses.

Lazy symbol references are resolved by the dynamic linker the first time they are used (not at load time). Subsequent calls to the referenced symbol jump directly to the symbol’s definition.Lazy symbol references are made up of a symbol pointer and a symbol stub, a small amount of code that directly dereferences and jumps through the symbol pointer. The compiler generates lazy symbol references when it encounters a call to a function defined in another file.

Non-lazy Symbol Pointer 存储的是指向符号界说的指针,它与 GOT 中的表项界说十分相似。由 Non-lazy Symbol Pointer 组成的表,在 Mach-O 中咱们称为 Non-lazy Symbol Pointer Table。

Lazy Symbol Pointer 包含一个指向符号界说的指针、一个占位桩以及一段代码(可用于推迟绑定和重定位),它与 PLT 中的表项界说十分相似。由 Lazy Symbol Pointer 组成的表,在 Mach-O 中咱们称之为 Lazy Symbol Pointer Table。

Indirect Symbol Table

上述的 Non-lazy Symbol Pointer 和 Lazy Symbol Pointer 并没有包含符号名相关的信息,然而在实践的符号查找、绑定的进程是需求用到的。因而,关于 Non-lazy Symbol Pointer Table 和 Lazy Symbol Pointer Table 各自有一个同步的直接符号表,能够用于配合完结链接作业。Fishhook 也是借助 Indirect Symbol Table 直接获取符号名,然后与方针符号进行判等比较,然后最终完结 hook 作业。

Indirect Symbol Table 与 Symbol Pointer Table 的表项是一一对应的,比方:Indirect Symbol Table 中第 1601 项存储的便是 Symbol Pointer Table 中第 1601 项的符号索引,如下图所示。

如何从链接原理的角度理解 fishhook 的设计思路?

Symbol Pointer 方针符号地址替换

Fishhook 的中心是 完结 Symbol Pointer 的地址替换,无论是 Non-lazy Symbol Pointer 还是 Lazy Symbol Pointer。其完结的关键进程首要包含以下几步:

  • 查找数据段,即 SEG_DATASEG_DATA_CONST
  • 在数据段中查找 LAZY_SYMNBOL_POINTERSNON_LAZY_SYMBOL_POINTERS 类型的 section
  • 分别对 LAZY_SYMBOL_POINTERSNON_LAZY_SYMBOL_POINTERS section 进行 Symbol Pointer 方针符号地址替换

Symbol Pointer 方针符号地址替换的进程首要有以下几步:

  • 依据 LAZY_SYMBOL_POINTERSNON_LAZY_SYMBOL_POINTERS section 获取其对应的 Indirect Symbol Table
  • 遍历 section,同步遍历 Indirect Symbol Table,获取对应的符号名
  • 遍历进程中,判断符号名是否与方针符号名匹配。如果匹配,则将 Symbol Pointer 的符号地址替换成 hook 函数的地址;不然,持续遍历,直到结束。

这里触及到了 fishhook 中的两个函数完结,分别是 rebind_symbols_for_image 函数和 perform_rebinding_with_section 函数,有兴趣的朋友能够自行阅读,本文就不张贴代码了。

总结

至此,咱们从链接原理的角度介绍了 fishhook 的规划思路。经过这种自顶向下的办法来剖析,咱们很快就能够联想到怎么去完结一个针对 ELF 格局的 hook 东西。

最后,咱们再来回忆一下本文开头预留的几个问题。

问题一:fishhook 是在什么时候完结函数 hook 的?fishhook 会在调用 rebind_symbolsrebind_symbols_image 办法时去遍历镜像,然后完结对方针符号的地址替换。

问题二:fishhook 为什么只支撑 hook 动态库函数?动态库的 PIC 技能支撑在数据段进行重定位,因而答应咱们进行方针地址修正。而 fishhook 的整个机制便是建立在动态链接原理的基础上,因而进支撑 hook 动态库函数。

参考

  1. 怎么从链接原理的角度了解 fishhook 的规划思路?
  2. 《程序员的自我涵养:装载、链接与库》
  3. OS X ABI Mach-O File Format Reference
  4. Mach-O Programming Topics
  5. fishhook
  6. BSD Library Functions Manual——dyld(3)
  7. dladdr(3) — Linux manual page