动态库原理

1,100 阅读9分钟

静态库原理一文中,我们介绍了静态的原理,在这里我们通过编译器clang命令,手动来制作动态库,并探索其原理。

链接动态库AFN

我们新建一个test.m文件,并使用 AFN动态库

#import <Foundation/Foundation.h>
#import <AFNetworking.h>

int main(){
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    NSLog(@"链接AFN----%@", manager);
    return 0;
}

AFNtest.m的目录结构如下

链接AFN1.png

将 test.m 编译为 test.o

打开终端,我们进入代码目录,输入一下命令

bel@beldeMacBook-Pro 链接动态库AFN % clang -target x86_64-apple-macos10.15 \  
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-I./AFNetworking \
-c test.m -o test.o
  • -target: 指定架构。
  • -fobjc-arc: ARC环境。
  • -isysroot: 编译的SDK路径。
  • -I :指定头文件位置, 对应Xcode中的 header search path。
  • -c xxx.m -o xxx.o: 将.m文件编译为 .o 文件。 这样我们就将test.m 编译为了test.o

链接test.o得到可执行文件

将 test.o 链接成可执行文件

bel@beldeMacBook-Pro 链接动态库AFN % clang   \                              
-target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-L./AFNetworking \
-lAFNetworking \
test.o -o test
  • -L< dir > : 指定库文件路径(.a.dylib库文件),对应Xcode中的Library search Path
  • -l< libarayr_name>:指定链接的库文件名称(.a.dylib库文件)。对应Xcode中的 other link flags

到这里我们就得到了可执行文件test

加载运行

我们使用 lldb -file命令来加载运行test文件,加载成功,输入 r 既可以运行

bel@beldeMacBook-Pro 链接动态库AFN % lldb -file test
(lldb) target create "test"
Current executable set to '/Users/bel/Desktop/动态库原理/链接动态库AFN/test' (x86_64).
(lldb) r
Process 40118 launched: '/Users/bel/Desktop/动态库原理/链接动态库AFN/test' (x86_64)
dyld: Library not loaded: @rpath/AFNetworking.framework/Versions/A/AFNetworking
  Referenced from: /Users/bel/Desktop/动态库原理/链接动态库AFN/test
  Reason: image not found
Process 40118 stopped
* thread #1, stop reason = signal SIGABRT
    frame #0: 0x000000010006bf7a dyld`__abort_with_payload + 10
