介绍
iOS外部符号加载方式有两种:懒加载和非懒加载。
-
懒加载:首次调用该符号才加载。
-
非懒加载:app启动时加载。
非懒加载
默认加载方式。比如下面的代码,外部符号调用会先替换成一个桩函数,在__TEXT,__stubs
段会生成该桩函数。
int main(int argc, const char * argv[]) {
MMFWHeaderTest();
}
.....
-> 0x100003c93 <+99>: callq 0x100003cea
我们接着打断点进去0x100003cea
这个桩函数看看。
-> 0x100003cea <+0>: jmpq *0x310(%rip)
先在MachOView看看这个段的内容__TEXT,__stubs
。
注意看Data的规律,都是FF25开头,MMFWHeaderTest的FF25
后面是1003
,考虑到MacOS是小端,所以1003
正对应了0x310
。
这也是为什么桩函数在__TEXT段,FF25是跳转指令,后面的都是跳转的长度。
0x100003cea <+0>: jmpq *0x310(%rip)
的意思是,从下个地址开始,加上0x310
,取出该地址存的值,跳转到这。
桩函数这里每个函数大小是0x6
,所以是0x100003cea + 0x6 + 0x310 = 0x0000000100004000
。
看看0x0000000100004000
在mach-o文件是哪个段,通过image list
得到偏移是0x0000000100000000
,相对地址就是0x0000000100004000-0x0000000100000000=0x4000
取出0x0000000100004000
存的数据,跳转到这个地址。
(lldb) x/gx 0x0000000100004000
0x100004000: 0x00000001000a9f60
(lldb) dis -a 0x00000001000a9f60
MMFW`MMFWHeaderTest:
0x1000a9f60 <+0>: pushq %rbp
0x1000a9f61 <+1>: movq %rsp, %rbp
0x1000a9f64 <+4>: popq %rbp
0x1000a9f65 <+5>: retq
总结
- 非懒加载会将外部函数替换成一个桩函数,桩函数保存在
__TEXT,__stubs
,桩函数指令是取出__DATA_CONST,__got
段指定位置的值,然后跳转到该地址。 __DATA_CONST,__got
保存非懒加载符号,会在启动时写入对应外部函数的具体地址。
懒加载
Other Link Flag指定-undefined dynamic_lookup
查看汇编指令
//调用MMFWHeaderTest
0x100003c18 <+104>: movq 0x4831(%rip), %rdi
......
//桩函数
0x100003c6a <+0>: jmpq *0x4390(%rip)
......
(lldb) p/x 0x100003c6a + 0x4390 + 0x6
(long) $0 = 0x0000000100008000
(lldb) x/gx 0x0000000100008000
0x100008000: 0x00000001000a9f60
(lldb) dis -a 0x00000001000a9f60
MMFW`MMFWHeaderTest:
0x1000a9f60 <+0>: pushq %rbp
0x1000a9f61 <+1>: movq %rsp, %rbp
0x1000a9f64 <+4>: popq %rbp
0x1000a9f65 <+5>: retq
奇怪了,好像跟之前没什么不一样,也是一个桩函数,然后桩函数调到一个函数指针上。但是跟网上其他文章说的不一样呀?
再看看文件mach-o文件。
发现外部函数指针从__DATA__CONST,__got
移到了__DATA,__la_symbol_ptr
,除此之外没有其他改变。
那没办法了,只能去看dyld源码了。
dyld
首先要确定从哪个函数开始,这里在OC类里的+load
方法里打个断点。
那就先从dyld4::prepare
函数看起,我也是第一次看,不熟悉。
那就不看细节,就从名称来看,看哪个像。
可以调试dyld的汇编代码,在一些可能的指令callq
前后打断点,执行 x/gx {符号指针}
,看执行完哪个函数后,函数指针里有正确的函数地址了,再用dis -a
验证。
你别说,这还真让我找到了
for ( const Loader* ldr : state.loaded ) {
......
ldr->applyFixups(fixupDiag, state, cacheDataConst, true);
......
}
applyFixups
执行几次后,我这边的外部函数指针就有正确的地址了。
接着进入到JustInTimeLoader::applyFixups
。看到里面有几行代码在打印日子。
if ( state.config.log.fixups ) {
const char* targetLoaderName = target.targetLoader ? target.targetLoader->leafName() : "<none>";
state.log("<%s/bind#%lu> -> %p (%s/%s)\n", this->leafName(), bindTargets.count(), targetAddr, targetLoaderName, target.targetSymbolName);
}
如果启动时设置一些环境变量,dyld是能打印一些信息的。所以问一下那个男人,man dyld
。找到了一个可能的变量。
DYLD_PRINT_BINDINGS
If set, causes dyld to print a line each time a symbolic name is
bound.
再把DYLD_PRINT_BINDINGS
在代码里一搜。
this->fixups = security.allowEnvVarsPrint && process.environ("DYLD_PRINT_BINDINGS");
估计就是这个了,设置好环境变量,看到输出栏里打印了
dyld[28319]: <MM/bind#0> -> 0x1000ae0b8 (MMFW/_OBJC_CLASS_$_MMFWHeader)
那么基本就可以断定了,现在懒加载符号也是在启动时绑定好符号了。
既然都下载源码了,那顺便看看dyld_stub_binder
这个懒加载辅助函数的源码吧(可以在__DATA_CONST,__got
找到)。
// dyld_stub_binder is no longer used, but needed by old binaries to link
.align 4
.globl dyld_stub_binder
dyld_stub_binder:
#if __x86_64__ || __i386__
jmp __dyld_missing_symbol_abort
#else
b __dyld_missing_symbol_abort
#endif
这里说dyld_stub_binder
不再使用了,保留这个函数是为了兼容老版本的二进制,因为里面有依赖到。现在实际上是跳转到__dyld_missing_symbol_abort
,abort这个词一听就知道,执行这个函数程序要流产、停止。
那我再试试声明一个不存在的函数调用,因为-undefined dynamic_lookup
的存在,所以不会报错。
void MMFWHeaderTestUndefined(void);
int main(int argc, const char * argv[]) {
MMFWHeaderTestUndefined();
}
//汇编
-> 0x100003bf8 <+104>: callq 0x100003c56 ; symbol stub for: MMFWHeaderTestUndefined
......
-> 0x100003c56 <+0>: jmpq *0x43ac(%rip) ; (void *)0x00007ff8005d1caa: _dyld_missing_symbol_abort
果然,本来应该调用dyld_stub_binder
,但是这个函数执行的是__dyld_missing_symbol_abort
,程序结束了。
总结
- 懒加载现在跟非懒加载一样,只是外部符号位置保存在
__DATA,__la_symbol_ptr
。 - 调用未定义的函数会走
dyld_stub_binder
,但dyld_stub_binder
实际上会走__dyld_missing_symbol_abort
终止程序,也就是说现在不存在实际意义的懒加载了。