2

图解 Mach-O 中的 got

 2 years ago
source link: https://juejin.cn/post/6918645161303998478
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

got 是什么

iOS 开发中,动态库是个绕不开的话题,系统库基本上是动态库。它的一大优势是节约内存,可让多个程序映射同一份的动态库,实现代码共享。动态库本身也是一个 Mach-O 文件,也有数据段、代码段等。其中代码段可读可执行,数据段可读可写。

动态库共享的只是代码段部分,为了达到代码段共享的目的,其符号地址在生成时就不能写死,因为它映射到每个程序中虚拟内存空间中的位置可能不一样。对于数据段部分,由于各个程序会对其进行修改,因此每个程序会单独映射一份。

那么如何解决代码段共享的问题呢?聪明的人们,想出一种精妙的解决方式。通过添加一个中间层,到另一个表中去查找符号的地址。这个表就叫 gotglobal offset table,全局符号偏移表,然后在运行时绑定地址信息,将地址填入到 got 中。这样代码段中的符号就与具体地址无关,只和 got 中的数据有关。这种方式就叫 PICProgram Independent Code,地址无关代码。

或许你可能会想到,got 中保存的是符号地址,而每个程序的地址是不一样的,那 got 肯定是不能共享的。没错,所以 got 会保存在数据段中,每个程序单独一份。在进行符号绑定时,更新 got 中对应符号的地址即可。

got 的位置

在了解 got 是什么之后,我们再来看看 Mach-Ogot 到底放在了哪里。通过下图可以看出,有个专门的 __got section 存放 got 数据,而它是属于 __DATA segment

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ce6b018a1a8449b29b9c913936bd3f84~tplv-k3u1fbpfcp-zoom-1.image

对于 segmentsection,可能大家会有些困惑。下面来简单解释一下。

section

section 称为节,是由编译器对 .o 内容的划分,将同类资源在逻辑上划分到一起。常见的 section 有:

  • 存放代码指令,.text
  • 存放已初始化全局变量,.data
  • 存放未初始化的全局变量和静态局部变量,.bss
  • 符号表,.symtab
  • 字符串表,.strtab

segment

segment 称为段,它是权限属性相同 section 的集合。

在程序装载时,操作系统并不关心 section 的数量和内容,只对其权限敏感,因此没必要一个个加载 section,只需将权限相同的 section 合到一起加载即可。

另外,这样还可节省内存。由于内存按页分配,即使不满一个页也得分配一整页。若单个 section 大小非系统页长度的整数倍,会造成内存碎片。而将其合并后,会有效缓解这种情况。

举个栗子, .text.init 的权限都是只读可执行,.init 是程序初始化代码。

假设页的大小是 4 KB.text 大小为 4098 字节,.init 大小为 900 字节。如下图所示,若将它们单独映射,.text 会占用 2 个页,.init 占用 1 个页,整体占用 3 个页。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b810c949f31047a9892223e43f62d662~tplv-k3u1fbpfcp-zoom-1.image

如果它们合并成代码段,那么只需占用 2 个页,减少内存浪费。如下图所示。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e9b0422fc26d4067b3659b420223c9b6~tplv-k3u1fbpfcp-zoom-1.image

可执行文件是由多个 .o 文件链接而成的,每个 .o 文件有各自的 section。因此链接器将所有 .o 文件中权限相同的 section 合并到一起,形成 segment。操作系统只需将 segment 映射到虚拟内存空间即可。

平常我们所说的代码段、数据段,便是指链接后的 segment

动态库符号类型

动态库中的符号分为 non-lazy symbollazy symbol

  • non-lazy symbol,是指在启动时就必须链接的符号,确定好符号地址。
  • lazy symbol ,顾名思义,延迟绑定符号,只在使用时才进行链接。

为啥要分为两种类型呢?我们试想一下,如果所有动态库的符号都是启动时链接,一个程序随随便便依赖的系统动态库就有大几十个。每个动态库中符号还不少,并且也不是所有符号都会用到,这样势必会拖慢启动速度。所以采用延迟绑定技术,只需在第一次用到时进行绑定,可提高性能。而数据符号相对较少,则可以采用 non-lazy 的方式,放到启动时就链接。

因此,Mach-O 中划分了两个 section 来保存 non-lazy symbollazy symbol。其中 __got 中保存的是 non-lazy symbol__la_symbol_ptr 保存的是 lazy symbol

下面,我们来实践一下,验证上述说法的正确性。请将以下文件放在同一个目录下。

print.c:

#include <stdio.h>

char *global = "hello";

