美文网首页
iOS:懒加载符号绑定流程

iOS:懒加载符号绑定流程

作者: 康小曹 | 来源:发表于2021-05-14 12:00 被阅读0次

    1. 桩函数的出现

    首先在 main 函数中打断点:

    image.png

    开启汇编调试,运行代码,结果如下:

    image.png

    上图可知调用 NSLog 实际都是 callq 0x10659f788。在该地址上 的调用地址上打断点:

    (lldb) breakpoint set -a 0x10659f788
    Breakpoint 8: where = XKLibTest`symbol stub for: NSLog, address = 0x000000010659f788
    

    执行代码,进入到对应断点,只有一句代码:

    image.png

    这里是一个关键点,慢慢看;

    我们先来看 0x10659f788 这个值怎么来的

    首先使用 image list找出偏移:

    image.png

    然后看下图:

    image.png

    到这里,我们知道了 NSLog 在被调用时,是直接跳转到了 __stubs 对应位置,然后执行该位置存储的机器码,这里猜测 FF25 就是 jmpq 的机器码指令,而 9A18、9C18 等等就是 jmpq 后面跟的参数,也就是和符号顺序相关的参数;

    至此,可以得出结论:

    • 在静态编译时期,外部符号的实际调用代码会被替换成桩函数的调用(call stub);

    2. 桩函数的具体含义

    到这里,我们还是有疑问,0x000000010659f804 这个是怎么来的, NSLog 最终又是怎么被调用的呢?

    我们继续看 jmpq *addr(%rip)

    这个汇编代码的意思是:

    1. address = Current address + Value before (%rip) + Current Instruction Size;
    2. 取出 1 中计算得到的 address 中的值;

    或者可以直接理解成:下一段汇编代码的地址 + rip 前的地址,然后取出这个指针的值进行跳转;

    这里我们来算一下,从 Mach-O 文件中可以看到:

    1. 下一个指令的地址为:278E
    2. 指令集大小为 6 ;

    如图:


    image.png

    所以可以这么算:

    (lldb) p/x 0x10659f78e + 0x189a
    (long) $3 = 0x00000001065a1028
    

    然后我们来看看 0x00000001065a1028 中存储的是什么:

    image.png

    即:0x000000010659f804,和汇编代码的注释一致;

    至此,我们又可以得出一个结论:

    • __stubs 表中存储的是机器码,其形式为:跳转指令 + 参数。

    这个肯定是和不同的架构相关的,所以针对不同架构要区别对待。因为在 mac 中查看 Mach-O 、系统动态库时比较方便,所以当前使用的是模拟器,即 x86 架构;

    3. 桩函数和懒加载符号表

    这里其实还有一个关键点,0x000000010659f804 存储在哪里?这就需要看看 0x00000001065a1028 在 Mach-O 文件中的表现了,减去偏移之后如下:

    image.png

    这里其实我们也可以得出一个结论:

    • 桩函数总是去懒加载符号表中取出符号对应的指针,以此来完成符号函数的调用,只不过懒加载符号在静态编译时,其指针指向和 binder 相关的机器码位置有关;

    4. stub_helper 函数

    至此,我们知道了懒加载符号表中初始化时,存储的是 0x000000010659f804 这个地址。其实到这里,我们基本可以猜出来 0x000000010659f804 这个地址应该就是和懒加载符号绑定相关函数的地址了。

    所以,我们需要看看这个地址在 Mach-O 中的表现,减去偏移看下结果:

    (lldb) p/x 0x000000010659f804 - 0x10659d000
    (long) $5 = 0x0000000000002804
    

    对比下 Mach-O 文件来看下:

    image.png

    也就是说,懒加载符号表初始化时存储的这个地址指向 __stub_helper

    从上图可以看出,0x000000010659f804 这个地址就是执行了 push + jmp 的指令,jmp 的位置都是一样的,push 的值不一样,而 push 的值肯定就是和懒加载符号顺序相关;

    大概可以参数来 0x1000027f4 是 binder 函数的真正逻辑代码,而 push 就是传递符号在符号表中位置的参数;

    为了验证自己的猜测,先打个断点看看实际的汇编代码吧,应该和 Mach-O 中的大差不差:

    image.png

    0x10659f7f4 就是 mach-o 文件的地址加上了偏移之后的结果,实际上都是指向 000027f4 这个地址,我们来看看这个地址存储的东西是否和 Mach-O 一致:

    image.png

    如上图,取 7 个字节转换大小端之后的结果为:0x051d8d4c + 0x180000 -> 0x4C8D1D05180000,和 Mach-o 中存储的值一致。

    总结:

    • 懒加载符号表中的指针初始化时,指向 stub_helper 函数。stub_helper 的作用和 stub 函数类似,都是一个代理或者说统筹代码的接口。其逻辑就是调用 binder 函数并传递符号位置参数,参数连同汇编指令(push)一起固定在了 __stup_helper 中;

    5. binder 函数

    根据上文,binder 函数实际的机器码其实在 mach-O 中就已经能看到了(lea r11......),但是作为辅助验证,我们来打个断点看看实际的调用:

    image.png

    从注释中可以看到,这里就出现了一个关键的函数:dyld_stub_binder 。先不慌,既然这里又出现了jmpq *xxx(%rip) 这种汇编,我们来算一下 0x00007fff2025cbb4 这个地址怎么来的,也算是作为复习了:

    0x10659f7fd 下一个指令的地址图上直接可以看到,即为 0x10659f803,所以:

    (lldb) p/x 0x10659f803 + 0x181d
    (long) $8 = 0x00000001065a1020
    (lldb) x/4xw 0x00000001065a1020
    0x1065a1020: 0x2025cbb4 0x00007fff 0x0659f804 0x00000001
    

    验证成功;

    接着,因为 binder 函数时从 0x00000001065a1020 这个地址取出来的,我们来看看 0x00000001065a1020 在 Mach-O 中的表现:

    image.png

    和运行时的汇编代码完美匹配。

    至此,可以得到一个结论:

    • 静态编译时期或者说懒加载符号还未被加载之前,懒加载符号表中指针指向 stub_help 函数,这个函数传递符号的位置参数,最终调用 dyld_stub_binder 函数执行懒加载符号的绑定流程;

    其实,这里还有个知识点。我们可以看看重定向表:

    重定向表

    如上图,重定向表中有三种符号,即 __stub、__nl_symbol_ptr、__la_symbol_ptr;懒加载和非懒加载符号好理解,__stub 为什么也会出现在重定向表中呢?

    其实也好理解,懒加载符号在动态编译时不会去寻址,而是被替换成了对应的桩函数。非懒加载符号会直接替换成实际函数的指针,而懒加载符号的调用都是通过桩函数来调用。

    第一次调用之后也不是直接去替换桩函数(也没办法在运行时去把__TEXT 中的代码改掉),所以必须有一个桩函数表和一个懒加载符号表。

    桩函数中的地址在链接时(静态链接 or 动态链接?)确认并写入 __TEXT 段;当程序运行时,__TEXT 段变成只读模式。此时对懒加载符号的修改操作作用在 __la_symbol_ptr 中。而 stub 函数中的逻辑是每次都去 __la_symbol_ptr 中寻址,第一次的指针指向 binder_helper 函数,第二次则指向真实的函数地址。就这样 __stub 和 __la_symbol_ptr 相互配合完成了懒加载的功能;

    也正是这个原因,在重定向表中的懒加载符号会有两份,__stub 一份,__la_symbol_ptr 一份:

    懒加载符号

    从这个现象来看,感觉更像是静态链接时生成桩函数,动态链接时将符号替换成了桩函数。操作完成后对 __TEXT 段 protect ,设置权限,变成只读模式;

    6. binder 函数汇编

    到这里,接下来就应该看看 dyld_stub_binder 函数了,打个断点:

    image.png

    这里的汇编代码有点多,以后再具体分析吧;

    7. 第二次调用

    继续看,第二次调用 NSLog 时是这样的:

    image.png

    看注释可以知道这里指针存储的位置就是实际的 NSLog 的函数地址了。

    也就是说,0x00000001065a1028 这个地址原本存储的是 stub_help 函数相关的位置,并传递了符号的位置信息,最终调用了 binder 函数。而现在被替换成了真实的函数地址,也就是在 binder 函数调用之后,真实的函数地址被替换到了懒加载符号表中,即:

    image.png

    至此,懒加载符号的绑定流程就结果了,流程图如下:

    image.png

    ps:其实真实逻辑是没有上图中的”已绑定“的判断的,都是直接调用,上图这么画是为了加深理解;

    8. 总结

    总结:

    1. 懒加载函数的调用代码都会被替换成对应桩函数的调用代码;
    2. 每个懒加载符号都有对应的桩函数,存储于 __stubs 中。符号的不同本质上是桩函数参数的不同,这个参数和符号在懒加载符号表中的位置有关,而且这个参数(位置)编译时期就确定了,以机器指令的形式(FF25xxxx0000)固定在 __stubs 中;
    3. 懒加载符号表的初始指针指向 __stub_help 函数,在 binder 完毕之后被替换成实际函数的地址。而__stub_help 函数的逻辑和桩函数一样,异常简单,通过 push 传递符号位置参数,然后直接调用 binder 函数;
    4. 桩函数不关心 binder 具体逻辑,每次调用外部函数都是通过桩函数进行调用。而它只是按部就班的每次都去懒加载符号表中取出该符号的指针指向的地址,然后进行跳转。所以第一次调用懒加载符号跳转到了 binder 函数,而第二次调用则跳转到了真实的函数地址;
    5. 非懒加载符号在动态链接时就在非懒加载符号表中写入了函数的具体地址,且非懒加载符号的调用会被直接替换成实际函数的指针,不通过桩函数调用(桩函数的存在是为了实现懒加载的功能);

    一句话总结:

    桩函数每次调用都会去懒加载符号表中取符号对应的函数地址进行调用。静态编译时期,懒加载符号的指针指向 dyld_stub_binder,所以第一次调用时都会执行外部函数 dyld_stub_binder 来对懒加载符号进行绑定,而 dyld_stub_binder 是非懒加载符号,在动态链接时就已经绑定。dyld_stub_binder 调用完毕之后替换懒加载符号中的指针,指向了实际的函数地址,所以第二次调用仍然是利用桩函数去懒加载符号表中取出指针,但此时的指针已经指向实际的函数地址,可以直接调用;

    相关文章

      网友评论

          本文标题:iOS:懒加载符号绑定流程

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