调试NIX项目中的动态链接错误

2020-07-19 11:17:05

前几天,在建立一个我是贡献者的科学项目时,伊朗陷入了两个系统库之间令人讨厌的版本冲突。一怒之下,我决定充分了解NIX,以便能够建立一个可重现的、严格控制的本地构建。现在已经完成了,总的来说,我对工具和设置非常满意。我正在使用direnv将我的普通shell与Nix的nix-shell特性紧密集成在一起,在很大程度上感觉一切都是无缝的。看到cmake报告说它发现了过多的二进制文件和库,内容经过散列并整齐地安装在/nix/store下的小行中,这让人非常耳目一新。

我使用NIX来管理我的开发环境,但不是用来构建项目本身。NIX确保项目依赖项已安装并且可由编译器和链接器发现。构建项目是使用CMake完成的,CMake是为cmake设置的,用于查找nix安装的库。NIX通过用自己的shell脚本包装C编译器并通过环境变量注入库和二进制文件的路径来实现这一点。除了声明您想要的包是buildInput之外,要使cmake正常工作几乎没有什么可做的。我的shell.nix文件的第一个版本如下所示:

#file shell.nix{pkgs?导入<;nixpkgs>;{}}:包。MkShell{buildInput=with pkgs;[cmake(callPackage nix/petsc.nix{})Metis hdf5 openmpi(python38.。With Packages(Packages:[Packages.。Numpy]))]];}。

使用此设置,我在构建项目时几乎没有遇到什么问题。我必须覆盖默认的PETSc派生,才能使用METIS和OpenMPIsupport进行编译,这并不太难:

#file nix/petsc.nix{petsc,blas,gfortran,lapack,python,metis,openmpi}:petsc。OverrideAttrs(oldAttrs:rec{nativeBuildInput=[blas gfortran gfortran.。CC.。Lib lapack python openmpi Metis];preconfigure=';';export fc=";${gfortran}/bin/gfortran";f77=";${gfortran}/bin/gfortran";patchShebangs。ConfigureFlagsArray=($configureFlagsArray";--with-mpi-dir=${openmpi}";";--with-metis=${metis}";";--with-blas-lib=[${blas}/lib/libblas.so,${gfortran。CC.。Lib}/lib/libgfortran.a]";";--with-lapack-lib=[${lapack}/lib/liblapack.so,${gfortran。CC.。Lib}/lib/libgfortran.a]";)';';})。

此Nix文件返回一个函数,该函数在shell.nix中使用callPackage函数调用。Petsc.overrideAttrs是覆盖使用stdenv.mkDerivation创建的派生属性的一种巧妙方式。构建具有MPI和METIS支持的PETSc太简单了,只需将一组不同的参数传递给配置脚本即可。

弄清楚怎么做这一切是很有趣的。我主要指的是NIX“药丸”,这是通过NIX工具和语言取得的巨大进步。

使用这些Nix文件,我可以执行cmake..。&;&;制作成功。让项目开始运行则是另一回事。最终的二进制文件立即失败,并出现动态加载错误:

该二进制文件试图从Nix在构建PETSC过程中创建的一个临时目录加载动态库。当然这失败了:当我调用bin/warpxm时,该目录已经被清理干净了。二进制文件应该链接到nix存储中/nix/store下的petsc派生结果,而不是/private/tmp下的文件。在某种程度上,似乎有一个环境变量被错误地设置到了这个intermediatedirectory中。要想弄清楚在哪里,我必须学习更多关于在OSX上链接的知识,这比我预期的要多得多。

首先,我检查了Nix的编译器包装器插入的编译器和链接器标志。它们通过NIX_CFLAGS_COMPILE和NIX_LDFLAGS传入。当您使用nix-shell和direnv时,派生的所有环境变量都会注入到shell中。这只是一个简单的问题,就是把它们呼应出来:

这些看起来不错!在这个shell中调用cmake和make应该会拉入正确的库。

然后我记起这个项目使用pkg-config来查找并将链接库放在一起。坦率地说,我不太理解pkg-config,但我知道在这个项目中,它是从cmake内部调用的。它根据自己的规则搜索库,并在Nix完成设置所有内容的工作后运行。因此,它绕过了我们刚才检查的编译器和linkerflag。

