背景
有個(gè)牛逼同事用QT在開(kāi)發(fā)一Mac小應(yīng)用,找到我說(shuō)他引用了一個(gè)zip解壓縮的庫(kù).在QT的IDE運(yùn)行起來(lái)之后,就崩潰.看控制臺(tái)的報(bào)錯(cuò)信息大概如下
dyld: Library not loaded: libquazip.1.dylib
Referenced from: /Users/USER/Documents/quawindow.app/Contents/MacOS/quawindow
Reason: image not found
看起來(lái)就是應(yīng)用啟動(dòng)的時(shí)候嘗試加載libquazip.1.dylib, 但是卻找不到.
作為一個(gè)iOS/Mac開(kāi)發(fā).首先想到的是用Xcode將工程跑起來(lái)調(diào)試.但是臥槽QT是自己的IDE.
不是自己熟悉的開(kāi)發(fā)環(huán)境,而且QT工程構(gòu)建出來(lái)的結(jié)果只是一個(gè)Mac可執(zhí)行的.app的程序.
現(xiàn)在只能憑借自己的開(kāi)發(fā)經(jīng)驗(yàn)意識(shí),與這個(gè)熟悉QT開(kāi)發(fā)的同事一起一點(diǎn)點(diǎn)的嘗試探索問(wèn)題入口.
一.檢查QT工程配置里關(guān)于Mac上加載dylib相關(guān)的配置
確認(rèn)了QT工程關(guān)于libquazip.1.dylib這個(gè)庫(kù)的加載路徑以及鏈接選項(xiàng)的配置都是沒(méi)有問(wèn)題的,也去搜索QT配置鏈接動(dòng)態(tài)庫(kù)的相關(guān)文檔以及博客.基本上也都是該做的都做了.
到這里似乎真的是有點(diǎn)陷入僵局.
然后我冷靜了下,試圖從結(jié)果出發(fā)逆向思考去分析:
第一.可執(zhí)行的.app的程序確實(shí)是想要鏈接libquazip.1.dylib的,就是死活找不到.
第二.libquazip.1.dylib這個(gè)庫(kù)也確實(shí)是存在的.但是它沒(méi)有被找到
想想看,一個(gè)事物確實(shí)存在,但另外一個(gè)確實(shí)想用它的人卻找不到.說(shuō)明什么?
說(shuō)明沒(méi)有找對(duì)路子啊~沒(méi)有找對(duì)路子.至少有兩方面的原因:
第一,這個(gè)存在的事物沒(méi)有給對(duì)信息,讓別人找到它,
第二,用它的人找它的途徑出了差錯(cuò)
帶著這個(gè)逆向思維繼續(xù)向下探索......
二.利用otool命令檢查Mach-O文件鏈接信息
現(xiàn)在給我的就只有這個(gè)QT構(gòu)建出的可執(zhí)行.app的程序.作為問(wèn)題查找的源頭.
接著當(dāng)時(shí)突然想到自己搞iOS逆向研究的時(shí)候,有一個(gè)otool命令可以顯示Mach-O文件的結(jié)構(gòu)信息.
Mach-O就是iOS/Mac可執(zhí)行程序的定義格式.
關(guān)于.app與可執(zhí)行二進(jìn)制Mach-O的目錄結(jié)構(gòu)如下圖:
然后執(zhí)行:
otool -L /Users/hxq/Documents/quawindow.app/Contents/MacOS/quawindow
-L表示顯示當(dāng)前可執(zhí)行程序要鏈接哪些動(dòng)態(tài)庫(kù)
/Users/hxq/Documents/quawindow.app/Contents/MacOS/quawindow:
libquazip.1.dylib (compatibility version 1.0.0, current version 1.0.0)
@rpath/QtWidgets.framework/Versions/5/QtWidgets (compatibility version 5.13.0, current version 5.13.0)
@rpath/QtGui.framework/Versions/5/QtGui (compatibility version 5.13.0, current version 5.13.0)
@rpath/QtCore.framework/Versions/5/QtCore (compatibility version 5.13.0, current version 5.13.0)
/System/Library/Frameworks/DiskArbitration.framework/Versions/A/DiskArbitration (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit (compatibility version 1.0.0, current version 275.0.0)
/System/Library/Frameworks/OpenGL.framework/Versions/A/OpenGL (compatibility version 1.0.0, current version 1.0.0)
/System/Library/Frameworks/AGL.framework/Versions/A/AGL (compatibility version 1.0.0, current version 1.0.0)
/usr/lib/libc++.1.dylib (compatibility version 1.0.0, current version 400.9.4)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1252.250.1)
看到它確實(shí)指名點(diǎn)姓的要去加載libquazip.1.dylib的,那么問(wèn)題出現(xiàn)到哪里了?
細(xì)心觀(guān)察對(duì)比可以發(fā)現(xiàn)下面這些動(dòng)態(tài)庫(kù),
@rpath/QtWidgets.framework/Versions/5/QtWidgets
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit
/usr/lib/libc++.1.dylib
可以看出來(lái)上面這些QT開(kāi)發(fā)依賴(lài)的QtWidgets等等,還有系統(tǒng)IOKit,libc++等動(dòng)態(tài)庫(kù),顯示都是有明確的指明路徑的.而唯獨(dú)出問(wèn)題的libquazip.1.dylib只有個(gè)名字,沒(méi)有路徑指示
那就嘗試一下修改對(duì)于libquazip.1.dylib的鏈接信息:利用install_name_tool -change命令把quawindow對(duì)libquazip.1.dylib的引用路徑指向一個(gè)明確的路徑/Users/hxq/Documents/libquazip.1.0.0.dylib
install_name_tool -change libquazip.1.dylib /Users/hxq/Documents/libquazip.1.0.0.dylib /Users/hxq/Documents/quawindow.app/Contents/MacOS/quawindow
再otool -L一下quawindow:
/Users/hxq/Documents/quawindow.app/Contents/MacOS/quawindow:
/Users/hxq/Documents/libquazip.1.0.0.dylib (compatibility version 1.0.0, current version 1.0.0)
........
確認(rèn)修改生效,然后雙擊quawindow.app,運(yùn)行起來(lái)了不再崩潰!問(wèn)題找到了,就是加載路徑的問(wèn)題!
接下來(lái)是要研究下mac os系統(tǒng)下的dylib特性以及加載機(jī)制....
三.探究dylib
dylib(dynamic library)是蘋(píng)果動(dòng)態(tài)函數(shù)庫(kù),在應(yīng)用程序編譯的時(shí)候, 不會(huì)編譯進(jìn)二進(jìn)制目標(biāo)代碼中, 只有當(dāng)程序里執(zhí)行相應(yīng)的函數(shù)才調(diào)用該函數(shù)庫(kù)里對(duì)應(yīng)的函數(shù)锅尘。
當(dāng)應(yīng)用程序啟動(dòng)的時(shí)候,有一個(gè)叫做動(dòng)態(tài)連接器和加載器dyld會(huì)尋找辛藻,加載镐牺,連接動(dòng)態(tài)庫(kù).
因此上面由于加載了路徑未明確libquazip.1.0.0.dylib而崩潰的時(shí)刻,就是發(fā)生在啟動(dòng)的時(shí)候.
dylib有一個(gè)很重要的屬性叫做install name,比較蛋疼的是其實(shí)它不單是名字,必須是一個(gè)路徑.它的作用是為了告訴想要鏈接它的可執(zhí)行程序或者其他庫(kù),要從哪里找到它.
蘋(píng)果官方文檔也有說(shuō)明:
又查到otool -D 命令可以顯示某個(gè)dylib的install name屬性:
otool -D /Users/hxq/Documents/libquazip.1.0.0.dylib
/Users/hxq/Documents/libquazip.1.0.0.dylib:
libquazip.1.dylib
顯示出來(lái)之前被quawindow鏈接的libquazip.1.0.0.dylib的install name是libquazip.1.dylib.
到這里就進(jìn)一步看出問(wèn)題了!!! 按照蘋(píng)果的規(guī)定install name必須是個(gè)路徑才對(duì)!!
因此我們需要把鏈接的libquazip.1.0.0.dylib的install name修改成一個(gè)正確的路徑.
接下來(lái)就要好好探究一下dylib加載路徑的規(guī)則機(jī)制.
四.dylib加載路徑
通常依賴(lài)dylib會(huì)有兩種方式:
一.放置于系統(tǒng)某個(gè)公共目錄,可被多個(gè)應(yīng)用進(jìn)程依賴(lài),運(yùn)行時(shí)調(diào)用:
最典型的案例是系統(tǒng)庫(kù):
/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit
/usr/lib/libc++.1.dylib
這種方式就很簡(jiǎn)單了.只需把dylib的install name指定成固定的絕對(duì)路徑即可.
二.嵌入到應(yīng)用程序中
很多時(shí)候單一應(yīng)用程序依賴(lài)一些動(dòng)態(tài)庫(kù).為了避免應(yīng)用發(fā)布的時(shí)候需要同步安裝所依賴(lài)的動(dòng)態(tài)庫(kù)帶來(lái)的繁瑣,就把所有依賴(lài)的dylib一個(gè)放入xx.app里面.
場(chǎng)景一:比如上面的QT構(gòu)建出來(lái)的Mac應(yīng)用:
看得出它依賴(lài)了很多跟QT開(kāi)發(fā)環(huán)境有關(guān)的組件庫(kù).
場(chǎng)景二:解釋Swift5的ABI 穩(wěn)定后為什么包體會(huì)減小
先看用xcode9.4創(chuàng)建的基于Swift語(yǔ)言的空工程構(gòu)建出來(lái)的.app內(nèi)部
可以看到app里面Frameworks目錄下放了很多關(guān)于Swift核心的動(dòng)態(tài)庫(kù).
而Swift5 (或以上) ABI穩(wěn)定后, Apple 會(huì)把Swift runtime相關(guān)的庫(kù)弄到 iOS 和 macOS 系統(tǒng)里公共目錄.這樣就不用每個(gè)app都留存一份.包體自然就會(huì)減小.
讀者可以自己嘗試用xcode10.2創(chuàng)建基于Swift語(yǔ)言的空工程構(gòu)建出app去驗(yàn)證.
好.通過(guò)案例深化dyld的應(yīng)用形式后,
繼續(xù)介紹動(dòng)態(tài)庫(kù)嵌入app時(shí),如何指定dylib加載路徑(install name):
三個(gè)環(huán)境變量出場(chǎng):
- @executable_path
- @loader_path
- @rpath
非常重要的提示:這三個(gè)環(huán)境變量?jī)H用于嵌入app里面的dyld的install name指定的時(shí)候!!!
1.@executable_path 這個(gè)變量表示可執(zhí)行程序所在的目錄
這里假使(假設(shè)是因?yàn)榇丝虇?wèn)題還沒(méi)有解決嘛),libquazip.1.dylib是經(jīng)過(guò)正確的工程配置構(gòu)建后,放在quawindow.app/Contents/Frameworks/下:
把libquazip.1.dylib的install name指定為@executable_path/../Frameworks/libquazip.1.dylib
otool -D /Users/hxq/Documents/quawindow.app/Contents/Frameworks/libquazip.1.dylib
/Users/hxq/Documents/quawindow.app/Contents/Frameworks/libquazip.1.dylib:
@executable_path/../Frameworks/libquazip.1.dylib
這里@executable_path就等于/Users/USER/Documents/quawindow.app/Contents/MacOS/quawindow
2.@loader_path 作為@executable_path的靈活增強(qiáng)版,表示任意一個(gè)某時(shí)刻被加載的mach-o文件(包括App, dylib, framework,appex等)所在的目錄.
因此在單一app下可執(zhí)行文件時(shí)候,@loader_path等價(jià)于@executable_path.
那么@loader_path的靈活性怎么體現(xiàn)呢,舉一個(gè)例子吧:
假如quawindow.app引用了一個(gè)插件Share.appex,位于quawindow.app/Contents/Extention/Share.appex
Share.appex,
Share.appex又引用了libquazip.1.dylib,位quawindow.app/Contents/Extention/Share.appex/Contents/Frameworks/libquazip.1.dylib:
如果把libquazip.1.dylib的install name指定為@loader_path/../Frameworks/libquazip.1.dylib的話(huà)
此時(shí):
@loader_path等于/Users/hxq/Documents/quawindow.app/Contents/Extention/Share.appex/Contents/MacOS/Share
@executable_path依舊等于/Users/USER/Documents/quawindow.app/Contents/MacOS/quawindow
因此使用@loader_path設(shè)定libquazip.1.dylib加載路徑,能夠保證不論被引用的Share.appex放入quawindow.app里面的任意位置,都能夠讓libquazip.1.dylib正確的加載.
3. @rpath 又進(jìn)一步增強(qiáng)靈活性
在前兩種@executable_path,@loader_path的設(shè)定機(jī)制里,被引入的dylib占據(jù)查找主動(dòng)權(quán):我來(lái)指定用我的人
怎么找到我,顯得比較傲嬌.
而@rpath出現(xiàn)后,使得主動(dòng)權(quán)站在了引用dylib的應(yīng)用程序這邊.
例如把libquazip.1.dylib的install name指定為@rpath/libquazip.1.dylib后,指定它加載路徑歸屬權(quán)就交給了引用它的quawindow.app.
要在編譯時(shí)候去指定quawindow.app的@rpath
注意哦~剛才是libquazip.1.dylib有一個(gè)@rpath設(shè)定,現(xiàn)在編譯quawindow.app也需要設(shè)定@rpath:
在xcode工程里Build Settings設(shè)置 Runpath Search Paths(對(duì)應(yīng)了@rpath)
這樣整體的加載流程就是:
quawindow.app啟動(dòng)查找引用的libquazip.1.dylib路徑,發(fā)現(xiàn)其install name是@rpath, 發(fā)現(xiàn)主動(dòng)權(quán)在自己手中.就立馬去查找自身設(shè)定的@rpath,設(shè)定為@executable_path/../Frameworks, @loader_path/../Frameworks
然后@executable_path或者@loader_path都被解析成了/Users/USER/Documents/quawindow.app/Contents/MacOS/quawindow
既而@executable_path/../Frameworks成功找到Frameworks下的libquazip.1.dylib.
到此為止,關(guān)于dylib的加載機(jī)制,路徑查找設(shè)定都搞清楚了....接下來(lái)終于可以解決文章一開(kāi)頭dyld: Library not loaded: libquazip.1.dylib的問(wèn)題了
五.正式解決dyld: Library not loaded崩潰問(wèn)題
現(xiàn)在就是很清晰明白了,就是libquazip.1.dylib路徑找不對(duì)的問(wèn)題.
怎么解決? 使用install_name_tool命令重新設(shè)定libquazip.1.dylib的install name.
設(shè)定之前,先考慮libquazip.1.dylib的使用方式,通過(guò)分析根據(jù)QT構(gòu)建Mac應(yīng)用的規(guī)律,決定采用將libquazip.1.dylib嵌入quawindow.app形式.
在.pro文件中添加: (不會(huì)QT的直接忽略這個(gè),不用理解,只關(guān)心結(jié)果即可,而且不影響上面所有關(guān)于dyld的知識(shí)點(diǎn)的理解)
macx {
plugins.path = Contents/Plugins/zip
plugins.files = ./lib/libquazip.1.dylib
QMAKE_BUNDLE_DATA += plugins
}
上面的配置,就使得構(gòu)建后,能將libquazip.1.dylib拷貝到quawindow.app/Contents/PlugIns/zip/libquazip.1.dylib:
確定了libquazip.1.dylib位置后就可以去修改libquazip.1.dylib的install name:
install_name_tool -id "@loader_path/../Plugins/zip/libquazip.1.dylib" libquazip.1.dylib
使用@executable_path也可以.
至此問(wèn)題完美解決
補(bǔ)充
前面提到使用install_name_tool去修改已生成的dylib的install name,那么怎么在構(gòu)建動(dòng)態(tài)庫(kù)的xcode工程里面設(shè)定這個(gè)install name:
六.總結(jié)
1.徹底研究了Mac/iOS 動(dòng)態(tài)庫(kù)的機(jī)制,尤其是路徑查找設(shè)定規(guī)則.
2.再次感受到逆向分析二進(jìn)制的重要,otool install_name_tool命令大大的好用.
參考文章
dylib淺析
探秘 Mach-O 文件
install_name_tool to update a executable to search for dylib in Mac OS X
Build Settings中的變量@rpath,@loader_path,@executable_path
Dynamic Library Programming Topics