如何正确地将link目标文件写入Haskell?

How to properly link object files written in Haskell?

大致按照 this tutorial,我设法让这个玩具项目开始工作。它从 C++ 程序调用 Haskell 函数。

这有效(Ubuntu 16.04,GCC 5.4.0),打印 893 – 但它不是很健壮,也就是说,如果我 删除 Haskell 函数的实际调用,即 std::cout << foo(37, 19) << "\n"; 行,然后它在链接步骤失败,并显示错误消息

/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to `base_GHCziTopHandler_flushStdHandles_closure'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to `base_GHCziStable_StablePtr_con_info'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to `base_GHCziPtr_FunPtr_con_info'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to `base_GHCziWord_W8zh_con_info'
/usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so: undefined reference to `base_GHCziIOziException_cannotCompactPinned_closure'
...

显然,包含 Haskell 项目需要额外的库文件。我如何明确地依赖所有必要的东西,以避免这种脆弱性?


包含 foo 调用时构建脚本的输出,ldd 在最终可执行文件中:

++ cabal build
Preprocessing executable 'foo.so' for call-haskell-from-C-0.1.0.0..
Building executable 'foo.so' for call-haskell-from-C-0.1.0.0..
Linking a.out ...
Linking dist/build/foo.so/foo.so ...
++ g++ -I /usr/local/lib/ghc-8.2.1/include '-DFOO="dist/build/foo.so/foo.so-tmp/Foo_stub.h"' -c bar.c++ -o test.o
++ g++ test.o dist/build/foo.so/foo.so -L /usr/local/lib/ghc-8.2.1/rts -lHSrts-ghc8.2.1 -o test
++ env LD_LIBRARY_PATH=dist/build/foo.so:/usr/local/lib/ghc-8.2.1/rts: sh -c 'ldd ./test; ./test'
    linux-vdso.so.1 =>  (0x00007fff23105000)
    foo.so => dist/build/foo.so/foo.so (0x00007fdfc5360000)
    libHSrts-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/rts/libHSrts-ghc8.2.1.so (0x00007fdfc52f8000)
    libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdfc4dbe000)
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdfc49f4000)
    libHSbase-4.10.0.0-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/base-4.10.0.0/libHSbase-4.10.0.0-ghc8.2.1.so (0x00007fdfc4020000)
    libHSinteger-gmp-1.0.1.0-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/integer-gmp-1.0.1.0/libHSinteger-gmp-1.0.1.0-ghc8.2.1.so (0x00007fdfc528b000)
    libHSghc-prim-0.5.1.0-ghc8.2.1.so => /usr/local/lib/ghc-8.2.1/ghc-prim-0.5.1.0/libHSghc-prim-0.5.1.0-ghc8.2.1.so (0x00007fdfc3b80000)
    libgmp.so.10 => /usr/lib/x86_64-linux-gnu/libgmp.so.10 (0x00007fdfc3900000)
    libffi.so.6 => /usr/local/lib/ghc-8.2.1/rts/libffi.so.6 (0x00007fdfc36f3000)
    libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdfc33ea000)
    librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007fdfc31e2000)
    libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007fdfc2fde000)
    libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007fdfc2dc1000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fdfc5140000)
    libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdfc2bab000)

通常 ghc 使用 -Wl,--no-as-needed 选项链接可执行文件,您也应该使用它。 (您可以检查 ghc 如何链接可执行文件,例如使用 cabal build --ghc-options=-v3。)

您可以找到更多详细信息 here。接下来我的理解是:foo.so 需要在运行时根据需要加载 libHSbase-4.10.0.0-ghc8.2.1.so,即当我们需要它的符号时(检查 readelf -a dist/build/foo.so/foo.so | grep NEEDED)。因此,如果您不调用 foo,则不会加载 base.so。但是ghc需要加载所有库(我不知道为什么)。 --no-as-needed 选项强制加载所有库。

注意--no-as-needed选项是依赖于位置的,所以把它放在共享库之前。

这个答案解释了在 linkage 期间发生的事情,为什么 -Wl,--no-as-needed 的解决方案有效,以及应该做些什么来获得更稳健的方法。

简而言之:-lHSrts-ghcXXX.so 取决于 libHSbaseXXX.solibHSinteger-gmpXXX.solibHSghc-primXXX.so,必须在 link 期间提供给 linker =]年龄。

这里提出的解决方案依赖于大量的手动工作,而且可扩展性不是很好。但是,我对 cabal 的了解还不够多,无法告诉您如何将其自动化,但我希望您能完成最后一步。

或者您可能会使用 -Wl,--no-as-needed 解决方案,因为您知道幕后发生的事情。


