iOS 懒加载符号的绑定流程

懒加载符号的绑定流程

一、测试资料及步骤
首先看一下我们使用的测试代码以及debug方式
1、测试代码如下

int main(int argc,char *argv[])
{	
	NSLog(@"--");
	NSLog(@"--");
}

2、开启debug模式
在源码NSLog上添加断点, 然后勾选xcode菜单Debug->Workflow->Always Show Disassembly, 这样就可以逐步调试汇编代码. 在lldb上输入si就可以按照指令逐步调试.

二、Mach-O文件中Dyld相关内容
在Dyld加载动态库时离不开Mach-O文件,Mach-O文件记录了应用的基本信息、代码信息以及内核加载应用的相关信息,其中Dyld使用的内容有

  • 在TEXT段中
    • __stubs 间接符号存根,存放的是汇编指令,用于跳转到懒加载指针表
    • __stubs_helper 懒加载符号加载辅助函数,懒加载符号表初始化时存储的地址指向 __stub_helper
  • 在DATA段中
    • __nl_symbol_ptr 非懒加载指针表,dyld加载时立即绑定值
    • __la_symbol_ptr 懒加载指针表,表中的指针一开始都指向 __stub_helper,第1次调用才绑定值
    • __got 非懒加载全局指针表

三、Dyld懒加载过程
第一次断点,开启汇编调试之后,断点执行到下面的位置在这里插入图片描述
我们可以看到调用NSLog,实现是访问0x10659f788这个地址,那么0x10659f788 这个值怎么来的呢?我们使用image list来查看应用的起始地址,得到结果是0x000000010659d000,而且在_stubs中,NSLog的地址是0x2788(应用内的偏移地址)

0x10659f788 =  0x10659d000 + 0x2788

查看__stubs中NSLog(,看到NSLog的偏移地址就是0x2788。
在这里插入图片描述
综上,当我们在程序中调用动态库函数时,代码早在在静态编译时,就将外部符号的实际调用代码替换成桩函数的调用(call stub)),并且调用地址保存在TEXT中的__stubs中,,所以调用NSlog,内核首先会调用__stubs中的桩函数

在执行一步断点,代码将执行到一个跳转的汇编代码中在这里插入图片描述
上面代码意思是计算出jmpq *addr(%rip)地址并跳转到这个地址执行,下面我们计算一下jmpq *addr(%rip)汇编代码地址
分为两步
第一步:计算地址:address = Current address + Value before (%rip) + Current Instruction Size;
第二步:取出 1 中计算得到的 address 中的值,跳转。
也就是10659f788(当前地址)+ 0x189a(Valuse before) + 6(指令大小) =1065A1028,另一种计算方式就是下一条指令地址(Current address + Current Instruction Size)+ 0x189a(Valuse before) ,下一条指令地址是偏移地址是278e,指令地址是0x10659d000+278e = 10659F78E,10659F78E+0x189a=1065A1028在这里插入图片描述
但是我们发现使用0x1065A1028减去应用的起始地址0x10659d000 ,得到应用内偏移0x4028中存储的是什么?
在这里插入图片描述
0x000000010659f804 存储在哪里呢?看下图计算,我们可以总结出__stubs 表中存储的是机器码,其形式为:跳转指令 + 参数,参数就是0x000000010659f804,这个地址应该就是和懒加载符号绑定相关函数的地址
在这里插入图片描述
在这里插入图片描述
对比上面__la_symbol_ptr和__stub_helper, 发现__la_symbol_ptr中NSLog区存放的是__stub_helper中对应符号的地址0x2804,也就是在符号完成bind之前, 存放的是__stub_helper区对应符号的地址. bind之后, 真正的地址就会被写进__la_symbol_prt对应符号上。从__stub_helper中看出0x2804地址是一个push指令,那么0x10659f804 应该是执行了push + jmp的指令,也就是push。
在这里插入图片描述
我们看到执行到0x10659f804(push)之后,执行jmp 0x10659f7f4,那0x10659f7f4是什么指令呢?我们获取偏移地址0x27F4
(0x10659f7f4 - 0x10659d000),也就是下图stub_helper中的第一条命令(lea r11,qword ptr [rip +0x1805])
在这里插入图片描述
我们继续执行断点
在这里插入图片描述
发现执行到0x10659f7f4,也就是第一行

 lea r11,qword ptr [rip +0x1805]

终于看到一个函数名,也就是dyld_stub_binder,那我们看看0x7fff2025cbb4地址怎么得到的,从图中可以0x10659f7fd 下一个指令的地址图上直接可以看到,即为 0x10659f803,也就是
在这里插入图片描述
binder 函数是从 0x00000001065a1020 这个地址取出来的,使用这个地址减去应用的起始地址,即可获得偏移地址0x4020(0x1065a1020 - 0x10659d000),这个偏移地址在DATA中的__got里面有记录,也就是dyld_stub_binder的地址
在这里插入图片描述
之后在打断点,就是dyld_stub_binder函数的执行过程了,这里就不赘述了

总结

  • 桩函数记录在TEXT的stub,桩函数总是去懒加载符号表中取出符号对应的指针,以此来完成符号函数的调用,只不过懒加载符号在静态编译时,其指针指向和 binder 相关的机器码位置有关
  • 懒加载符号表中的指针初始化时,指向 stub_helper 函数。stub_helper 的作用和 stub 函数类似,都是一个代理或者说统筹代码的接口。其逻辑就是调用 binder 函数并传递符号位置参数,参数连同汇编指令(push)一起固定在了 __stup_helper 中

第二次调用NSLog时,直接获取0x1065a1028地址,这个地址原本存储的是 stub_help 函数相关的位置,并传递了符号的位置信息,最终调用了 binder 函数。而现在被替换成了真实的函数地址,也就是在 binder 函数调用之后,真实的函数地址被替换到了懒加载符号表中
在这里插入图片描述

四、懒加载符号绑定流程如下
在这里插入图片描述
总结:

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

参考研读

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员的修养

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值