MacOS - Xcode的使用注意事项

发布时间 2024-01-02 14:56:40作者: [BORUTO]

背景

有个牛逼同事用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命令大大的好用.



 
 
 
============================================================

作者:Johankoi
链接:https://www.jianshu.com/p/5bf7795db50d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。