我想我会用这个问题为所有“找不到图像”问题写一个规范的答案。
1.问题
让我们从一个包含主二进制文件和库的最小设置开始,如下所示:
main.c
:
#include <stdio.h>
extern int f(void);
int main(void)
{
printf("%u\n", f());
return 0;
}
xyz.c
:
int f(void)
{
return 42;
}
命令行:
% cc -Wall -O3 -shared -o libxyz.dylib xyz.c
% cc -Wall -O3 -o main main.c -L. -lxyz
这行得通。您可以运行./main
,它会打印42
.
但是,如果您现在创建一个文件夹lib
,请移到libxyz.dylib
那里并重新编译main
,如下所示:
% cc -Wall -O3 -o main main.c -Llib -lxyz
然后编译仍然会成功,但是启动它不会:
% ./main
dyld: Library not loaded: libxyz.dylib
Referenced from: /private/tmp/./main
Reason: image not found
但是如果你直接返回并重新编译libxyz.dylib
到该lib
文件夹,然后重新构建main
,如下所示:
% cc -Wall -O3 -shared -o lib/libxyz.dylib xyz.c
% cc -Wall -O3 -o main main.c -Llib -lxyz
然后它将再次工作。但只是为了说明,如果你libxyz.dylib
再次移动,这是你得到的错误:
% ./main
dyld: Library not loaded: lib/libxyz.dylib
Referenced from: /private/tmp/./main
Reason: image not found
更糟的是,您甚至可以在不移动库的情况下产生此错误:只需cd lib
调用../main
.
还要注意与之前的区别,libxyz.dylib
与lib/libxyz.dylib
. 这将我们带到了问题的核心。
2.原因
在 macOS 上,每个共享库都有一个“安装名称”,即预期在运行时可以找到的路径。这条路径可以采取三种形式:
- 绝对的,例如
/usr/lib/libxyz.dylib
。
- 相对的,例如
lib/libxyz.dylib
。
- 魔术,例如
@rpath/libxyz.dylib
。
该路径通过LC_ID_DYLIB
load 命令嵌入到库的 Mach-O 标头中。可以这样查看otool
:
% otool -l /tmp/lib/libxyz.dylib | fgrep -B1 -A5 LC_ID_DYLIB
Load command 2
cmd LC_ID_DYLIB
cmdsize 48
name lib/libxyz.dylib (offset 24)
time stamp 1 Thu Jan 1 01:00:01 1970
current version 0.0.0
compatibility version 0.0.0
此加载命令由链接器创建,其手册页 ( man ld
) 告诉我们以下内容:
-install_name name
Sets an internal "install path" (LC_ID_DYLIB) in a dynamic library.
Any clients linked against the library will record that path as the
way dyld should locate this library. If this option is not specified,
then the -o path will be used. This option is also called
-dylib_install_name for compatibility.
这告诉我们安装名称如何工作的三个步骤:
- 链接器在构建库时嵌入名称。
- 链接器在构建时将名称复制到链接到该库的二进制文件中。
- Dyld 使用该名称来尝试加载库。
如果库被移动,或者甚至没有使用与它们最终路径匹配的安装名称进行编译,这显然会导致问题。
3.解决方案
解决方案是更改安装名称路径。地点和方式取决于您的设置。您可以通过两种方式更改它:
- 使用正确的安装名称(
-Wl,-install_name,...
或直接-o ...
)重新编译库,然后重新编译主二进制文件以链接它。
- 使用
install_name_tool
. 这涉及更多一点。
无论哪种情况,您都需要决定要使用哪种形式的安装名称:
绝对。
这建议用于全局路径中的库,由所有用户共享。您也可以使用它来指向您的用户目录,但这有点难看,因为您无法移动二进制文件或将它们分发给其他人。
相对的。
相对于您的工作目录意味着这完全不可靠。永远不要使用这个。只是不要。
魔法。
除了绝对路径和相对路径之外,还有三个“特殊”标记:
@executable_path
是进程的主要二进制文件的运行时目录。这是最简单的形式,但仅当您的库仅在单个主二进制文件中使用时才有效。
@loader_path
是取决于库的二进制文件的运行时目录。我建议不要使用它,因为如果您在不同的文件夹中有两个二进制文件想要链接到同一个库,它会中断。
@rpath
是从LC_RPATH
加载命令组装的运行时目录列表。这有点复杂,但它是最灵活的解决方案,因为它本身可以包含@executable_path
and @loader_path
。
使用这些允许您构建可以自由移动的二进制文件,只要它们都保留它们的相对位置。
有关它们的完整描述,请参阅man dyld
。
有了这个,让我们看看实现可能的解决方案。我们有:
cc -Wl,-install_name,...
在编译时指定安装名称。
install_name_tool -id ...
更改嵌入库中的路径。
install_name_tool -change old new
更改嵌入在针对库的二进制链接中的路径。
3.1 绝对路径
如果您可以重新编译库和主二进制文件:
% cc -Wall -O3 -shared -o /tmp/lib/libxyz.dylib xyz.c
% cc -Wall -O3 -o main main.c -L/tmp/lib -lxyz
如果您只能重新编译主二进制文件:
% install_name_tool -id '/tmp/lib/libxyz.dylib' /tmp/lib/libxyz.dylib
% cc -Wall -O3 -o main main.c -L/tmp/lib -lxyz
如果您无法重新编译:
% install_name_tool -id '/tmp/lib/libxyz.dylib' /tmp/lib/libxyz.dylib
% install_name_tool -change 'libxyz.dylib' '/tmp/lib/libxyz.dylib' main
3.2@executable_path
如果您可以重新编译库和主二进制文件:
% cc -Wall -O3 -shared -o lib/libxyz.dylib xyz.c -Wl,-install_name,'@executable_path/lib/libxyz.dylib'
% cc -Wall -O3 -o main main.c -Llib -lxyz
如果您只能重新编译主二进制文件:
% install_name_tool -id '@executable_path/lib/libxyz.dylib' lib/libxyz.dylib
% cc -Wall -O3 -o main main.c -Llib -lxyz
如果您无法重新编译:
% install_name_tool -id '@executable_path/lib/libxyz.dylib' lib/libxyz.dylib
% install_name_tool -change 'libxyz.dylib' '@executable_path/lib/libxyz.dylib' main
3.3@rpath
Rpath 需要手动添加运行时路径,这需要一些规划。假设您有以下文件层次结构:
a
andb
是链接到 and 的二进制文件,而libx
andliby
又链接到libz
. 对于 的安装名称libz
,既不能使用@executable_path
(因为a
和b
在不同的目录中)也不能使用@loader_path
(因为libx
和liby
在不同的目录中)。但是你可以在里面使用它们中的任何一个@rpath
,这是你必须做出的决定:
- 您可以嵌入
@executable_path
ina
和@executable_path/..
in的 rpath b
。然后您可以使用@rpath
从所有二进制文件中引用项目根目录。libz
安装名称为@rpath/lib/libz.dylib
.
- 或者您可以嵌入
@loader_path/lib
inlibx
和@loader_path
in的 rpath liby
。然后您可以使用@rpath
来引用包含每个二进制文件的目录。libz
安装名称为@rpath/libz.dylib
.
我通常发现前者更容易处理,但如果您有大量二进制文件分散在许多目录和几个库中,则后者可能更可取。
要实际将 rpath 添加到二进制文件,您可以使用:
cc -Wl,-rpath,...
在编译时。
install_name_tool -add_rpath ...
然后。
因此,如果您可以重新编译库和主二进制文件:
% cc -Wall -O3 -shared -o lib/libxyz.dylib xyz.c -Wl,-install_name,'@rpath/lib/libxyz.dylib'
% cc -Wall -O3 -o main main.c -Llib -lxyz -Wl,-rpath,'@executable_path'
如果您只能重新编译主二进制文件:
% install_name_tool -id '@rpath/lib/libxyz.dylib' lib/libxyz.dylib
% cc -Wall -O3 -o main main.c -Llib -lxyz -Wl,-rpath,'@executable_path'
如果您无法重新编译:
% install_name_tool -id '@rpath/lib/libxyz.dylib' lib/libxyz.dylib
% install_name_tool -change 'libxyz.dylib' '@rpath/lib/libxyz.dylib' main
% install_name_tool -add_rpath '@executable_path' main
请注意,如果您的任何二进制文件已签名,这当然会使该签名无效。用于codesign -f ...
替换现有签名。