美文网首页iOS应用程序安全
深入浅出@executable_path @loader_pat

深入浅出@executable_path @loader_pat

作者: 捡书 | 来源:发表于2021-11-28 17:35 被阅读0次

    从报错开始

    当你在APP中引入三方动态库时,是不是经常遇到下面这种错误:

    dyld: Library not loaded: @rpath/TestKit.framework/TestKit
     Referenced from: <long_path_name>/TestApp.app/TestApp
     Reason: image not found
    

    错误消息中的@rpath 是什么?

    @rpath 代表运行路径搜索路径。
    要了解它的含义以及我们为什么需要它,我们需要看看动态库(在 macOS 和 iOS 世界中称为 dylib)如何与其他 dylib 和可执行文件链接。但在了解@rpath 之前,我们需要先搞明白@executable_path 和@loader_path 的含义。

    什么是@executable_path

    这里我们以C语言为例,当然OC和Swift的原理是一样的,用C只是因为在命令行里操作简单一些。

    ❯ mkdir Demo
    ❯ cd Demo
    ❯ vim Cat.c
    

    输入以下内容,保存并退出:

    #include <stdio.h>
    
    void catSound() {
      printf("MEOW!\n");
    }
    

    创建main文件:

    ❯ vim main.c
    

    输入以下代码

    void catSound();
    
    int main(int argc, char** argv) {
      catSound();
      return 0;
    }
    

    现在,我们执行编译命令,将Cat.c编译成动态库,main.c编译成mac的可执行文件,一定要注意先后顺序:

    ❯ clang -dynamiclib Cat.c -o libCat.dylib && clang -L. -lCat main.c -o main
    ❯ ls
    Cat.c        libCat.dylib    main         main.c
    

    现在,我们运行以下main,发现成功调用了libCat.dylib:

    ❯ ./main                                                                                                                                                                                                                                                         
    MEOW!
    

    使用otool命令看一下动态库的链接有哪些:

    ❯ otool -L main                                                                                                                                                                                                                                                 
    main:
        libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    

    可以看到,main已经链接了libCat.dylib,但这是一个相对路径。
    这意味着我们的主要可执行文件希望在执行它的同一目录中找到 libCat.dylib。
    所以如果我们尝试从其他目录以相对路径运行 main,我们会得到一个大家非常熟悉错误:

    ❯ cd ..
    ❯ ./Demo/main                                                                                                                                                                                                                                                         
    dyld: Library not loaded: libCat.dylib
      Referenced from: /Users/admin/Desktop/Code/inject/./Demo/main
      Reason: image not found
    [1]    11442 abort      ./Demo/main
    

    但是,我们可以使用install_name_tool这个工具,将libCat.dylib的路径改为绝对路径,这样就可以解决上面的问题:

    ❯ install_name_tool -change libCat.dylib @executable_path/libCat.dylib main
    ❯ otool -L main                                                                                                                                                                                                                                                 
    main:
        @executable_path/libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    ❯ cd ..
    ❯ ./Demo/main
    MEOW!
    

    什么是@loader_path

    要理解@loader_path,我们需要让测试用例更加复杂一些。
    让我们在不同的目录中创建另一个 dylib,并使这个 dylib 依赖于我们的 Cat dylib。

    ❯ mkdir Animal
    ❯ cd Animal
    ❯ vim Animal.c
    

    输入以下代码:

    void catSound();
    
    void animalSound() {
      catSound();
    }
    

    退到上层目录(因为libCat.dylib在上层目录),然后编译一下吧:

    ❯ cd ..
    ❯ clang -dynamiclib -L. -lCat Animal/Animal.c -o Animal/libAnimal.dylib
    ❯ ls
    Animal       libCat.dylib
    

    修改一下main.c文件,我们改为调用animalSound,内容如下:

    void animalSound();
    
    int main(int argc, char** argv) {
      animalSound();
      return 0;
    }
    

    然后链接一下Animal.dylib:

    ❯ clang -LAnimal -lAnimal main.c -o main
    ❯ ./main                                                                                                                                                                                                                                                        
    MEOW!
    

    像刚才一样,我们知道可以从当前目录执行 main,但不能从其他目录执行。
    我们也知道解决这个问题的方法,所以让我们继续这样做:

    ❯ install_name_tool -change Animal/libAnimal.dylib @executable_path/Animal/libAnimal.dylib main
    ❯ ./main                                                                                                                                                                                                                                                         
    MEOW!
    

    看上去没有问题,我们返回上层目录,再尝试一下:

    ❯ cd ..
    ❯ ./Demo/main                                                                                                                                                                                                                                                         
    dyld: Library not loaded: libCat.dylib
      Referenced from: /Users/admin/Desktop/Code/inject/Demo/Animal/libAnimal.dylib
      Reason: image not found
    [1]    12240 abort      ./Demo/main
    

    出人意料,刚才还可以的办法,现在居然不管用了。但是仔细观察报错,发现是libAnimal找不到libCat,而不是main找不到libAnimal。
    我们看一下libAnimal的动态库依赖:

    ❯ otool -L Animal/libAnimal.dylib                                                                                                                                                                                                                       
    Animal/libAnimal.dylib:
        Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
        libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    

    我们需要在这里将libCat.dylib 的相对路径改为绝对路径。
    但是我们应该把它改成什么?使用 @executable_path 会起作用 - 但仅适用于我们的主要可执行文件。
    Dylib 旨在在多个客户端之间共享,所有客户端都可以位于不同的路径中。
    这意味着 @executable_path 将根据正在运行的可执行文件解析为不同的值,这一点从命名中不难看出,executable path直译就是可执行路径。

    让我们再分析一下,看看我们拥有的依赖树。
    main 依赖于 libAnimal,而 libAnimal 依赖于 libCat。 libCat 不依赖任何东西。

    libCat.dylib <--- Animal/libAnimal.dylib <--- main
    

    不管是main加载libAnimal,还是libAnimal加载libCat,@executable_path 将始终解析为 main 的路径。
    因此dyld 提供了另一个变量 - @loader_path - 解析为客户端执行加载的路径。
    我们看一下当前文件目录的结构,这有助于我们分析动态库的加载路径:

    .
    ├── Demo
       ├── Animal
       │   ├── Animal.c
       │   └── libAnimal.dylib
       ├── Cat.c
       ├── libCat.dylib
       ├── main
       └── main.c
    

    我们列个表格,看一下:

    依赖关系 @executable_path @loader_path
    main -> libAnimal ./Demo/ ./Demo/
    libAnimal -> libCat ./Demo/ ./Demo/Animal/

    搞清楚了这个路径关系,那问题就很好解决了,还是用install_name_tool,但是这次是修改libAnimal的依赖:

    ❯ install_name_tool -change libCat.dylib @loader_path/../libCat.dylib Animal/libAnimal.dylib
    ❯ otool -L Animal/libAnimal.dylib                                                                                                                                                                                                                               
    Animal/libAnimal.dylib:
        Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
        @loader_path/../libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    

    让我们试一下吧:

    ❯ ./main                                                                                                                                                                                                                                                         
    MEOW!
    ❯ cd ..                                                                                                                                                                                                                                                          
    ❯ ./Demo/main                                                                                                                                                                                                                                                         
    MEOW!
    

    可以看到,现在从任何目录运行 main 都会成功。

    事实上,如果你要添加一个新的依赖于 libAnimal 的可执行文件 foo/main,你只需要在 foo/main 本身中设置 libAnimal.dylib 的安装路径。

    libCat和libAnimal现在是真正意义上的“共享库”,只要 libCat 或 libAnimal 之间的相对路径保持不变,就不需要更改它们,但这是不可避免的。

    有一点需要注意,对于可执行文件,@loader_path 和@executable_path 的意思是一样的。

    安装ID

    在介绍@rpath之前,我们先认识一个叫install id的概念,用otool查看两个dylib的动态库链接信息:

     ❯ otool -L Animal/libAnimal.dylib                                                                                                                                                                                                                                
    Animal/libAnimal.dylib:
        Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
        @loader_path/../libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    ❯ otool -L libCat.dylib                                                                                                                                                                                                                                          
    libCat.dylib:
        libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    

    对于 dylib来说,第一个条目不是安装路径,而是安装 ID。
    当另一个客户端链接到这个 dylib 时,dylib 的安装 ID 会被复制到客户端中作为dylib的安装路径。
    对于安装ID,依然可以使用install_name_tool来进行修改。

    ❯ install_name_tool -id @rpath/xxx/xxx.dylib xxx/xxx.dylib
    

    终于到了@rpath

    在一个大型项目中,在不同位置的多个客户端相互依赖,管理 @loader_path 是一件是非复杂且麻烦的事情。
    在这种情况下,我们可以使用@rpath。与上面介绍的两个变量不同,@rpath 对 dyld 没有任何特殊意义。由我们为每个客户端的 @rpath 定义一个(或多个)值。@rpath 的出现极大的降低了管理动态库加载路径的复杂度。

    让我们修改测试用例以使用@rpath。
    添加另一个与 main.c 代码相同的可执行文件 foo/main.c(直接用cp命令也可以),先不着急编译,我们的目录结构如下所示:

    .
    ├── Demo
       ├── Animal
       │   ├── Animal.c
       │   └── libAnimal.dylib
       ├── Cat.c
       ├── foo
       │   └── main.c
       ├── libCat.dylib
       ├── main
       └── main.c
    

    第一步,在我们的目录结构中选择一个路径作为锚路径。
    让我们选择./Demo/ 作为我们的锚点。

    接下来,让我们将 dylib 安装 ID 更改为 @rpath/xxx,其中xxx是从锚点到 dylib 的相对路径。

    对于 libCat.dylib,路径为 @rpath/libCat.dylib:

    ❯ install_name_tool -id @rpath/libCat.dylib libCat.dylib
    ❯ otool -L libCat.dylib                                                                                                                                                                                                                                          
    libCat.dylib:
        @rpath/libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    

    对于Animal/libAnimal.dylib,路径为@rpath/Animal/libAnimal.dylib

    ❯ install_name_tool -id @rpath/Animal/libAnimal.dylib Animal/libAnimal.dylib
    ❯ otool -L Animal/libAnimal.dylib                                                                                                                                                                                                                                
    Animal/libAnimal.dylib:
        @rpath/Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
        @loader_path/../libCat.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    

    接下来,让我们向可执行文件添加一个 @rpath,其值等于 @loader_path/xxx,其中 xxx 是从可执行文件到锚点的相对路径。

    对于 foo/main.c ,这个路径为 @loader_path/../

    ❯ clang -LAnimal -lAnimal -rpath "@loader_path/../" foo/main.c -o foo/main
    ❯ otool -L foo/main                                                                                                                                                                                                                                              
    foo/main:
        @rpath/Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    ❯ ./foo/main                                                                                                                                                                                                                                                     
    MEOW!
    

    而对于 main.c,这个路径是 @loader_path。
    我们可以使用 install_name_tool 将@rpath 添加到编译后的可执行文件中,但是由于在编译 main.c 之后 libAnimal 和 libCat 的安装 ID 发生了变化,最好重新编译并重新链接它,以便它获取更新的 ID :

    ❯ clang -LAnimal -lAnimal -rpath "@loader_path" main.c -o main
    ❯ otool -L main                                                                                                                                                                                                                                                  
    main:
        @rpath/Animal/libAnimal.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.0.0)
    ❯ ./main
    MEOW!
    

    完成这些步骤之后,我们可以将Demo目录移动到任何地方,甚至是不同的mac电脑上,这两个main可执行文件将继续运行 - 只要 Demo中的目录结构本身不改变。

    请注意,您可以在链接时或稍后使用 install_name_tool 为可执行文件定义多个 @rpath 值。
    dyld 将尝试所有值以检查是否存在 dylib。

    回到最初的报错

    了解所有这些后,我们现在可以更好地了解初始错误消息的含义以及如何修复它。
    让我们分析一下错误——

    dyld: Library not loaded: @rpath/TestKit.framework/TestKit
     Referenced from: <long_path_name>/TestApp.app/TestApp
     Reason: image not found
    

    可执行文件是 <long_path_name>/TestApp.app/TestApp。
    找不到的Dylib 是 TestKit。
    可执行文件在 @rpath/TestKit.framework/TestKit 中找不到 dylib。

    对于 iOS 应用程序,所有第三方框架都驻留在应用程序目录中的 Frameworks 目录中。
    所以 dylib 的实际路径是 <long_path_name>/TestApp.app/Frameworks/TestKit.framework/TestKit。
    锚目录是 <long_path_name>/TestApp.app/Framewoks/。
    因此,要找出此错误的原因,我们可以检查三件事

    • <long_path_name>/TestApp.app/TestApp 的@rpath 值为@loader_path/Frameworks/。 @executable_path/Frameworks/ 也可以工作,因为两者对可执行文件的意义相同。如果您有源代码,则可以在目标的构建设置 (LD_RUNPATH_SEARCH_PATHS) 中进行检查。如果没有, 试一下otool -l。
    • TestKit dylib 的安装 ID 为 @rpath/TestKitFramework.framework/TestKit。如果您有源代码,请在构建设置 (LD_DYLIB_INSTALL_NAME) 中进行检查。如果没有, 再试一下otool -l。
    • TestKit.framework 实际上存在于 Frameworks 目录中。为此,您需要将framework嵌入到应用程序中。

    当您向应用程序添加新framework时,Xcode 会为您处理所有这些设置。

    相关文章

      网友评论

        本文标题:深入浅出@executable_path @loader_pat

        本文链接:https://www.haomeiwen.com/subject/lsfhxrtx.html