dyld`__abort_with_payload:
->  0x10006bf7a <+10>: jae    0x10006bf84               ; <+20>
    0x10006bf7c <+12>: movq   %rax, %rdi
    0x10006bf7f <+15>: jmp    0x10006a160               ; cerror_nocancel
    0x10006bf84 <+20>: retq   
Target 0: (test) stopped.
(lldb) 

What!!!!!!在运行时报错了,原因为dyld: Library not loaded,为什么会这样呢?这里也卖个关子,稍后进行在进行解答。

制作动态库

我们使用编译器命令来制作动态库并进行链接。首先新建dylib文件夹,里面只用一个.m.h文件,只执行了一个NSLog方法,其目录结构如下

制作动态库1.png

编译.m 文件

1,将主程序test.m 编译为 test.o

clang -target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-I./dylib \
-c test.m -o test.o

将动态库.m文件编译为 .dylib

clang -dynamiclib \
-target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
TestExample.m -o libTestExample.dylib

此时,我们的动态库已经制作完成

动态库制作2.png

我们通过file命令来查看libTestExample.dylib属性,它是 dynamically linked shared library

bel@beldeMacBook-Pro dylib % file libTestExample.dylib 
libTestExample.dylib: Mach-O 64-bit dynamically linked shared library x86_64

动态库是编译链接后的最终产物。和静态库不同的是静态库是.o文件的合集,

链接动态库,生成可执行文件

clang -target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-L./dylib \
-lTestExample \
test.o -o test

这样我们的主程序test就将动态库TestExample链接完成,我们使用lldb lldb -file test进行加载运行,同样,我们又碰到了dyld: Library not loaded: libTestExample.dylib Referenced from: /Users/bel/Desktop/动态库原理/制作动态库/test Reason: image not found错误。

dyld加载可执行文件

LC_LOAD_DYLIB

当运行程序时,dyld会根据cmd等于LC_LOAD_DYLIBLoad Command去加载动态库,我们使用otool来查看test可执行文件中的DYLIB命令

bel@beldeMacBook-Pro 制作动态库 % otool -l test | grep 'DYLIB' -A 2
          cmd LC_LOAD_DYLIB
      cmdsize 48
         name libTestExample.dylib (offset 24)
--
          cmd LC_LOAD_DYLIB
      cmdsize 96
         name /System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (offset 24)
--
          cmd LC_LOAD_DYLIB
      cmdsize 56
         name /usr/lib/libobjc.A.dylib (offset 24)
--
          cmd LC_LOAD_DYLIB
      cmdsize 56
         name /usr/lib/libSystem.B.dylib (offset 24)
--
          cmd LC_LOAD_DYLIB
      cmdsize 104
         name /System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation (offset 24)
  • -l: The objdump(1) option to display the load commands,查找MachO中的load commands
  • grep 'DYLIB' -A 2: 从结果中筛选出包含 DYLIB的信息,并向下多展示2行

从结果我们可以看出:

  • 1,test程序共使用了 5 个动态库。
  • 2,libTestExample.dylibname路径,不是其所在位置的真实路径。 根据以上分析我们可知,dyld: Library not loaded: libTestExample.dylib的错误,是由 name路径没有指向动态库的真实路径,导致在运行时,找不到动态库。

LC_ID_DYLIB

动态库的路径是保存在动态库自己的Mach-O里面的,动态库的路径是由 LC_ID_DYLIB提供的, 我们来查看下libTestExample.dylibLC_ID_DYLIB

bel@beldeMacBook-Pro dylib % otool -l libTestExample.dylib | grep 'ID' -A 2
          cmd LC_ID_DYLIB
      cmdsize 48
         name libTestExample.dylib (offset 24)
--
     cmd LC_UUID
 cmdsize 24
    uuid 9DDBE61D-71C2-3E5A-A6CA-65119022065E

这说明,我们在链接生成动态库的时候,路径给的是错误的。

install_name_tool

既然我们在链接的时候给的路径是错误的,那有没有办法去更改路径呢。我们可以使用install_name_tool去修改LC_ID_DYLIBname值。 使用man install_name_tool可以查看其用途,主要是用来更改动态库的安装路径的。

我们将真实的动态库路径写入libTestExample.dylibname值里面:

install_name_tool -id /Users/bel/Desktop/动态库原理/制作动态库/dylib/libTestExample.dylib libTestExample.dylib
bel@beldeMacBook-Pro dylib % otool -l libTestExample.dylib | grep 'ID' -A 2 
          cmd LC_ID_DYLIB
      cmdsize 104
         name /Users/bel/Desktop/动态库原理/制作动态库/dylib/libTestExample.dylib (offset 24)
--
     cmd LC_UUID
 cmdsize 24
    uuid 9DDBE61D-71C2-3E5A-A6CA-65119022065E

我们已经将路径值写入到了动态库的name值里面。返回到上一目录,我们重新进行链接:

bel@beldeMacBook-Pro 制作动态库 %  clang -target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-L./dylib \
-lTestExample \
test.o -o test

然后加载运行

bel@beldeMacBook-Pro 制作动态库 % lldb -file test                         
(lldb) target create "test"
Current executable set to '/Users/bel/Desktop/动态库原理/制作动态库/test' (x86_64).
(lldb) r
Process 40895 launched: '/Users/bel/Desktop/动态库原理/制作动态库/test' (x86_64)
2021-07-04 18:12:34.400920+0800 test[40895:3209920] testApp----
2021-07-04 18:12:34.401236+0800 test[40895:3209920] TestExample----
Process 40895 exited with status = 0 (0x00000000) 

在这里已经成功运行了!!!!!!

RPATH

我们在动态库中,我们将name值写入了一个绝对路径,但如果我们在其他工程使用,该路径将会报错。那有没有一种通用的路径设置呢 ? 苹果给我们提供了@rpath来解决该问题。

@rpath

RPATH全称 Runpath search Paths,是dyld的搜索路径。在运行时@rpath指示dyld按顺序搜索路径列表,以找到动态库。

我们将libTestExample.dylib的name值用@rpath来替代

bel@beldeMacBook-Pro dylib % install_name_tool -id @rpath/dylib/libTestExample.dylib libTestExample.dylib
bel@beldeMacBook-Pro dylib % otool -l libTestExample.dylib | grep 'ID' -A 2
          cmd LC_ID_DYLIB
      cmdsize 64
         name @rpath/dylib/libTestExample.dylib (offset 24)
--
     cmd LC_UUID
 cmdsize 24
    uuid 9DDBE61D-71C2-3E5A-A6CA-65119022065E

谁链接动态库,谁提供RPATH值,在这里需要程序test提供RPATH

bel@beldeMacBook-Pro 制作动态库 % install_name_tool -add_rpath /Users/bel/Desktop/动态库原理/制作动态库 test
bel@beldeMacBook-Pro 制作动态库 % otool -l test | grep 'RPATH' -A 5
          cmd LC_RPATH
      cmdsize 64
         path /Users/bel/Desktop/动态库原理/制作动态库 (offset 12)
bel@beldeMacBook-Pro 制作动态库 % 
  • test文件中的 LC_RPATH的值就变成了我们写入的路径

我们不需要在次进行链接,直接运行

bel@beldeMacBook-Pro 制作动态库 % lldb -file test
(lldb) target create "test"
Current executable set to '/Users/bel/Desktop/动态库原理/制作动态库/test' (x86_64).
(lldb) r
Process 40955 launched: '/Users/bel/Desktop/动态库原理/制作动态库/test' (x86_64)
2021-07-04 18:34:01.233540+0800 test[40955:3221046] testApp----
2021-07-04 18:34:01.233942+0800 test[40955:3221046] TestExample----
Process 40955 exited with status = 0 (0x00000000) 
(lldb) 

@executable_path

此时,我们已经注意到,在testLC_RPATH中,路径是我们写死的一个路径,在使用过程中会造成很多错误,我们可以使用@executable_path 变量来替代。 @executable_path:表示可执行程序所在的目录,解析为可执行文件的绝对路径

bel@beldeMacBook-Pro 制作动态库 % install_name_tool -rpath /Users/bel/Desktop/动态库原理/制作动态库  @executable_path test
bel@beldeMacBook-Pro 制作动态库 % otool -l test | grep 'RPATH' -A 5
          cmd LC_RPATH
      cmdsize 32
         path @executable_path (offset 12)
  • 1,由于之前我们已经写入了RPATH值,所以,我们使用rpath参数来进行修改RPATH值。将原来的值替换为 @executable_path

我们直接运行

bel@beldeMacBook-Pro 制作动态库 % lldb -file test
(lldb) target create "test"
Current executable set to '/Users/bel/Desktop/动态库原理/制作动态库/test' (x86_64).
(lldb) r
Process 40984 launched: '/Users/bel/Desktop/动态库原理/制作动态库/test' (x86_64)
2021-07-04 18:48:49.994214+0800 test[40984:3227228] testApp----
2021-07-04 18:48:49.994511+0800 test[40984:3227228] TestExample----
Process 40984 exited with status = 0 (0x00000000) 
(lldb) 

Xcode 的项目中 在 Build Settings 里面也可以找到 RPATH变量的设置

Xcode RPATH.png

编译器写入

在上面的过程中,我们使用的是install_name_tool工具来对Mach-O进行修改,我们也可以使用链接器的 -Xlinker参数来修改 动态库的LC_ID_DYLIB值和可执行程序LC_RPATH

-Xlinker -install_name -Xlinker @rpath/dylib/libTestExample.dylib // 1

-Xlinker -rpath -Xlinker @executable_path  // 2
  • 1,修改动态库的 LC_ID_DYLIB值。
  • 2,修改可执行程序的LC_RPATH值。

我们可以将以上步骤写在Shell脚本里面,就会非常的便捷。

echo "编译test.m --- test.o"
clang -target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-I./dylib \
-c test.m -o test.o
#-I 指定头文件所处的位置

echo "编译 TestExample.m --- libTestExample.dylib"
# -dynamiclib:动态库
pushd ./dylib

clang -dynamiclib \
-target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-Xlinker -install_name -Xlinker @rpath/dylib/libTestExample.dylib \
TestExample.m -o libTestExample.dylib

# -Xlinker -install_name -Xlinker @rpath/libTestExample.dylib: 将 name 定义为 @rpath/libTestExample.dylib

echo "-------LC_ID_DYLIB---------"
otool -l libTestExample.dylib | grep 'LC_ID_DYLIB' -A 3

popd

#-L 库文件的位置
#-l 库文件的名称
echo "链接libTestExample.dylib -- test EXEC"
clang -target x86_64-apple-macos10.15 \
-fobjc-arc \
-isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk \
-Xlinker -rpath -Xlinker @executable_path \
-L./dylib \
-lTestExample \
test.o -o test

echo "-------LC_RPATH---------"
otool -l test | grep 'LC_RPATH' -A 3
# -Xlinker -rpath -Xlinker @executable_path \ 将rpath定义为 @executable_path

总结:

  • 1,动态库是编译链接后的最终产物,静态库是.o文件的合集。
  • 2,程序运行时,dyld加载动态库是按照Mach-O中LC_LOAD_DYLIB去加载动态库的。name值是动态库的路径。
  • 3,可执行文件通过 LC_RPATHLoad Command 提供@rpath的值,用@executable_path变量表示可执行程序所在的目录。
  • 4,动态库 通过 LC_ID_DYLIBLoad Comand 使用 @rpath值,和 动态库的相对路径,来表示动态库所在的位置。

最后,我将本文探索的两个实例和编译脚本buildDynamic.sh,存放到了我的仓库(动态库原理),有兴趣的可以去下载。