void print(char *str)
{
  printf("%s\n", str);
}
复制代码

main.c:

void print(char *str);

extern char *global;

int main()
{
  print(global);
  return 0;
}
复制代码

run.sh:

// 生成 main.o,目标版本 14.0
xcrun -sdk iphoneos clang -c main.c -o main.o -target arm64-apple-ios14.0

// 生成 libPrint.dylib 动态库
xcrun -sdk iphoneos clang -fPIC -shared print.c -o libPrint.dylib -target arm64-apple-ios14.0

// 链接生成可执行文件,"-L .", 表示在当前目录中查找。"-l Print",链接 libPrint.dylib 动态库
xcrun -sdk iphoneos clang main.o -o main -L . -l Print -target arm64-apple-ios14.0
复制代码

run.sh 添加可执行权限后再运行,生成可执行文件。

chmod +x run.sh
./run.sh
复制代码

执行完毕后,在目录中会生成 libPrint.dylib 动态库和 main 可执行文件。

main 拖到 MachOView 中,如下图所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/8874bf8299d449e09c796c04b797edc0~tplv-k3u1fbpfcp-zoom-1.image

右边红框中的 _global 就是动态库 libPrint.dylib 中的符号。它被放到了 __got 中,并且其初始地址为 0。它是表的第一项,表地址是 0x10008000,那么 0x10008000 中的值就是符号地址。

另外,我们还发现,在 __got 中还有一条记录 dyld_stub_binder,初始地址也是 0。它是表的第二项,也就是 0x10008008 地址中的值为符号地址。

_global 在启动时会进行链接,那么如何知道需要链接哪个动态库呢?我们点开 Symbol Table,会看到如下信息:

可见,符号表中已经包含了 _global 所属动态库的信息,libPrint.dylib*。*同样 dyld_stub_binder ,它在 libSystem.B.dylib **中。

虽然动态库中的符号,在生成可执行文件时,没有进行链接,但是在符号表中记录了它在哪个动态库中。这样在运行时进行链接,才能到相应动态库中找到。

dyld_stub_binder

从字面意思,我们大致可以猜到,它是用来做符号绑定用的。前面提到过,函数符号都是在第一次使用时才进行绑定,其实是通过 dyld_stub_binder 来进行符号查找与地址重定位。鉴于它肩负重大使命,因此必须预先绑定好地址,所以会放到 __got 中。

dyld_stub_binder 是用汇编实现的,在 dyld_stub_binder.s 中。它的调用链路如下:


// 汇编中调用 fastBindLazySymbol
1. dyld::fastBindLazySymbol

2. ImageLoaderMachOCompressed::doBindFastLazySymbol

// 符号绑定
3. ImageLoaderMachOCompressed::bindAt

// 符号地址解析
4. ImageLoaderMachOCompressed::resolve

// 符号地址更新
5. ImageLoaderMachO::bindLocation
复制代码

其中 resolve 是解析符号地址,bindLocation 进行符号地址更新。

lazy 符号重定位

上面我们说到,函数符号的重定位是通过 dyld_stub_binder 来做的,那么有没有依据可寻呢?当然有啦。

从下图可以看出,_print 的地址是 0x100007FAC,不是说在第一次调用时才绑定地址吗?为什么该函数的地址会有值呢?没错,但它需要有人帮忙来进行地址重定位,这个帮手就是 0x100007FAC 处的神秘嘉宾。

这个地址处在 __TEXT 段范围,通过查看 __TEXT 段各个 section 的地址范围,我们很容易发现它处在 __stub_helper 中。如下图所示:

请注意看图上的 1、2、3 标号。地址 0x100007FAC 处于 1 号。它对应的汇编代码功能是:

  • 取出 0x100007fb4 处的值放入 w16,也就是将 w16 清 0。
  • b 是无返回跳转指令,跳转到 0x100007f94,也就是开头 2 号处。

然后,从 2 号处开始执行,一直到 3 号位置。3 号区域的功能是:

  • 第一行是相对地址偏移取值指令。在距离当前行地址 0x10007FA4 偏移 0x64 的地方取出值,放入 x16。也就是取出 0x10007FA4 + 0x64 = 0x10008008 处的内容。
  • br x16,进行函数调用,跳转到 x16 中的地址。

所以,最主要是得弄清楚 0x10008008 地址里面的内容是啥,根据 br 指令推断,它肯定是个函数地址。

有没有觉得 0x10008008 有些熟悉呢?再看看下面这张图,其实在第一节的图中我们已经看到过它。got 中第二项的地址就是 0x10008008,而它正好存储的是 dyld_stub_binder 地址。