在设置这个Nix环境之前,我碰巧安装了pkg-config。因此,cmake能够从我的用户路径调用系统pkg-config。也许系统版本的pkg-config不知何故找到了错误的库?实际上,echo$PKG_CONFIG_PATH确认它正在搜索我的$HOME下的一个目录。我认为当我向Nix派生添加依赖项时,可能有一些问题交叉了,一次一个:适当配置pkg-config可能会有所帮助。

一种普遍的看法是,通过传递--pury标志,可以将nix-shell与用户安装的内容隔离开来。这是不对的。事实上,它保持了用户的整个路径。我已经提交了一张罚单来解决这个问题。然而,在更多地使用nix-shell之后,我相信即使没有对开发环境进行热隔离,它也是一个超级有用的工具。

我再次参考了关于C项目的Nix wiki页面,其中也有关于使用pkg-config的内容。将pkg-config派生作为nativeBuildInput包含似乎会让petsc这样的包将它们的iroutput路径附加到PKG_CONFIG_PATH环境变量。我这样做了:

一包。MkShell{buildInput=with pkgs;[...];nativeBuildInput=with pkgs;[pkg-config];}

但这并没有解决问题。我将不得不更深入地追查坏库是在哪里被拉进来的。

深入研究cmake文档和项目的.cmake文件后,我插入了三条打印语句:

