美文网首页iOS学习笔记
关于dyld: Library not loaded那点事儿

关于dyld: Library not loaded那点事儿

作者: Johankoi | 来源:发表于2019-07-18 22:20 被阅读46次

    背景

    有个牛逼同事用QT在开发一Mac小应用,找到我说他引用了一个zip解压缩的库.在QT的IDE运行起来之后,就崩溃.看控制台的报错信息大概如下

    dyld:  Library not loaded: libquazip.1.dylib
      Referenced from: /Users/USER/Documents/quawindow.app/Contents/MacOS/quawindow
      Reason: image not found
    

    看起来就是应用启动的时候尝试加载libquazip.1.dylib, 但是却找不到.
    作为一个iOS/Mac开发.首先想到的是用Xcode将工程跑起来调试.但是卧槽QT是自己的IDE.
    不是自己熟悉的开发环境,而且QT工程构建出来的结果只是一个Mac可执行的.app的程序.
    现在只能凭借自己的开发经验意识,与这个熟悉QT开发的同事一起一点点的尝试探索问题入口.

    一.检查QT工程配置里关于Mac上加载dylib相关的配置

    确认了QT工程关于libquazip.1.dylib这个库的加载路径以及链接选项的配置都是没有问题的,也去搜索QT配置链接动态库的相关文档以及博客.基本上也都是该做的都做了.
    到这里似乎真的是有点陷入僵局.
    然后我冷静了下,试图从结果出发逆向思考去分析:
    第一.可执行的.app的程序确实是想要链接libquazip.1.dylib的,就是死活找不到.
    第二.libquazip.1.dylib这个库也确实是存在的.但是它没有被找到

    想想看,一个事物确实存在,但另外一个确实想用它的人却找不到.说明什么?
    说明没有找对路子啊~没有找对路子.至少有两方面的原因:
    第一,这个存在的事物没有给对信息,让别人找到它,
    第二,用它的人找它的途径出了差错

    带着这个逆向思维继续向下探索......

    二.利用otool命令检查Mach-O文件链接信息

    现在给我的就只有这个QT构建出的可执行.app的程序.作为问题查找的源头.
    接着当时突然想到自己搞iOS逆向研究的时候,有一个otool命令可以显示Mach-O文件的结构信息.
    Mach-O就是iOS/Mac可执行程序的定义格式.
    关于.app与可执行二进制Mach-O的目录结构如下图:

    mac-mach-o.jpg
    然后执行:
    otool -L /Users/hxq/Documents/quawindow.app/Contents/MacOS/quawindow
    

    -L表示显示当前可执行程序要链接哪些动态库

    /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)
    

    看到它确实指名点姓的要去加载libquazip.1.dylib的,那么问题出现到哪里了?
    细心观察对比可以发现下面这些动态库,

    @rpath/QtWidgets.framework/Versions/5/QtWidgets
    /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit
    /usr/lib/libc++.1.dylib
    

    可以看出来上面这些QT开发依赖的QtWidgets等等,还有系统IOKit,libc++等动态库,显示都是有明确的指明路径的.而唯独出问题的libquazip.1.dylib只有个名字,没有路径指示
    那就尝试一下修改对于libquazip.1.dylib的链接信息:利用install_name_tool -change命令把quawindow对libquazip.1.dylib的引用路径指向一个明确的路径/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)
    ........
    

    确认修改生效,然后双击quawindow.app,运行起来了不再崩溃!问题找到了,就是加载路径的问题!
    接下来是要研究下mac os系统下的dylib特性以及加载机制....

    三.探究dylib

    dylib(dynamic library)是苹果动态函数库,在应用程序编译的时候, 不会编译进二进制目标代码中, 只有当程序里执行相应的函数才调用该函数库里对应的函数。
    当应用程序启动的时候,有一个叫做动态连接器和加载器dyld会寻找,加载,连接动态库.
    因此上面由于加载了路径未明确libquazip.1.0.0.dylib而崩溃的时刻,就是发生在启动的时候.

    dylib有一个很重要的属性叫做install name,比较蛋疼的是其实它不单是名字,必须是一个路径.它的作用是为了告诉想要链接它的可执行程序或者其他库,要从哪里找到它.
    苹果官方文档也有说明:

    usedylib.jpg
    又查到otool -D 命令可以显示某个dylib的install name属性:
    otool -D  /Users/hxq/Documents/libquazip.1.0.0.dylib
    /Users/hxq/Documents/libquazip.1.0.0.dylib:
    libquazip.1.dylib
    

    显示出来之前被quawindow链接的libquazip.1.0.0.dylib的install name是libquazip.1.dylib.
    到这里就进一步看出问题了!!! 按照苹果的规定install name必须是个路径才对!!
    因此我们需要把链接的libquazip.1.0.0.dylib的install name修改成一个正确的路径.

    接下来就要好好探究一下dylib加载路径的规则机制.

    四.dylib加载路径

    通常依赖dylib会有两种方式:

    一.放置于系统某个公共目录,可被多个应用进程依赖,运行时调用:
    最典型的案例是系统库:

    /System/Library/Frameworks/IOKit.framework/Versions/A/IOKit 
    /usr/lib/libc++.1.dylib
    

    这种方式就很简单了.只需把dylib的install name指定成固定的绝对路径即可.

    二.嵌入到应用程序中
    很多时候单一应用程序依赖一些动态库.为了避免应用发布的时候需要同步安装所依赖的动态库带来的繁琐,就把所有依赖的dylib一个放入xx.app里面.

    场景一:比如上面的QT构建出来的Mac应用:
    frameworks.jpg

    看得出它依赖了很多跟QT开发环境有关的组件库.

    场景二:解释Swift5的ABI 稳定后为什么包体会减小

    先看用xcode9.4创建的基于Swift语言的空工程构建出来的.app内部


    swiftapp.jpg

    可以看到app里面Frameworks目录下放了很多关于Swift核心的动态库.
    而Swift5 (或以上) ABI稳定后, Apple 会把Swift runtime相关的库弄到 iOS 和 macOS 系统里公共目录.这样就不用每个app都留存一份.包体自然就会减小.
    读者可以自己尝试用xcode10.2创建基于Swift语言的空工程构建出app去验证.

    好.通过案例深化dyld的应用形式后,
    继续介绍动态库嵌入app时,如何指定dylib加载路径(install name):
    三个环境变量出场:

    • @executable_path
    • @loader_path
    • @rpath

    非常重要的提示:这三个环境变量仅用于嵌入app里面的dyld的install name指定的时候!!!

    1.@executable_path 这个变量表示可执行程序所在的目录
    这里假使(假设是因为此刻问题还没有解决嘛),libquazip.1.dylib是经过正确的工程配置构建后,放在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的灵活增强版,表示任意一个某时刻被加载的mach-o文件(包括App, dylib, framework,appex等)所在的目录.
    因此在单一app下可执行文件时候,@loader_path等价于@executable_path.
    那么@loader_path的灵活性怎么体现呢,举一个例子吧:
    假如quawindow.app引用了一个插件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的话
    此时:

    @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设定libquazip.1.dylib加载路径,能够保证不论被引用的Share.appex放入quawindow.app里面的任意位置,都能够让libquazip.1.dylib正确的加载.

    3. @rpath 又进一步增强灵活性
    在前两种@executable_path,@loader_path的设定机制里,被引入的dylib占据查找主动权:我来指定用我的人
    怎么找到我,显得比较傲娇.
    而@rpath出现后,使得主动权站在了引用dylib的应用程序这边.
    例如把libquazip.1.dylib的install name指定为@rpath/libquazip.1.dylib后,指定它加载路径归属权就交给了引用它的quawindow.app.
    要在编译时候去指定quawindow.app的@rpath
    注意哦~刚才是libquazip.1.dylib有一个@rpath设定,现在编译quawindow.app也需要设定@rpath:

    @path.jpg
    在xcode工程里Build Settings设置 Runpath Search Paths(对应了@rpath)

    这样整体的加载流程就是:
    quawindow.app启动查找引用的libquazip.1.dylib路径,发现其install name是@rpath, 发现主动权在自己手中.就立马去查找自身设定的@rpath,设定为@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.

    到此为止,关于dylib的加载机制,路径查找设定都搞清楚了....接下来终于可以解决文章一开头dyld: Library not loaded: libquazip.1.dylib的问题了

    五.正式解决dyld: Library not loaded崩溃问题

    现在就是很清晰明白了,就是libquazip.1.dylib路径找不对的问题.
    怎么解决? 使用install_name_tool命令重新设定libquazip.1.dylib的install name.
    设定之前,先考虑libquazip.1.dylib的使用方式,通过分析根据QT构建Mac应用的规律,决定采用将libquazip.1.dylib嵌入quawindow.app形式.

    在.pro文件中添加: (不会QT的直接忽略这个,不用理解,只关心结果即可,而且不影响上面所有关于dyld的知识点的理解)

    macx {
        plugins.path = Contents/Plugins/zip
        plugins.files = ./lib/libquazip.1.dylib
        QMAKE_BUNDLE_DATA += plugins
    }
    

    上面的配置,就使得构建后,能将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也可以.
    至此问题完美解决

    补充

    前面提到使用install_name_tool去修改已生成的dylib的install name,那么怎么在构建动态库的xcode工程里面设定这个install name:


    set-insname.jpg

    六.总结

    1.QT for Mac真JB坑. 第三方动态库的加载配置支持超极不友好.
    2.好处是通过这个坑,彻底研究了Mac/iOS 动态库的机制,尤其是路径查找设定规则.
    3.再次感受到逆向分析二进制的重要,otool install_name_tool命令大大的好用.
    4.有机会向QT官方提个PR,M了个:坑我一天解决问题,吭哧了我一天写文章.

    参考文章

    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

    相关文章

      网友评论

        本文标题:关于dyld: Library not loaded那点事儿

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