技能回顾
在上一篇文章静态库、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的导出符号表
- 发现空空如也啥也没有。那为啥呢?在回到上面我们修改的脚本 哪里出问题了呢?
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的接口
@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怎么办?