图解 Mach-O 中的 got

作者: 微微笑的蜗牛 | 来源:发表于2021-01-02 11:03 被阅读0次

    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

    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 个页。

    image

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

    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 中,如下图所示:

    image

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

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

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

    image

    可见,符号表中已经包含了 _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 处的神秘嘉宾。

    image

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

    image

    请注意看图上的 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 地址。

    image

    这样,一切都清楚了。

    • 函数符号的地址绑定会调用到 dyld_stub_binder

    • 通过它获取到地址后,再更新下图中红框处的值为函数的真正地址。

    • 以后就不用走 dyld_stub_binder 地址绑定的流程了,直接跳转到函数地址去执行。

    image

    got 符号值查找

    查找原理

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

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

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

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

    image

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

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

    image

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

    这里有点绕,流程如下:

    
    1.  通过 __got section header,拿到 indirectSymIndex。
    
    2.  拿 indirectSymIndex 到间接表中(indirect symbol table)取到符号表中的下标 symIndex。
    
    3.  拿 symIndex 到符号表中取到最终的符号信息,这里有它在字符串表中的下标 strIndex。
    
    4.  拿 strIndex 到字符串表中取到符号字符字符串。
    
    

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

    image

    实践验证

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

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

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

    image

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

    image

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

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

    image

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

    image

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

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

    image

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

    image

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

    此时字符串表数据如下:

    image

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

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

    image

    总结

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

    参考资料:

    相关文章

      网友评论

        本文标题:图解 Mach-O 中的 got

        本文链接:https://www.haomeiwen.com/subject/fjheoktx.html