背景
有个牛逼同事用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的目录结构如下图:
然后执行:
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,比较蛋疼的是其实它不单是名字,必须是一个路径.它的作用是为了告诉想要链接它的可执行程序或者其他库,要从哪里找到它.
苹果官方文档也有说明:
又查到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应用:
看得出它依赖了很多跟QT开发环境有关的组件库.
场景二:解释Swift5的ABI 稳定后为什么包体会减小
先看用xcode9.4创建的基于Swift语言的空工程构建出来的.app内部
可以看到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/下:
把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:
如果把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:
在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:
确定了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:
六.总结
1.彻底研究了Mac/iOS 动态库的机制,尤其是路径查找设定规则.
2.再次感受到逆向分析二进制的重要,otool install_name_tool命令大大的好用.
参考文章
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