对于存在于别的动态库的函数,程序在运行的时候需要通过动态链接来获取函数的调用地址。在iOS上是通过dyld来实现的。下面对这个原理做一下梳理。
以最令人熟悉的NSLog来做例子
先写一个简单的程序
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"first");
NSLog(@"second");
}
然后在两个NSLog都打上断点,运行。
程序断住之后,调节成汇编模式Debug --> Debug Workflow --> Always Show Disassembly
,然后会切换到下面的视图

留意到
symbol stub for: NSLog
这句,这句就是跳转到NSLog的stub实现imp___stubs__NSLog
我们先用
image list
命令来获取模块的基地址
0x0000000100d80000
就是这次运行时模块的基地址。为什么说是这次运行呢?因为iOS有ASLR(Address space layout randomization),将可执行程序随机装载到内存中,这里的随机只是偏移,而不是打乱。所以程序运行时候的基地址为:静态的基地址+ASLR偏移静态的基地址可以使用machOView来看

或者去看TEXT段

总结一下:运行时程序的基址 = __LINKEDIT.VM_Address –__LINKEDIT.File_Offset + ASLR偏移
所以,这次运行的时候ASLR偏移=0x0000000100d80000 - 0x100000000 = 0xd80000
好了,回到刚刚的bl 0x100dc2dac ; symbol stub for: NSLog
,我们可以去看看这个地址里面究竟是什么代码,先求出它对应的 静态地址=运行时地址-ASLR偏移 = 0x100dc2dac - 0xd80000 = 0x100042DAC。然后用hopper打开,跳转到该地址,如下

这三句代码是取值之后跳转,先看看
#0x100050228
的值,单击点进去。这里有个坑,如果你用hopper导入的时候选择了Resolve Lazy Binding,那么这个地址会解析成了符号。所以导入的时候不要选,那么可以看到这个地址的值如下:0000000100050228 dq 0x0000000100043598
所以上面的三句汇编的意思是跳转
0x0000000100043598
这个地址,然后我们再切到这个地址看看。

在这里压入一个参数之后,继续跳转到
0x1000433b8
执行,继续跟入到这里,终于找到了stub_helper的调用。
继续执行,到第二次进入NSLog,然后你会发现0x100dc2dac
处的执行代码变成了下面的,这里其实仍然是取的静态地址0x100050228
处的值,只不过这个值已经变成了0x18350e680

我们可以打印一下静态地址
0x100050228
对应的动态地址0x100050228+ 0xd80000 = 0x100DD0228
,确实已经变成了0x18350e680

然后进入0x18350e680
这个地址,你会发现这里就是NSLog的函数入口

附上刚刚image list的另外一个模块,你会发现
0x18350e680
这个地址已经是另外模块的了。[ 6] 18908B96-750C-3898-8EB0-D7028C656A6D 0x00000001834b5000 /Users/zheng/Library/Developer/Xcode/iOS DeviceSupport/11.3.1 (15E302)/Symbols/System/Library/Frameworks/Foundation.framework/Foundation
这个就是MachO的懒加载符号表的加载过程。第一次调用的时候是指向stub_helper,调用完一次之后,就会修改懒加载符号指针,让其指向真正的函数地址,以后就可以直接跳转。
最后再附上MachOView上的几张图片,指明了刚刚所说的几处函数位置,这里是Symbol Stubs,留意到这个0x00042DAC
是不是很熟悉?这个是文件偏移,它和静态基地址的关系是静态基地址 = 模块起始地址 + 文件偏移。起始下面就是我们调用NSLog最开始会跳转的地方。它的值就是刚刚那三句跳转代码。

到静态地址
0x100050228
的地方(留意这里是DATA区),可以看到它的值是最开始的0x0000000100043598
,当调用过一次之后,stub_helper就会把这里的值改为函数的真实地址。
至于stub_helper的绑定过程,看汇编比较麻烦,建议去看源码吧
libdyld.dylib dyld_stub_binder --> libdyld.dylib _dyld_fast_stub_entry --> dyld::fastBindLazySymbol -->ImageLoaderMachO::bindLocation
网友评论