技能回顧
在上一篇文章靜態庫、Framework 的鏈接與合并我們詳細的講解了靜態庫是什么,鏈接一個靜態庫需要的三大要素是什么,有需要的小伙伴請前去查看。
下面我們來通過實踐的方式 深入理解動態庫 以及鏈接一個動態庫都有哪些坑
動態庫
首先準備環境
- 看到上面的環境就知道我們要干嘛了。首先將A_Manager.m 編譯為dylib 。然后將test.m 和dylib進行鏈接 生成可執行文件,
shell 腳本搞起
echo "-----開始編譯test.m"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-I./dylib \
-c test.m -o test.o
echo "-----開始進入dylib目錄"
pushd ./dylib
echo "-----開始編譯A_Manager.m 為 A_Manager.o"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-c A_Manager.m -o A_Manager.o
echo "-----A_Manager.o 編譯為 libA_Manager.dylib"
# -dynamiclib: 告訴clang我要編譯的是動態庫
clang -dynamiclib \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
A_Manager.o -o libA_Manager.dylib
popd
echo "-----鏈接 libA_Manager.dylib 生成 test EXEC"
clang -target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-L./dylib \
-lA_Manager \
test.o -o test
第一步 將test.m 編譯為.o 因為.m 中 引用著A_Manager.h 及方法。所以需要-I(大i)來告訴clang 頭文件的路徑,其他的不必多說。
第二步 進入dylib路徑
1、將 A_Manger.m編譯為.o
2、將.o 編譯為 dylib (這里用到了一個參數 -dynamiclib 它是告訴編譯器要編譯成動態庫)第三方 返回test.m所在目錄 并將test.o 鏈接 libA_manager.dylib生成 test ExEC
這里可以看到 鏈接庫需要-L(大L)庫所在的位置 -l(小L)xxxx 庫的名稱
鏈接成功后我們運行test可執行文件
dyldTest lldb -file test
(lldb) target create "test"
r
Current executable set to 'test' (x86_64).
(lldb) r
Process 723 launched: '/Users/liuhao/Desktop/dyldTest/test' (x86_64)
dyld: Library not loaded: libA_Manager.dylib
Referenced from: /Users/liuhao/Desktop/dyldTest/test
Reason: image not found
Process 723 stopped
* thread #1, stop reason = signal SIGABRT
frame #0: 0x000000010003424a dyld`__abort_with_payload + 10
dyld`__abort_with_payload:
-> 0x10003424a <+10>: jae 0x100034254 ; <+20>
0x10003424c <+12>: movq %rax, %rdi
0x10003424f <+15>: jmp 0x100033aa8 ; cerror_nocancel
0x100034254 <+20>: retq
Target 0: (test) stopped.
(lldb)
- 可以看到報了一個經典的錯誤 image not found
動態庫的本質
為了鬧清楚這個錯誤 我們需要理解 動態庫到底是個什么東西,上片文章我們有實操,如何將一個.o 直接變成一個靜態庫,也有說過靜態庫是一個.o文件的合集,而動態庫是編譯鏈接的最終產物,那也就意味著 能不能將一個.a鏈接生成一個動態庫呢?當然是可以的,因為它是一個.o文件,.o文件是不是可以進一步生成可執行文件,或者 動態庫。下面我們就來修改我們的腳本嘗試著將A_Manager 編譯為一個.a 文件. 再去鏈接生成一個動態庫
上片文章我們是通過ar將一個.o生成靜態庫
echo "-----開始將目標文件打包為 libA_Manager.a"
ar -rc libA_Manager.a A_Manager.o
今天我們在用xcode內置的libtool 命令
libtool -static -arch_only x86_64 A_Manager.o -o libA_Manager.a
- -static 告訴libtool 我要編譯為一個靜態庫
- -arch_only 指定架構
- 后面跟上你要把哪些.o 編譯為靜態庫
那編譯為.a了那怎樣將它鏈接生成動態庫呢?
用 clang 也可以 為了讓大家更好的理解 ld 鏈接器,下面直接給ld傳遞參數 生成動態庫
ld -dylib -arch x86_64 \
-macosx_version_min 10.13 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-lsystem -framework Foundation \
libA_Manager.a -o libA_Manager.dylib
- -dylib 告訴ld 是要把一個.a 或者一個.o 鏈接生成一個動態庫。
- -arch x86_64 指定架構
- -macosx_version_min 告訴ld 支持的最小版本
- -syslibroot 指定用到的系統的如Foundation 的sdk到底在哪個路徑 (有點像clang的參數 -isysroot)
- 再去告訴它需要鏈接哪些庫 這里需要 兩個系統庫作為支撐
第一個 -lsystem :這個給我們提供 dylib dyld 相關的一些鏈接器的函數的
第二個 Foundation : 這里們用到了NSLog - 然后將libA_Manager.a 生成 libA_Manager.dylib
知道了命令下面修改腳本
echo "-----開始編譯test.m"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-I./dylib \
-c test.m -o test.o
echo "-----開始進入dylib目錄"
pushd ./dylib
echo "-----開始編譯A_Manager.m 為 A_Manager.o"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-c A_Manager.m -o A_Manager.o
echo "----- 編譯A_Manager.o 為 libA_Manager.a"
# Xcode->靜態庫
libtool -static -arch_only x86_64 A_Manager.o -o libA_Manager.a
# -dynamiclib:動態庫
#echo "-----A_Manager.o 編譯為 libA_Manager.dylib"
#
#clang -dynamiclib \
#-target x86_64-apple-macos10.13 \
#-fobjc-arc \
#-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
# A_Manager.o -o libA_Manager.dylib
echo "----- 通過ld 把 libA_Manager.a 鏈接去生成 libA_Manager.dylib"
ld -dylib -arch x86_64 \
-macosx_version_min 10.13 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-lsystem -framework Foundation \
libA_Manager.a -o libA_Manager.dylib
popd
echo "-----鏈接 libA_Manager.dylib 生成 test EXEC"
clang -target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-L./dylib \
-lA_Manager \
test.o -o test
運行腳本
- 運行發現報錯了沒有找到上述符號 在鏈接libA_Manager.dylib 生成可執行文件的時候,上述符號找不到,未定義的符號
- 那為啥會出現這個問題呢?
我們來看一下出問題的腳本
echo "-----鏈接 libA_Manager.dylib 生成 test EXEC"
clang -target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-L./dylib \
-lA_Manager \
test.o -o test
在看一眼test.m
-
那請問對于??所使用的A_Manager 還有所調用的方法 是上述腳本中 -L./dylib 的導出還是導入?想什么呢那肯定是導出符號了,那也就意味著 我們的動態庫的導出符號表中并不存在我用到的符號 下面我看一下libA_Manager.dylib的導出符號表
屏幕快照 2021-04-07 下午10.10.16.png - 發現空空如也啥也沒有。那為啥呢?在回到上面我們修改的腳本 哪里出問題了呢?
echo "----- 通過ld 把 libA_Manager.a 鏈接去生成 libA_Manager.dylib"
ld -dylib -arch x86_64 \
-macosx_version_min 10.13 \
-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-lsystem -framework Foundation \
libA_Manager.a -o libA_Manager.dylib
- 我們將一個.a 通過鏈接器ld 生成一個動態庫,符合我們上篇文章講解的-noall_load ,它默認是-noall_load 此時 libA_Manager.dylib 動態庫在鏈接 libA_Manager.a 并沒有使用libA_Manager.a 中的代碼,所以要給它拼接一個 -all_load 或者-Objc 都可以
接下來繼續運行腳本
- 可以看到此時鏈接成功 可執行文件test Mach-O 被完美的編譯鏈接出來
- 查看動態庫的導出符號 發現也被完美的導出了。
繼續運行 test 可執行文件
- 依舊是上面最原始的問題 image not found
先總一下
- 靜態庫是.o 文件的合集
- 動態庫是.o文件鏈接過后的產物 (所以就是說為啥我們可以將一個.a鏈接生成一個dylib)
- 動態庫是 最終鏈接產物 并不能進行合并
- 動態庫要比靜態庫多走一次鏈接的過程。它是和我們的exec 是同一級別的。
解決 image not found
LC_LOAD_DYLIB
為什么會出現這個問題,這就要從我們的dyld去加載動態來說起
- 當dyld 去加載Mach-O的時候,通過它里面的一個Load command LC_LOAD_DYLIB 去查找它所使用的動態庫。
也就是LC_LOAD_DYLIB 保存著 上面我們的test可執行文件的用到的動態庫的路徑 - 因為動態庫是運行時的時候加載的
我們來看一下test的Load command 可以用otool 也可用 objdump
- 可以清晰的看到我們自己的動態庫并沒有有將路徑寫入
解決
LC_ID_DYLIB
既然知道為啥了那就好辦了,我直接給他搞一個路徑不就可以了嗎 ?怎么搞?其實在編譯鏈接成動態庫庫文件的時候,在它自己的mach-O中存在一個command 來告訴外界,我在哪里。這個 Load command 的cmd 就是LC_ID_DYLIB,我們來看一下libA_Manager.dylib
- 可以看到和test mach-o里libA_Manager.dylib 信息的內容一樣
可通過外置命令來修改
- 描述:改變動態庫的安裝目錄
下面我們找到動態庫所在的目錄 并通過命令將其修改
- 我們 通過 install_name_tool -id 將它的絕對路徑寫入
- 這里需要注意上面的報錯 原因只寫了路徑 沒告訴它往誰身
再次查看libA_Manager.dylib的LC_ID_DYLIB
- 可以看到被我們成功的寫入了
- 改了意味著我們需要主工程重新鏈接 動態庫
修改腳本 屏蔽掉libA_Manager.dylib的編譯及生成步驟,直接重新編譯 test 鏈接我們用命令修改的 dylib
echo "-----開始編譯test.m"
clang -x objective-c \
-target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-I./dylib \
-c test.m -o test.o
#echo "-----開始進入dylib目錄"
#pushd ./dylib
#echo "-----開始編譯A_Manager.m 為 A_Manager.o"
#clang -x objective-c \
#-target x86_64-apple-macos10.13 \
#-fobjc-arc \
#-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
#-c A_Manager.m -o A_Manager.o
#
#echo "----- 編譯A_Manager.o 為 libA_Manager.a"
# Xcode->靜態庫
#libtool -static -arch_only x86_64 A_Manager.o -o libA_Manager.a
# -dynamiclib:動態庫
#echo "-----A_Manager.o 編譯為 libA_Manager.dylib"
#
#clang -dynamiclib \
#-target x86_64-apple-macos10.13 \
#-fobjc-arc \
#-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
# A_Manager.o -o libA_Manager.dylib
#echo "----- 通過ld 把 libA_Manager.a 鏈接去生成 libA_Manager.dylib"
#
#ld -dylib -arch x86_64 \
#-macosx_version_min 10.13 \
#-syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
#-lsystem -framework Foundation \
#-all_load \
#libA_Manager.a -o libA_Manager.dylib
#popd
echo "-----鏈接 libA_Manager.dylib 生成 test EXEC"
clang -target x86_64-apple-macos10.13 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk \
-L./dylib \
-lA_Manager \
test.o -o test
運行腳本后查看test的Load command LC_LOAD_DYLIB
- 可以看到被我們成功的修改
運行 test
總結
動態庫保存自己的位置路徑,存在mach-O的 LC_ID_DYLIB 段兒。 誰在鏈接我的時候我把路徑告訴你,并存放在誰的Mach-O的LC_LOAD_DYLIB段兒,當程序啟動時候,dyld會通過 解析可執行文件的Mach-O 的 LC_LOAD_DYLIB 來找到所用到的動態庫,進行加載,運行。
拓: 我們這是通過修改install_name 那能不能在創建的時候直接給它附上?當然可以 查看ld的接口
屏幕快照 2021-04-11 下午4.25.48.png
@rpath
通過上面的操作,是可以跑起來,但是細心的你應該發現了我給的是一個絕對路徑,別人用我的動態庫不照樣找不到么?那怎么辦?我們是不是需要雙方約定一個規則:動態庫說,你給我提供一個變量,我基于你這個變量做一個相對路徑,比如說你test 所在的路徑。我只給你提供一個相對于你的路徑 。
test說:妥了。
這個變量就是@rpath :誰鏈接
我 誰來提供。
通過install_name_tool -id 來改成相對路徑
查看 libA_Manager.dylib的 load command
運行腳本 test 重新鏈接 libA_Manager.dylib
查看 test 的load command
運行
- ?為啥又成了image not found 了
這時候動態庫站出來了:大哥這絕對不是我的鍋,讓你給我提供變量,你有給這變量賦值么?
test:??,我看看,這。。。這不能怨我 啊 我 找 install_name_tool
查看 install_name_tool
- 在指定的Mach-O二進制文件中添加新的rpath路徑名。不止一個
可以指定選項。如果Mach-O二進制文件已經包含新的rpath路徑名
在-add\ rpath中指定這是一個錯誤。 - install_name_tool:我明明有這個功能 這能怨我?
好吧你們別爭了怨作者
給test 添加一個rpath 供動態庫使用。
查看test的Mach-O
- 發現此時多了一個Load command cmd為:LC_RPATH
運行
總結:
項目報了image not found 就證明在啟動的時候,dyld解析自身Mach-O LC_LOAD_DYLIB 段 來找到動態庫真實所在的路徑,并未找到,首先排查第三庫,的路徑是否引入正確,也就是在第三方動態庫的 Mach-O LC_ID_DYLIB 段 存有自描述路徑 @rpath。 還要排查自身是否有提供LC_RPATH
疑問:
通過上面的操作我們知道了@rpath是什么,但是你們有沒有發現,它能算相對路徑?對于上面的 libA_Manager.dylib 來說它是LC_ID_DYLIB是相對路徑 因為它前面拼接了test為它提供的@rpath的賦值LC_RPATH 那對于test來說我們寫入的LC_RPATH 依舊是絕對路徑。我給test換個位置在運行,依然找不到。 為了解決這個問題系統為我提供了兩個變量:@ executable_path @loader_path
@executable_path
表示可執行程序所在的目錄,解析為可執行文件的絕對路徑。
- 這是什么意思 帶入到我們上面的目錄結構,就是代表test的目錄
@loader_path
表示被加載的‘Mach-O’所在的目錄,每次加載時,都可能被設置為不同的路徑,由上層決定
- 這就代表,誰加載我誰的路徑
對于test來說 @executable_path 就是它所在的路徑。
對于libA_Manager.dylib 來說@loader_path 也是 test 所在的路徑。因為是test來鏈接的。
那現在我要將test提供的LC_RPATH路徑改為相對路徑怎么改?這里不妨停下想一下。
有人說了我有@executable_path了那@loader_path啥時候用?
對于上面的例子我們只是可執行文件->動態庫,那請問 當可執行文件->動態庫1->動態庫2怎么辦?