注: 本文已過(guò)時(shí)。macosx > 10.14.99 則此方法不行。
馬上2020年了,所以你可能基本不需要用到iOS9模擬器了,本文純灌水,不感興趣的同學(xué)可以不看。
問(wèn)題描述
當(dāng)Xcode11工程中引入了iOS11才有的CoreML等庫(kù)(雖然庫(kù)是weak依賴的),運(yùn)行iOS9模擬器,啟動(dòng)時(shí)會(huì)崩潰,提示如下:
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時(shí),它加載了MacOSX系統(tǒng)路徑下的CoreML.framework,導(dǎo)致后續(xù)加載它依賴的庫(kù)時(shí),發(fā)生錯(cuò)誤告警。然而,iOS10的模擬器,同樣沒(méi)有CoreML庫(kù),為什么不會(huì)有這問(wèn)題?
貌似Mac OSX系統(tǒng)低于10.14.1 加載相同的iOS9模擬器鏡像也不會(huì)有問(wèn)題。
另外,附錄A提供了手動(dòng)安裝低版本Xcode模擬器的方法供參考。
分析
首先,我們知道應(yīng)用啟動(dòng)時(shí),針對(duì)模擬器,/usr/lib/dyld會(huì)加載模擬器鏡像下的dyld_sim,然后轉(zhuǎn)交給dyld_sim來(lái)加載模擬器版本的庫(kù)。
(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
因?yàn)閐yld是開源的,可以拿到源碼來(lái)更好地分析。先通過(guò)dyld_sim中的LC_SOURCE_VERSION的load command來(lái)查看對(duì)應(yīng)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
這里查看到對(duì)應(yīng)源碼版本390.7,同樣的,拿到iOS 10.1的dyld_sim對(duì)應(yīng)的源碼版本421.0
去dyld下載源碼,可以下載到421.0的源碼,但并沒(méi)有390.7的源碼,可以下個(gè)比較接近的360.22用來(lái)分析。
從錯(cuò)誤開始,查找mach-o, but not built for iOS simulator錯(cuò)誤提示。發(fā)現(xiàn)它是由isSimulatorBinary 函數(shù)判定的,通過(guò)該函數(shù)判定庫(kù)文件是Mac OSX還是模擬器版本的庫(kù),如果不是模擬器版,就會(huì)拋上述異常出去。
下面來(lái)分析360.22的源碼和421.0的源碼在判斷模擬器時(shí)的差別:

可以看到,兩個(gè)版本,判斷庫(kù)文件是否為模擬器,有兩個(gè)主要的區(qū)別:
- 低版本只讀取了一頁(yè)(4096,4k大小)的macho頭部,而高版本按
mh->sizeofcmds來(lái)確定cmdsReadEnd邊界(該版本最大可以取32K)。 - 超過(guò)邊界cmdsReadEnd,低版本返回true,而高版本返回false。
看下目前問(wèn)題的表現(xiàn),能否由這兩段差異的代碼來(lái)解釋。目前的問(wèn)題表現(xiàn)如下:
iOS9 dyld_sim,加載系統(tǒng)的
/System/Library/Frameworks/CoreML.frameworks時(shí),沒(méi)有報(bào)異常,把它當(dāng)模擬器版本加載了。iOS9 dyld_sim,加載
CoreML依賴的/System/Library/Frameworks/Accelerate.framework時(shí),卻提示它不是模擬器版,并崩潰。iOS10 dyld_sim,不會(huì)加載
CoreML,因?yàn)檎_識(shí)別它為模擬器版本了。老的MacOS(更低版本的CoreML.framework),iOS9 dyld_sim識(shí)別CoreML正常,模擬器可以正常運(yùn)行。
用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字節(jié),4080+20 > 4096,而Accelerate很小,沒(méi)有超過(guò)4096。
這里還要補(bǔ)充一個(gè)背景,新版本的CoreML.framework等系統(tǒng)庫(kù),除了load commands超過(guò)4K,還廢棄掉了LC_VERSION_MIN_MACOSX等load command,改用LC_BUILD_VERSION來(lái)描述系統(tǒng)版本。
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
所以,來(lái)總結(jié)下iOS9模擬器啟動(dòng)崩潰問(wèn)題的原因。新MacosX系統(tǒng)升級(jí)后(好像是10.14.1),/System/Library/Frameworks/CoreML.frameworks等庫(kù)廢棄了LC_VERSION_MIN_MACOSX等load command,導(dǎo)致之前的dyld_sim,無(wú)法判斷動(dòng)態(tài)庫(kù)是否模擬器版本。當(dāng)無(wú)法判斷時(shí),iOS10以下的dyld_sim對(duì)load command超過(guò)4K的庫(kù),認(rèn)為是模擬器版本進(jìn)行了加載,導(dǎo)致了后面的崩潰問(wèn)題。iOS10以上的dyld_sim沒(méi)法區(qū)別時(shí),默認(rèn)認(rèn)為不是模擬器版本,所以加載CoreML.frameworks時(shí)拋異常,但這個(gè)庫(kù)是weak依賴的,所以表現(xiàn)正常。
看起來(lái)理論可以解釋,但iOS9的模擬器沒(méi)有源碼,它的邏輯和390.7源碼的一致嗎?我們通過(guò)HopperDisassembler簡(jiǎn)單分析下這個(gè)isSimulatorBinary函數(shù)。

