關(guān)于dyld: Library not loaded那點(diǎn)事兒

背景

有個(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)如下圖:

mac-mach-o.jpg

然后執(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ō)明:

usedylib.jpg

又查到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)用:
frameworks.jpg

看得出它依賴(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)部


swiftapp.jpg

可以看到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/下:

frameworksinapp.jpg

把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:

quawindow-ref-appex.jpg

如果把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:

@path.jpg

在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:


20190718220100.jpg

確定了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:


set-insname.jpg

六.總結(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末尖阔,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌砂心,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蛇耀,死亡現(xiàn)場(chǎng)離奇詭異辩诞,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)纺涤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門(mén)译暂,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人撩炊,你說(shuō)我怎么就攤上這事外永。” “怎么了拧咳?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,340評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵伯顶,是天一觀(guān)的道長(zhǎng)。 經(jīng)常有香客問(wèn)我骆膝,道長(zhǎng)砾淌,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,449評(píng)論 1 279
  • 正文 為了忘掉前任谭网,我火速辦了婚禮汪厨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘愉择。我一直安慰自己劫乱,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布锥涕。 她就那樣靜靜地躺著衷戈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪层坠。 梳的紋絲不亂的頭發(fā)上殖妇,一...
    開(kāi)封第一講書(shū)人閱讀 49,166評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音破花,去河邊找鬼谦趣。 笑死疲吸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的前鹅。 我是一名探鬼主播摘悴,決...
    沈念sama閱讀 38,442評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼舰绘!你這毒婦竟也來(lái)了蹂喻?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,105評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤捂寿,失蹤者是張志新(化名)和其女友劉穎口四,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秦陋,經(jīng)...
    沈念sama閱讀 43,601評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡窃祝,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了踱侣。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片粪小。...
    茶點(diǎn)故事閱讀 38,161評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖抡句,靈堂內(nèi)的尸體忽然破棺而出探膊,到底是詐尸還是另有隱情,我是刑警寧澤待榔,帶...
    沈念sama閱讀 33,792評(píng)論 4 323
  • 正文 年R本政府宣布逞壁,位于F島的核電站,受9級(jí)特大地震影響锐锣,放射性物質(zhì)發(fā)生泄漏腌闯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評(píng)論 3 307
  • 文/蒙蒙 一雕憔、第九天 我趴在偏房一處隱蔽的房頂上張望姿骏。 院中可真熱鬧,春花似錦斤彼、人聲如沸分瘦。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,352評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)嘲玫。三九已至,卻和暖如春并扇,著一層夾襖步出監(jiān)牢的瞬間去团,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,584評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留土陪,地道東北人昼汗。 一個(gè)月前我還...
    沈念sama閱讀 45,618評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像旺坠,于是被迫代替她去往敵國(guó)和親乔遮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子扮超,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容