让我们从不调用 foo 的情况下逐步完成版本的 linking 过程开始,以一种稍微简化的方式(here 是 Eli Bendersky 的一篇很棒的文章,即使它是关于静态 linkage):

  1. linker 维护着 table 个符号,必须为所有符号找到 definitions/machine-code。让我们简化并假设,一开始它在table中只有符号main并且这个符号的定义是未知的。

  2. 符号 main 的定义是在目标文件 test.o 中找到的。但是,函数 main 使用函数 hs_iniths_exit。因此我们找到了main的定义,但是除非我们知道hs_iniths_exit的定义,否则它不起作用。所以现在我们必须寻找它们的定义。

  3. 在下一步中,linker 查看 foo.so,但是 foo.so 没有定义我们感兴趣的任何符号(foo 未使用!)并且 linker 只是跳过 foo.so 并且永远不会回头看。

  4. link人看着-lHSrts-ghcXXX.so。它在那里找到 hs_iniths_exit 的定义。因此,共享库的全部内容都被使用,但它需要定义诸如 base_GHCziTopHandler_flushStdHandles_closure 之类的符号。这意味着 linker 开始寻找这些符号的定义。

  5. 但是命令行中没有更多的库,因此 linker 没有什么可看的,linkage fails/is 没有成功,因为缺少某些符号的定义。

使用foo的情况有什么不同?在第 2 步之后,不仅要 hs_iniths_exit,还要 foo,在 foo.so 中找到。所以必须包含foo.so

由于库 foo.so 的构建方式,包含以下信息:

>>> readelf -d dist/build/foo.so/foo.so | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libHSrts-ghc7.10.3.so]
 0x0000000000000001 (NEEDED)             Shared library: [libHSbase-4.8.2.0-HQfYBxpPvuw8OunzQu6JGM-ghc7.10.3.so]
 0x0000000000000001 (NEEDED)             Shared library: [libHSinteger-gmp-1.0.0.0-2aU3IZNMF9a7mQ0OzsZ0dS-ghc7.10.3.so]
 0x0000000000000001 (NEEDED)             Shared library: [libHSghc-prim-0.4.0.0-8TmvWUcS1U1IKHT0levwg3-ghc7.10.3.so]
 0x0000000000000001 (NEEDED)             Shared library: [libgmp.so.10]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

>>> readelf -d dist/build/foo.so/foo.so | grep RPATH
 0x000000000000000f (RPATH)              Library rpath: [
          /usr/lib/ghc/base_HQfYBxpPvuw8OunzQu6JGM:
          /usr/lib/ghc/rts:
          /usr/lib/ghc/ghcpr_8TmvWUcS1U1IKHT0levwg3:
          /usr/lib/ghc/integ_2aU3IZNMF9a7mQ0OzsZ0dS]

根据此信息,linker 知道需要哪些共享库(NEEDED-flag)以及它们在系统中的什么位置(RPATH)。这些库是 found/opened/processed(即标记为需要),因此所有必要的定义都存在。

您可以通过添加

来了解整个过程
g++ ...
    -Wl,--trace-symbol=base_GHCziTopHandler_flushStdHandles_closure \
    -Wl,--verbose \
    -o test

进入link年龄阶段。

如果我们按照@Yuras 的建议强制将 foo.so 通过 -Wl,--no-as-needed 包含到生成的 executable 中,也会发生同样的事情。

这个分析的结果是什么?

我们应该在命令行上提供所需的库(在 -lHSrts-ghcXXX.so 之后),而不是依赖于通过其他共享库偶然添加它们。显然,有些神秘的名称仅对我的安装有效:

g++ ...
   -L/usr/lib/ghc/base_HQfYBxpPvuw8OunzQu6JGM  -lHSbase-4.8.2.0-HQfYBxpPvuw8OunzQu6JGM-ghc7.10.3 \
   -L/usr/lib/ghc/integ_2aU3IZNMF9a7mQ0OzsZ0dS -lHSinteger-gmp-1.0.0.0-2aU3IZNMF9a7mQ0OzsZ0dS-ghc7.10.3 \
   -L/usr/lib/ghc/ghcpr_8TmvWUcS1U1IKHT0levwg3 -lHSghc-prim-0.4.0.0-8TmvWUcS1U1IKHT0levwg3-ghc7.10.3 \
   ...
   -o test

现在构建,但在 运行 时不加载(毕竟正确的 rpath 仅在 foo.so 中设置,但 foo.so 不是用过的)。要修复它,我们可以扩展 LD_LIBRARY_PATH 或添加 -rpath link-命令行:

g++ ...
   -L...  -lHSbase-...  -Wl,-rpath,/usr/lib/ghc/base_HQfYBxpPvuw8OunzQu6JGM  \
   -L... -lHSinteger-gmp-... -Wl,-rpath,/usr/lib/ghc/integ_2aU3IZNMF9a7mQ0OzsZ0dS \
   -L... -lHSghc-prim-...  -Wl,-rpath,/usr/lib/ghc/ghcpr_8TmvWUcS1U1IKHT0levwg3 \
   ...
   -o test

必须有一个实用程序来自动获取路径和库名称(cabal 似乎在构建 foo.so 时这样做),但我不知道该怎么做,因为我没有使用 haskell/cabal.