可以看到,除了多判斷了0x2f<=rsi<0x31(這里hopper錯(cuò)翻譯成rsi<0x31)其他邏輯和360.22基本一致。
這里也可以通過(guò)調(diào)試,來(lái)確認(rèn)邏輯。附錄B給出具體的調(diào)試方法,供參考。
解決
說(shuō)了這么多,你可能要問(wèn)了,一開始為啥模擬器要去加載Mac OSX里的動(dòng)態(tài)庫(kù)呢?原因是,dyld_sim默認(rèn)支持加載操作系統(tǒng)里任意路徑的動(dòng)態(tài)庫(kù),不過(guò)它會(huì)先加模擬器鏡像路徑前綴,沒(méi)找到才會(huì)嘗試原始路徑:

所以看起來(lái),像/System/Library/Frameworks/CoreML.frameworks這個(gè)庫(kù),只要在模擬器鏡像路徑(DYLD_ROOT_PATH)下沒(méi)找到,它就會(huì)找到Mac OSX下面去。
這里DYLD_ROOT_PATH=/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS\ 9.3.simruntime/Contents/Resources/RuntimeRoot
道理都懂,但怎么修復(fù)呢?我們無(wú)法修改dyld_sim、/System/Library/Frameworks/CoreML.frameworks,因?yàn)檫@些都是蘋果的庫(kù),做了codesign,改完無(wú)法加載。通過(guò)lldb動(dòng)態(tài)修改也不靠譜,因?yàn)闊o(wú)法確認(rèn)debugger接入時(shí)機(jī),可能attach上時(shí),CoreML都加載進(jìn)去了。
好在這里有個(gè)trick,可以成功騙過(guò)了dyld_sim。只要拷貝一個(gè)模擬器鏡像目錄下其他正常的庫(kù),如CoreFoundation.frameworks,并把它改名為CoreML.frameworks(記得也要改名里面的macho文件),那么它加載 DYLD_ROOT_PATH/System/Library/Frameworks/CoreML.frameworks時(shí)就可以正常加載而不會(huì)報(bào)錯(cuò)(實(shí)際加載的是CoreFoundation),就不會(huì)找到Mac OSX系統(tǒng)里去導(dǎo)致后面的問(wèn)題。完美~~~
同樣發(fā)現(xiàn), Vision.framework和Intents.framework也有相同的問(wèn)題??梢酝瑯硬僮饕话研迯?fù)相關(guān)問(wèn)題。
附錄A. Xcode安裝低版本模擬器
你的Xcode11不一定安裝了iOS9的模擬器。如果沒(méi)有安裝過(guò),需要手動(dòng)安裝(模擬器下載列表可能找不到iOS9了)。從iPhoneSimulatorSDK9_3 下載,并從dmg中提取出pkg安裝包。
我們知道,Xcode的模擬器鏡像都是存放在/Library/Developer/CoreSimulator/Profiles/Runtimes/iOS x.x.simruntime下,但從蘋果下載的pkg安裝時(shí)默認(rèn)是安裝到/下。我們可以安裝后把/Contents下的文件全部拷到上述鏡像目錄下,也可以像如下方法,修改pkg安裝路徑。
# 1. 解壓pkg到temp目錄
pkgutil --expand iPhoneSimulatorSDK9_3.pkg temp
# 2. 修改解包目錄里的PackageInfo文件,設(shè)置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. 調(diào)試dyld_sim
這里介紹如何調(diào)試本文描述的問(wèn)題。dyld啟動(dòng)時(shí),很快就加載完了,如果庫(kù)加載有問(wèn)題,很快就崩潰了,都跑不到代碼里(初始化的代碼也沒(méi)機(jī)會(huì)運(yùn)行)。但通過(guò)shell里的lldb用waitfor方式等待調(diào)試,在attach時(shí),它會(huì)自動(dòng)發(fā)送SIGSTOP,此時(shí)就可以看到dyld_sim的代碼。
? ~ lldb -n MyiOS9 -w
(lldb) process attach --name "MyiOS9" --waitfor
上述命令等待MyiOS9的模擬器App。點(diǎn)擊模擬器中的app,它就可以斷點(diǎn)進(jìn)去(當(dāng)然斷的時(shí)機(jī)不確定)。


同時(shí)加載的庫(kù)也不多。
(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*)
接下來(lái)就可以設(shè)置斷點(diǎn)了:
br set -a '0x000000010eba8000+17971+0x1000' -c '(int)strstr($rsi, "CoreML") != 0'
這里0x000000010eba8000是dyld_sim的隨機(jī)加載地址(通過(guò)image list查看),17971是函數(shù)相對(duì)dyld_sim.__TEXT.__text的偏移,0x1000是__TEXT.__text段在dyld_sim鏡像里的文件偏移。
你也可以調(diào)試loadPhase0、__cxa_throw、__cxa_begin_catch等函數(shù),如果函數(shù)在attach上之后運(yùn)行的話。
注,如果要調(diào)本文說(shuō)的CoreML的加載情況,最好給工程加個(gè)動(dòng)態(tài)庫(kù),讓這個(gè)動(dòng)態(tài)庫(kù)再去依賴CoreML,不然lldb attach上時(shí),基本CoreML的依賴解析已經(jīng)處理完了。