关于dyld: Library not loaded那点事儿

背景

有个牛逼同事用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.彻底研究了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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,312评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,578评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,337评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,134评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,161评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,303评论 1 280
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,761评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,421评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,609评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,450评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,504评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,194评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,760评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,836评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,066评论 1 257
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,612评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,178评论 2 341

推荐阅读更多精彩内容