这样,一切都清楚了。

  • 函数符号的地址绑定会调用到 dyld_stub_binder
  • 通过它获取到地址后,再更新下图中红框处的值为函数的真正地址。
  • 以后就不用走 dyld_stub_binder 地址绑定的流程了,直接跳转到函数地址去执行。

got 符号值查找

变量和函数统称为符号,所有符号信息都在符号表 Symbol Table 中,符号值在字符串表 String Table 中。符号表只是记录了它在字符串表中的下标,因为这样可以节省空间。

而我们上文中提到的 global 是个外部全局变量,那么它存在了符号表中的哪里?可以通过何种路径找到它呢?下面来探寻一下。

首先让我们回到 Mach-OLoad Commands 中。它里面有一系列的加载命令,告诉系统如何加载不同的 segment。加载命令中包含了 Section Header 的数组,header 里面包含了每个 section 的基础信息,比如节名称、所属 segment 的名称、地址、大小、偏移、保留字段等等。

既然 __got 是一个 section,那么肯定也有对应的头信息。从下图可以看到,在 LG_SEGMENT_64(__DATA_CONST) 中,包含了 __gotheader

注意右边红框中 Indirect Sym Indx 部分,它表示了 __got 中的第一个符号在间接表中的下标,间接表其实就是动态库符号表。如果 __got 中有多个符号,那么下标依次 +1 即可。

举个栗子,假设 __got 第一个符号在间接表中的下标是 x,那么第二个符号的下标为 x+1,第三个为 x+2,以此类推。如下图所示:

而间接表中的内容是该符号在符号表的下标,取出内容,然后到符号表中查找,便可找到符号信息。到这里还没完,由于符号值并不是直接存在符号表中,而是在字符串表。最后拿字符串下标到字符串表中查找。

这里有点绕,流程如下:

1. 通过 __got section header,拿到 indirectSymIndex。

2. 拿 indirectSymIndex 到间接表中(indirect symbol table)取到符号表中的下标 symIndex。

3. 拿 symIndex 到符号表中取到最终的符号信息,这里有它在字符串表中的下标 strIndex。

4. 拿 strIndex 到字符串表中取到符号字符字符串。
复制代码

整体图示如下(注:符号表中仅画出了下标,省略了其他信息):

光说不练假把式,下面我们来验证一下。

__got section header 中在间接符号表的下标为 1,也就是说第一个符号下标为 1。从上文图中可以看到,__got 中总共有 2 个符号,分别为 _globaldyld_stub_bind。如果找到的符号为 _global,那么表示上述结论是正确的。

此时 __got section header 的数据如下图所示,indirect sym index = 1

那我们到 dynamic symbol table 中去瞧一瞧,找到下标为 1 的数据信息,即第二个数据。如下所示:

从上图可以看出,在对应的 Data 一列中,内容为 3,表示它在符号表中的下标为 3。

此时 indirect symbol table 中的数据如下所示:

然后继续到符号表中看看下标为 3 的数据是啥。如下图所示:

第四项数据 String Table Index,它的值是 0x1c,转换为十进制为 28,这就是字符串表中的下标。

此时符号表中的数据如下所示:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d4b9d440c85641c4a8ffa55cd7bf26ec~tplv-k3u1fbpfcp-zoom-1.image

最后一步,来到字符串表中。看看下标为 28 的内容是什么?一行是 16 字节,第二行倒数第四个数就是符号开始处(不放心的可以自己数一数😆)。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/12dfb1e0a0224e018d51dd2b7d55931a~tplv-k3u1fbpfcp-zoom-1.image

其中,5F_ascii 码,67gascii 码,...,一直到 . 号为止。正好对应的是 _global,也就证明了查找过程的正确性。

此时字符串表数据如下:

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/d3cd689661d542b28b30272d17777341~tplv-k3u1fbpfcp-zoom-1.image

那对于第二个符号 dyld_stub_binder ,你是否可以自行实践出来呢?

其实,以上查找不仅限于 __got 中的符号,对于延迟加载符号一样适用。下图中 __la_symbol 同样也有 Indirect Sym Index。动态库中的符号都是这种查找方式。

https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/16b04ad0f4024463b1de4573c2d62154~tplv-k3u1fbpfcp-zoom-1.image

这篇文章中,我们介绍了什么是 got、got 在 mach-o 中的位置、函数符号如何与 dyld_stub_binder 进行关联,以及如何一步步查找动态库符号的值。希望对你有用处~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK