解决Xcode11无法运行iOS9模拟器问题

注: 本文已过时。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的源码在判断模拟器时的差别:

WeChatWorkScreenshot_f6bde04d-a64f-4ea8-99a8-89d8a6c4bda5.png

可以看到,两个版本,判断库文件是否为模拟器,有两个主要的区别:

  • 低版本只读取了一页(4096,4k大小)的macho头部,而高版本按mh->sizeofcmds来确定cmdsReadEnd边界(该版本最大可以取32K)。
  • 超过边界cmdsReadEnd,低版本返回true,而高版本返回false。

看下目前问题的表现,能否由这两段差异的代码来解释。目前的问题表现如下:

  1. iOS9 dyld_sim,加载系统的/System/Library/Frameworks/CoreML.frameworks时,没有报异常,把它当模拟器版本加载了。

  2. iOS9 dyld_sim,加载CoreML依赖的/System/Library/Frameworks/Accelerate.framework时,却提示它不是模拟器版,并崩溃。

  3. iOS10 dyld_sim,不会加载CoreML,因为正确识别它为模拟器版本了。

  4. 老的MacOS(更低版本的CoreML.framework),iOS9 dyld_sim识别CoreML正常,模拟器可以正常运行。

用otool查看CoreML.frameworkAccelerate.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函数。

WeChatWorkScreenshot_25d0f1a2-c240-4328-aa14-0675f32ecac9.png

可以看到,除了多判断了0x2f<=rsi<0x31(这里hopper错翻译成rsi<0x31)其他逻辑和360.22基本一致。

这里也可以通过调试,来确认逻辑。附录B给出具体的调试方法,供参考。

解决

说了这么多,你可能要问了,一开始为啥模拟器要去加载Mac OSX里的动态库呢?原因是,dyld_sim默认支持加载操作系统里任意路径的动态库,不过它会先加模拟器镜像路径前缀,没找到才会尝试原始路径:

source3.png

所以看起来,像/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,类似如下:



# 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,它就可以断点进去(当然断的时机不确定)。

lldb1.png
lldb2.png

同时加载的库也不多。

(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的依赖解析已经处理完了。

参考资料

  1. XNU、dyld源码分析Mach-O和动态库的加载过程

  2. dyld:iOS8 and iOS9 simulators in macOS 10.14.1 system crash caused by dyld loading error dynamic library

你可能感兴趣的:(解决Xcode11无法运行iOS9模拟器问题)