got 是什么
iOS 开发中,动态库是个绕不开的话题,系统库基本上是动态库。它的一大优势是节约内存,可让多个程序映射同一份的动态库,实现代码共享。动态库本身也是一个 Mach-O
文件,也有数据段、代码段等。其中代码段可读可执行,数据段可读可写。
动态库共享的只是代码段部分,为了达到代码段共享的目的,其符号地址在生成时就不能写死,因为它映射到每个程序中虚拟内存空间中的位置可能不一样。对于数据段部分,由于各个程序会对其进行修改,因此每个程序会单独映射一份。
那么如何解决代码段共享的问题呢?聪明的人们,想出一种精妙的解决方式。通过添加一个中间层
,到另一个表中去查找符号的地址。这个表就叫 got
,global offset table
,全局符号偏移表,然后在运行时绑定地址信息,将地址填入到 got
中。这样代码段中的符号就与具体地址无关,只和 got
有关。这种方式就叫 PIC
,Program Independent Code
,程序地址无关代码。
或许你可能会想到,got
中保存的是符号地址,而每个程序的地址是不一样的,那 got
肯定是不能共享的。没错,所以 got
会保存在数据段中,每个程序单独一份。在进行符号绑定时,更新 got
中对应符号的地址即可。
got 的位置
在了解 got
是什么之后,我们再来看看 Mach-O
中 got
到底放在了哪里。
通过下图可以看出,有个专门的 __got section
存放 got
数据,而它是属于 __DATA segment
。
对于 segment
和 section
,可能大家会有些困惑。下面来简单解释一下。
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
个页。
如果它们合并成代码段,那么只需占用 2
个页,减少内存浪费。如下图所示。
可执行文件是由多个 .o
文件链接而成的,每个 .o
文件有各自的 section
。因此链接器将所有 .o
文件中权限相同的 section
合并到一起,形成 segment
。操作系统只需将 segment
映射到虚拟内存空间即可。
平常我们所说的代码段、数据段,便是指链接后的 segment
。
动态库符号类型
动态库中的符号分为 non-lazy symbol
和 lazy symbol
。
non-lazy symbol
,是指在启动时就必须链接的符号,确定好符号地址。lazy symbol
,顾名思义,延迟绑定符号,只在使用时才进行链接。
为啥要分为两种类型呢?我们试想一下,如果所有动态库的符号都是启动时链接,一个程序随随便便依赖的系统动态库就有大几十个。每个动态库中符号还不少,并且也不是所有符号都会用到,这样势必会拖慢启动速度。所以采用延迟绑定技术,只需在第一次用到时进行绑定,可提高性能。而数据符号相对较少,则可以采用 non-lazy
的方式,放到启动时就链接。
因此,Mach-O
中划分了两个 section
来保存 non-lazy symbol
和 lazy symbol
。其中 __got
中保存的是 non-lazy symbol
,__la_symbol_ptr
保存的是 lazy symbol
。
下面,我们来实践一下,验证上述说法的正确性。请将以下文件放在同一个目录下。
print.c:
#include
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
中,如下图所示:
右边红框中的 _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
这个陌生人。从字面意思,我们大致可以猜到,它是用来做符号绑定用的。前面提到过,函数符号都是在第一次使用时才进行绑定,其实是通过 dyld_stub_binder
来进行符号查找与地址重定位。鉴于它肩负重大使命,因此必须预先绑定好地址,所以会放到 __got
中。
dyld_stub_binder
是用汇编实现的,在 dyld_stub_binder.s
中。它的调用链路如下:
// 汇编中调用 fastBindLazySymbol
1. dyld::fastBindLazySymbol
// 调用 ImageLoader 处理
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-O
的 Load Commands
中。它里面有一系列的加载命令,告诉系统如何加载不同的 segment
。加载命令中包含了 Section Header
的数组,header
里面包含了每个 section
的基础信息,比如节名称、所属 segment
的名称、地址、大小、偏移、保留字段等等。
既然 __got
是一个 section
,那么肯定也有对应的头信息。从下图可以看到,在 LG_SEGMENT_64(__DATA_CONST)
中,包含了 __got
的 header
。
注意右边红框中 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
个符号,分别为 _global
和 dyld_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
,这就是字符串表中的下标。
此时符号表中的数据如下所示:
最后一步,来到字符串表中。看看下标为 28
的内容是什么?一行是 16 字节,第二行倒数第四个数就是符号开始处(不放心的可以自己数一数)。
其中,5F
是 _
的 ascii
码,67
是 g
的 ascii
码,...,一直到 .
号为止。正好对应的是 _global
,也就证明了查找过程的正确性。
此时字符串表数据如下:
那对于第二个符号 dyld_stub_binder
,你是否可以自行实践出来呢?
其实,以上查找不仅限于 __got
中的符号,对于延迟加载符号一样适用。下图中 __la_symbol
同样也有 Indirect Sym Index
。动态库中的符号都是这种查找方式。
总结
这篇文章中,我们介绍了什么是 got、got 在 mach-o 中的位置、函数符号如何与 dyld_stub_binder 进行关联,以及如何一步步查找动态库符号的值。希望对你有用处~
参考资料:
- https://juejin.cn/post/6844903926051897358
- 《程序员的自我修养》