注: 本文已過時踩窖。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系統(tǒng)路徑下的CoreML.framework
翘魄,導致后續(xù)加載它依賴的庫時鼎天,發(fā)生錯誤告警。然而暑竟,iOS10的模擬器斋射,同樣沒有CoreML
庫,為什么不會有這問題但荤?
貌似Mac OSX系統(tǒng)低于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
錯誤提示。發(fā)現(xiàn)它是由isSimulatorBinary
函數判定的憎账,通過該函數判定庫文件是Mac OSX還是模擬器版本的庫套硼,如果不是模擬器版,就會拋上述異常出去胞皱。
下面來分析360.22
的源碼和421.0
的源碼在判斷模擬器時的差別:
可以看到邪意,兩個版本,判斷庫文件是否為模擬器反砌,有兩個主要的區(qū)別:
- 低版本只讀取了一頁(4096雾鬼,4k大小)的macho頭部,而高版本按
mh->sizeofcmds
來確定cmdsReadEnd邊界(該版本最大可以取32K)宴树。 - 超過邊界cmdsReadEnd策菜,低版本返回true,而高版本返回false酒贬。
看下目前問題的表現(xiàn)又憨,能否由這兩段差異的代碼來解釋。目前的問題表現(xiàn)如下:
iOS9 dyld_sim锭吨,加載系統(tǒng)的
/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字節(jié),4080+20 > 4096矛紫,而Accelerate
很小赎瞎,沒有超過4096。
這里還要補充一個背景颊咬,新版本的CoreML.framework
等系統(tǒng)庫务甥,除了load commands超過4K,還廢棄掉了LC_VERSION_MIN_MACOSX
等load command喳篇,改用LC_BUILD_VERSION
來描述系統(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
所以,來總結下iOS9模擬器啟動崩潰問題的原因麸澜。新MacosX系統(tǒng)升級后(好像是10.14.1)挺尿,/System/Library/Frameworks/CoreML.frameworks
等庫廢棄了LC_VERSION_MIN_MACOSX
等load command,導致之前的dyld_sim炊邦,無法判斷動態(tài)庫是否模擬器版本编矾。當無法判斷時,iOS10以下的dyld_sim對load command超過4K的庫馁害,認為是模擬器版本進行了加載窄俏,導致了后面的崩潰問題。iOS10以上的dyld_sim沒法區(qū)別時蜗细,默認認為不是模擬器版本裆操,所以加載CoreML.frameworks
時拋異常怒详,但這個庫是weak依賴的炉媒,所以表現(xiàn)正常。
看起來理論可以解釋昆烁,但iOS9的模擬器沒有源碼吊骤,它的邏輯和390.7
源碼的一致嗎?我們通過HopperDisassembler簡單分析下這個isSimulatorBinary
函數静尼。
可以看到白粉,除了多判斷了0x2f<=rsi<0x31
(這里hopper錯翻譯成rsi<0x31)其他邏輯和360.22
基本一致。
這里也可以通過調試鼠渺,來確認邏輯鸭巴。附錄B給出具體的調試方法,供參考拦盹。
解決
說了這么多鹃祖,你可能要問了,一開始為啥模擬器要去加載Mac OSX里的動態(tài)庫呢普舆?原因是恬口,dyld_sim默認支持加載操作系統(tǒng)里任意路徑的動態(tài)庫校读,不過它會先加模擬器鏡像路徑前綴,沒找到才會嘗試原始路徑:
所以看起來祖能,像/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動態(tài)修改也不靠譜,因為無法確認debugger接入時機太援,可能attach上時闽晦,CoreML都加載進去了。
好在這里有個trick提岔,可以成功騙過了dyld_sim仙蛉。只要拷貝一個模擬器鏡像目錄下其他正常的庫,如CoreFoundation.frameworks
碱蒙,并把它改名為CoreML.frameworks
(記得也要改名里面的macho文件)荠瘪,那么它加載 DYLD_ROOT_PATH/System/Library/Frameworks/CoreML.frameworks
時就可以正常加載而不會報錯(實際加載的是CoreFoundation),就不會找到Mac OSX系統(tǒng)里去導致后面的問題赛惩。完美~~~
同樣發(fā)現(xiàn)哀墓, 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時某饰,它會自動發(fā)送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的加載情況墓捻,最好給工程加個動態(tài)庫,讓這個動態(tài)庫再去依賴CoreML坊夫,不然lldb attach上時,基本CoreML的依賴解析已經處理完了撤卢。