FIND_PACKAGE(需要PkgConfig)pkg_CHECK_MODULES(PETSC PETSC REQUIRED)link_directories(${PETSC_LIBRARY_DIRS})+MESSAGE(";PETSC库:${PETSC_LIBRARY}";)+MESSAGE(";PETS库目录:${PETSC_LIBRARY_DIRS}";)+MESSAGE(";PETSC链接库:${PETSC_LINK_LIBRARS}";)列表(追加WARPXM_LINK。

第二个看起来不错。但是第一个,仅仅是库的名字petsc,为了舒适,有点太含蓄了。正是这个变量被附加到链接目标列表中。在编译时,将由链接器来查找库petsc,而我不确定它会显示在哪里。使用.dylib的绝对路径更安全,如下所示:

我在这里的想法是错误的。我们可以确定链接器将在编译时查看的位置:在NIX_LDFLAGS中列出的路径中!我没有清楚地考虑到编译过程中的数据流。

将链接目标更改为绝对路径仅在下一次cmake期间缓解了我的疑虑。&;&;生成循环。当然,链接器现在不可能搞砸了。不涉及神秘的图书馆搜索,只有一条绝对路径,这不可能被误解…。

在这一点上,我完全被搞糊涂了。每次尝试修复时,igrep都会徒劳地在build目录中查找有问题的/private/tmp路径,然后空手而归。我跟踪了传递给编译器的最终的、不可撤销的链接选项,这些选项隐藏在构建树中的link.txt文件中。它们无可争议地表明我的二进制文件链接到了正确的库:

我几乎令我满意地证明,CMake用这个库做了正确的事情,我完全失去了想法。最后,一个非常幸运的谷歌搜索将我带到了NIX手册中描述达尔文(MacOS)平台特定问题的部分。它声明:

在Darwin上,库使用绝对路径进行链接,库在链接时通过其安装名称进行解析。有时,包不能正确设置这一点,导致库查找在运行时失败。这可以通过添加额外的链接器标志或在修复阶段运行install_name_tool-id来修复。

这是一种非常实事求是的表达方式,当我理解它时,它让我大吃一惊。据我所知,以下是MacOS上发生的事情:

我的源代码有一个include指令,include<;petsc.h>;或类似的指令,它创建一个由链接器满足的二进制接口。

在链接时,我们将绝对路径列表传递给库,链接器查找与接口匹配的路径。

然后,链接器保存它在二进制文件的加载部分中找到的库的install_name。

在运行时,二进制文件(实际上是MacOS DYLD系统)加载库。

我肯定在这件事上有些地方弄错了,所以如果能听到比我更了解这件事的人的意见,我会非常感激的!

无论如何,这个发现让我想到了install_name的概念,所以我有了一些东西可以继续。更多的搜索导致了一篇有用的博客文章,准确地描述了我面临的问题。还介绍了如何检查库的INSTALL_NAME:

NIX手册指出“有些软件包无法正确设置”,并指向修复程序,即使用install_name_tool更改已构建库的install_name。Nixpkgs上的PETSC派生是否正确执行此操作?我看到它正在使用install_name_tool执行某些操作:

PrePatch=';';substituteInPlace配置\--替换/bin/sh/usr/bin/python';';+stdenv。利布。OptionalString stdenv.。IsDarwin;替换InPlace配置/install.py\--替换/usr/bin/install_name_tool install_name_tool';';;

此指令仅用install_name_tool替换字符串/usr/bin/install_name_tool的外观。Nix包这样做的原因是为了确保构建依赖于Nix构建的工具,这些工具在构建shell的路径中提供,而不是依赖于系统目录(如/usr/bin)中的二进制文件。

引入此替换的PR表明它修复了Darwin上的构建,因此在PETSc中必须调用/usr/bin/install_name_tool。在PETSCrepo中搜索它会导致下面这一行,该行执行的正是Mark日志在install_name上发布的指令:它使用install_name_tool-id将install_name更改为库在其安装目录中的绝对路径。

如果是os的话。小径。Splitext(Dst)[1]=.dylib;和os。小径。Isfile(';/usr/bin/install_name_tool';):[output,err,flg]=self。EcuteShellCommand(";otool-D";+src)oldname=output[Output.。查找(";\n";)+1:]installName=oldname。替换(操作系统。小径。Realpath(自我。ArchDir),赛尔夫。InstallDir)Self。EcuteShellCommand(';/usr/bin/install_name_tool-id';+installName+';';';+dst)。

根据这一点,在构建库时,库的install_name应该已经由PETSc修复了!除…外。注意到什么了吗?IF语句中的第二个条件。在PETSc派生运行其prePatchstep之后,该条件将变为和os.path.isfile(';install_name_tool';)。这肯定会失败:install_name_tool不会是运行configure的目录中的一个文件!打补丁的配置脚本将静默跳过此步骤,将库的INSTALL_NAME保留为构建它的TEMPORARY目录!

幸运的是,这个问题的解决方案并不太难。我们应该将绝对路径传递给我们想要运行的程序,而不是路径上的程序名。这可以通过覆盖PrePatch步骤来完成,如下所示:

PrePatch=';';substituteInPlace配置\--替换/bin/sh/usr/bin/python';';+stdenv。利布。OptionalString stdenv.。IsDarwin';';substituteInPlace config/install.py\--替换/usr/bin/install_name_tool${Darwin。Cctools}/bin/install_name_tool';';;

NIX变量${darwin.cctools}将展开到构建的darwin.cctools派生的完整路径,即/nix/store下的目录。因此,PETSc的configure.py内的打补丁的if语句变为。

并且结果库的INSTALL_NAME将是正确的。我们可以使用otool-D再次检查:

看起来好多了!由于错误位于动态加载的库中,我们甚至不需要重新编译就可以检查它是否正常工作:

主构建GIT:(➜)Build dyld_✗_library=1 bin/warpxmdyld:已加载:/Users/jack/src/warpxm/build/bin/warpxmdyld:已加载:/nix/store/z4f1bq363m0ydmbyncfi2srij8vlsx32-Libsystem-osx-10.12.6/lib/libSystem.B.dylibdyld:已加载:/。Nix/store/w23r8kplmfx2xc111cpvmdjwmkwy6ip3-petsc-3.13.2/lib/libpetsc.3.13.dylib...。

我花了大部分时间调试这个问题,而没有对不同的构建阶段有所了解。我应该很清楚,cmake和pkg-config设置都不可能是原因,因为在我调用cmake时,有问题的/private/tmp目录早就消失了。如果我只关注NIX提供的PETScDeriation,我可能会更早地关注install_name_toolpatch。在这过程中,我很幸运地在Nix手册中找到了关于达尔文特有的链接器问题的注释。

至于Nix,我绝对会更多地使用它。值得注意的是,它能产生的影响是如此之小。我可以使用它来管理这个项目的环境,而不会影响其他开发人员管理他们环境的方式。当然,如果他们问起,我会建议他们试试Nix,但每个人都能在自己的时间做这件事是很好的。