注: 本文已过时。macosx > 10.14.99 则此方法不行。
马上2020年了,所以你可能基本不需要用到iOS9模拟器了,本文纯灌水,不感兴趣的同学可以不看。
问题描述
当Xcode11工程中引入了iOS11才有的CoreML
等库(虽然库是weak依赖的),运行iOS9模拟器,启动时会崩溃,提示如下:
dyld: Library not loaded: /System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate
Referenced from: /System/Library/Frameworks/CoreML.framework/CoreML
Reason: no suitable image found. Did find:
/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate: mach-o, but not built for iOS simulator
看提示,加载CoreML.framework
时,它加载了MacOSX系统路径下的CoreML.framework
,导致后续加载它依赖的库时,发生错误告警。然而,iOS10的模拟器,同样没有CoreML
库,为什么不会有这问题?
貌似Mac OSX系统低于10.14.1 加载相同的iOS9模拟器镜像也不会有问题。
另外,附录A提供了手动安装低版本Xcode模拟器的方法供参考。
分析
首先,我们知道应用启动时,针对模拟器,/usr/lib/dyld会加载模拟器镜像下的dyld_sim,然后转交给dyld_sim来加载模拟器版本的库。
(lldb) image list
[ 0] 97B86B7D-AF80-3222-B291-A0973B774C3B 0x000000010c0ec000 /Users/vincent/Library/Developer/CoreSimulator/Devices/F56758C0-3F87-4ED6-A373-CA542AD17C13/data/Containers/Bundle/Application/06ED7A2E-E9AA-4974-BC21-DF22D204180E/MyiOS9.app/MyiOS9
[ 1] CE635DB2-D47E-3C05-A0A3-6BD982E7E750 0x000000010e1b0000 /usr/lib/dyld
[ 2] 49268249-F1CD-35FC-BFFD-B4B8F3751B0D 0x000000010c0fe000 /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 9.3.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
因为dyld是开源的,可以拿到源码来更好地分析。先通过dyld_sim中的LC_SOURCE_VERSION
的load command来查看对应dyld源码版本:
➜ ~ otool -l /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 9.3.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim | grep -A 3 "LC_SOURCE_VERSION"
cmd LC_SOURCE_VERSION
cmdsize 16
version 390.7
这里查看到对应源码版本390.7
,同样的,拿到iOS 10.1
的dyld_sim对应的源码版本421.0
去dyld下载源码,可以下载到421.0
的源码,但并没有390.7
的源码,可以下个比较接近的360.22
用来分析。
从错误开始,查找mach-o, but not built for iOS simulator
错误提示。发现它是由isSimulatorBinary
函数判定的,通过该函数判定库文件是Mac OSX还是模拟器版本的库,如果不是模拟器版,就会抛上述异常出去。
下面来分析360.22
的源码和421.0
的源码在判断模拟器时的差别:
可以看到,两个版本,判断库文件是否为模拟器,有两个主要的区别:
- 低版本只读取了一页(4096,4k大小)的macho头部,而高版本按
mh->sizeofcmds
来确定cmdsReadEnd边界(该版本最大可以取32K)。 - 超过边界cmdsReadEnd,低版本返回true,而高版本返回false。
看下目前问题的表现,能否由这两段差异的代码来解释。目前的问题表现如下:
iOS9 dyld_sim,加载系统的
/System/Library/Frameworks/CoreML.frameworks
时,没有报异常,把它当模拟器版本加载了。iOS9 dyld_sim,加载
CoreML
依赖的/System/Library/Frameworks/Accelerate.framework
时,却提示它不是模拟器版,并崩溃。iOS10 dyld_sim,不会加载
CoreML
,因为正确识别它为模拟器版本了。老的MacOS(更低版本的CoreML.framework),iOS9 dyld_sim识别CoreML正常,模拟器可以正常运行。
用otool查看CoreML.framework
和Accelerate.framework
的sizeofcmds大小,如下:
➜ Frameworks otool -l CoreML.framework/CoreML| head -4
CoreML.framework/CoreML:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 6 23 4080 0x02918085
➜ Frameworks otool -l Accelerate.framework/Accelerate| head -4
Accelerate.framework/Accelerate:
Mach header
magic cputype cpusubtype caps filetype ncmds sizeofcmds flags
0xfeedfacf 16777223 3 0x00 6 17 968 0x02000085
CoreML
加上header的20字节,4080+20 > 4096,而Accelerate
很小,没有超过4096。
这里还要补充一个背景,新版本的CoreML.framework
等系统库,除了load commands超过4K,还废弃掉了LC_VERSION_MIN_MACOSX
等load command,改用LC_BUILD_VERSION
来描述系统版本。
Frameworks otool -l CoreML.framework/CoreML| grep -A 4 LC_BUILD_VERSION
cmd LC_BUILD_VERSION
cmdsize 32
platform 1
sdk 10.14
minos 10.14
所以,来总结下iOS9模拟器启动崩溃问题的原因。新MacosX系统升级后(好像是10.14.1),/System/Library/Frameworks/CoreML.frameworks
等库废弃了LC_VERSION_MIN_MACOSX
等load command,导致之前的dyld_sim,无法判断动态库是否模拟器版本。当无法判断时,iOS10以下的dyld_sim对load command超过4K的库,认为是模拟器版本进行了加载,导致了后面的崩溃问题。iOS10以上的dyld_sim没法区别时,默认认为不是模拟器版本,所以加载CoreML.frameworks
时抛异常,但这个库是weak依赖的,所以表现正常。
看起来理论可以解释,但iOS9的模拟器没有源码,它的逻辑和390.7
源码的一致吗?我们通过HopperDisassembler简单分析下这个isSimulatorBinary
函数。
可以看到,除了多判断了0x2f<=rsi<0x31
(这里hopper错翻译成rsi<0x31)其他逻辑和360.22
基本一致。
这里也可以通过调试,来确认逻辑。附录B给出具体的调试方法,供参考。
解决
说了这么多,你可能要问了,一开始为啥模拟器要去加载Mac OSX里的动态库呢?原因是,dyld_sim默认支持加载操作系统里任意路径的动态库,不过它会先加模拟器镜像路径前缀,没找到才会尝试原始路径:
所以看起来,像/System/Library/Frameworks/CoreML.frameworks
这个库,只要在模拟器镜像路径(DYLD_ROOT_PATH)下没找到,它就会找到Mac OSX下面去。
这里DYLD_ROOT_PATH=/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 9.3.simruntime/Contents/Resources/RuntimeRoot
道理都懂,但怎么修复呢?我们无法修改dyld_sim、/System/Library/Frameworks/CoreML.frameworks,因为这些都是苹果的库,做了codesign,改完无法加载。通过lldb动态修改也不靠谱,因为无法确认debugger接入时机,可能attach上时,CoreML都加载进去了。
好在这里有个trick,可以成功骗过了dyld_sim。只要拷贝一个模拟器镜像目录下其他正常的库,如CoreFoundation.frameworks
,并把它改名为CoreML.frameworks
(记得也要改名里面的macho文件),那么它加载 DYLD_ROOT_PATH/System/Library/Frameworks/CoreML.frameworks
时就可以正常加载而不会报错(实际加载的是CoreFoundation),就不会找到Mac OSX系统里去导致后面的问题。完美~~~
同样发现, Vision.framework和Intents.framework也有相同的问题。可以同样操作一把修复相关问题。
附录A. Xcode安装低版本模拟器
你的Xcode11不一定安装了iOS9
的模拟器。如果没有安装过,需要手动安装(模拟器下载列表可能找不到iOS9了)。从iPhoneSimulatorSDK9_3 下载,并从dmg
中提取出pkg
安装包。
我们知道,Xcode的模拟器镜像都是存放在/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS x.x.simruntime
下,但从苹果下载的pkg
安装时默认是安装到/
下。我们可以安装后把/Contents
下的文件全部拷到上述镜像目录下,也可以像如下方法,修改pkg
安装路径。
# 1. 解压pkg到temp目录
pkgutil --expand iPhoneSimulatorSDK9_3.pkg temp
# 2. 修改解包目录里的PackageInfo文件,设置install-location,类似如下:
<pkg-info auth="root" identifier="com.apple.pkg.iPhoneSimulatorSDK9_3" install-location="/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 9.3.simruntime/" postinstall-action="none" format-version="2" version="9.3.1.1460411551">
# 3. 重新生成pkg包
pkgutil --flatten temp MyiPhoneSimulatorSDK9_3.new.pkg
# 4. 双击新的pkg安装,并看镜像目录下是否有iOS 9.3.simruntime的目录
安装完后,重启Xcode,在模拟器列表中就可以看到9.3
版本的模拟器了。
附录B. 调试dyld_sim
这里介绍如何调试本文描述的问题。dyld启动时,很快就加载完了,如果库加载有问题,很快就崩溃了,都跑不到代码里(初始化的代码也没机会运行)。但通过shell里的lldb用waitfor方式等待调试,在attach时,它会自动发送SIGSTOP,此时就可以看到dyld_sim的代码。
➜ ~ lldb -n MyiOS9 -w
(lldb) process attach --name "MyiOS9" --waitfor
上述命令等待MyiOS9的模拟器App。点击模拟器中的app,它就可以断点进去(当然断的时机不确定)。
同时加载的库也不多。
(lldb) image list
[ 0] 97B86B7D-AF80-3222-B291-A0973B774C3B 0x000000010eb2a000 /Users/vincent/Library/Developer/CoreSimulator/Devices/F56758C0-3F87-4ED6-A373-CA542AD17C13/data/Containers/Bundle/Application/06ED7A2E-E9AA-4974-BC21-DF22D204180E/MyiOS9.app/MyiOS9
[ 1] CE635DB2-D47E-3C05-A0A3-6BD982E7E750 0x00000001152d1000 /usr/lib/dyld
[ 2] 49268249-F1CD-35FC-BFFD-B4B8F3751B0D 0x000000010eb3c000 /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 9.3.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim
[ 3] 29506256-362E-38EB-9D36-D03C6D57E363 0x000000010eba8000 /Users/vincent/Library/Developer/CoreSimulator/Devices/F56758C0-3F87-4ED6-A373-CA542AD17C13/data/Containers/Bundle/Application/06ED7A2E-E9AA-4974-BC21-DF22D204180E/MyiOS9.app/Frameworks/lolz.dylib
查看isSimulatorBinary
位置:
(lldb) image lookup -rn "isSimulatorBinary" dyld_sim
1 match found in /Library/Developer/CoreSimulator/Profiles/Runtimes/iOS 9.3.simruntime/Contents/Resources/RuntimeRoot/usr/lib/dyld_sim:
Address: dyld_sim[0x00007fff5fc05633] (dyld_sim.__TEXT.__text + 17971)
Summary: dyld_sim`dyld::isSimulatorBinary(unsigned char const*, char const*)
接下来就可以设置断点了:
br set -a '0x000000010eba8000+17971+0x1000' -c '(int)strstr($rsi, "CoreML") != 0'
这里0x000000010eba8000
是dyld_sim的随机加载地址(通过image list查看),17971是函数相对dyld_sim.__TEXT.__text的偏移,0x1000是__TEXT.__text段在dyld_sim镜像里的文件偏移。
你也可以调试loadPhase0
、__cxa_throw
、__cxa_begin_catch
等函数,如果函数在attach上之后运行的话。
注,如果要调本文说的CoreML的加载情况,最好给工程加个动态库,让这个动态库再去依赖CoreML,不然lldb attach上时,基本CoreML的依赖解析已经处理完了。