iOS最新外部符号加载

介绍

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终止程序,也就是说现在不存在实际意义的懒加载了。
相关推荐
游戏开发爱好者821 小时前
日常开发与测试的 App 测试方法、查看设备状态、实时日志、应用数据
android·ios·小程序·https·uni-app·iphone·webview
黑码哥21 小时前
ViewHolder设计模式深度剖析:iOS开发者掌握Android列表性能优化的实战指南
android·ios·性能优化·跨平台开发·viewholder
2501_915106321 天前
app 上架过程,安装包准备、证书与描述文件管理、安装测试、上传
android·ios·小程序·https·uni-app·iphone·webview
2501_915106321 天前
使用 Sniffmaster TCP 抓包和 Wireshark 网络分析
网络协议·tcp/ip·ios·小程序·uni-app·wireshark·iphone
熊猫钓鱼>_>1 天前
移动端开发技术选型报告:三足鼎立时代的开发者指南(2026年2月)
android·人工智能·ios·app·鸿蒙·cpu·移动端
徐同保2 天前
通过ip访问nginx的服务时,被第一个server重定向了,通过设置default_server解决这个问题
ios·iphone
2501_915918412 天前
在 iOS 环境下查看 App 详细信息与文件目录
android·ios·小程序·https·uni-app·iphone·webview
2501_916007472 天前
没有 Mac 用户如何上架 App Store,IPA生成、证书与描述文件管理、跨平台上传
android·macos·ios·小程序·uni-app·iphone·webview
夏幻灵3 天前
HTTPS全面解析:原理、加密机制与证书体
ios·iphone
TheNextByte13 天前
如何在iPhone上恢复已删除的笔记的综合指南
笔记·ios·iphone