如何正确链接用Haskell编写的目标文件?

21

大致按照这篇教程的步骤,我成功地实现了这个玩具项目。它可以在C++程序中调用一个Haskell函数。

  • Foo.hs

    {-# LANGUAGE ForeignFunctionInterface #-}
    
    module Foo where
    
    foreign export ccall foo :: Int -> Int -> IO Int
    
    foo :: Int -> Int -> IO Int
    foo n m = return . sum $ f n ++ f m
    
    f :: Int -> [Int]
    f 0 = []
    f n = n : f (n-1)
    
  • bar.c++

  • #include "HsFFI.h"
    #include FOO       // Haskell module (path defined in build script)
    
    #include <iostream>
    
    int main(int argc, char *argv[]) {
      hs_init(&argc, &argv);
    
      std::cout << foo(37, 19) << "\n";
    
      hs_exit();
      return 0;
    }
    
  • call-haskell-from-cxx.cabal

    name:                call-haskell-from-cxx
    version:             0.1.0.0
    build-type:          Simple
    cabal-version:       >=1.10
    
    executable foo.so
      main-is:          Foo.hs   
      build-depends:       base >=4.10 && <4.11
      ghc-options:         -shared -fPIC -dynamic
      extra-libraries:     HSrts-ghc8.2.1
      default-language:    Haskell2010
    
  • 构建脚本

    #!/bin/bash
    
    hs_lib="foo.so"
    hs_obj="dist/build/$hs_lib/$hs_lib"
    
    ghc_version="8.2.1"                          # May need to be tweaked,
    ghc_libdir="/usr/local/lib/ghc-$ghc_version" # depending on system setup.
    
    set -x
    
    cabal build
    
    g++ -I "$ghc_libdir/include" -D"FOO=\"${hs_obj}-tmp/Foo_stub.h\"" -c bar.c++ -o test.o
    g++ test.o "$hs_obj" \
       -L "$ghc_libdir/rts" "-lHSrts-ghc$ghc_version" \
       -o test
    
    env LD_LIBRARY_PATH="dist/build/$hs_lib:$ghc_libdir/rts:$LD_LIBRARY_PATH" \
      ./test
    

    这可以工作(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项目会引入其他必需的库文件。我该如何明确依赖于所有必要的内容,以避免这种脆弱性?


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

    ++ 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)
    

1
你可能需要至少链接到 base,即 -L "$ghc_libdir/base-x.y.z.t" "-lHSrts-base-x.y.z.t$ghc_version",其中 x.y.z.t 是你的 base 版本。 - n. m.
1
你也可以尝试使用ghc作为链接器,并包含必要的C++内容,而不是反过来。 - luqui
我无法在 ghc-8.2.1 和 gcc-7.3.0 中重现它。请在构建脚本的最后一步中运行 ldd ./test 而不是 ./test,然后发布输出。 - Yuras
@Yuras 我已经在问题中添加了 ldd 信息。 - leftaroundabout
@leftaroundabout 如果在链接可执行文件时添加-Wl,--no-as-needed作为第一个参数会怎样呢?例如:g++ -Wl,--no-as-needed test.o "$hs_obj" <...> - Yuras
@Yuras 是的,即使没有调用 foo 函数,它也可以成功链接。(并且输出相同的 ldd。)但这似乎是一个不太优雅的解决方法... - leftaroundabout
2个回答

3
这个答案解释了链接过程中发生的事情,为什么使用-Wl,--no-as-needed解决方案可行,以及应该采取什么措施来实现更加健壮的方法。简而言之:-lHSrts-ghcXXX.so依赖于libHSbaseXXX.solibHSinteger-gmpXXX.solibHSghc-primXXX.so,在链接过程中必须向链接器提供这些依赖项。这里提出的解决方案需要大量手动工作,不太可扩展。然而,我对cabal的了解不足,无法告诉您如何自动化此过程,但希望您能完成最后一步。或者,您也可以使用-Wl,--no-as-needed解决方案,因为您知道幕后发生了什么。

让我们首先以一种较为简化的方式,逐步了解不调用foo版本的链接过程(此处是Eli Bendersky的一篇优秀文章,即使它是关于静态链接的):

  1. 链接器维护一个符号表,并需要找到所有符号的定义/机器码。让我们简化一下,假设在开始时,表中只有符号main,并且这个符号的定义是未知的。

  2. 符号main的定义可以在目标文件test.o中找到。然而,函数main使用了函数hs_iniths_exit。因此,我们找到了main的定义,但如果不知道hs_iniths_exit的定义,它就无法工作。所以现在我们必须寻找它们的定义。

  3. 接下来,链接器查看foo.so,但是foo.so没有定义我们感兴趣的任何符号(foo没有被使用!),链接器会跳过foo.so,并且不会再回头查看。

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

  5. 然而,在命令行上没有更多的库,因此链接器没有任何东西可查看,链接失败/不成功,因为一些符号的定义缺失。

在使用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]

从这些信息中,链接器知道哪些共享库是必需的(NEEDED标志),以及它们在您的系统上的位置(RPATH)。这些库被找到/打开/处理(即标记为所需),因此所有必要的定义都存在。
您可以通过添加 -Wl,-verbose 标志来跟踪整个过程。
g++ ...
    -Wl,--trace-symbol=base_GHCziTopHandler_flushStdHandles_closure \
    -Wl,--verbose \
    -o test

进入链接步骤。

如果我们强制使用-Wl,--no-as-needed来将foo.so包含到生成的可执行文件中,就会发生同样的事情,正如@Yuras所建议的那样。

这个分析的后果是什么?

我们应该在命令行上提供所需的库(在-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
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

一定有一个工具可以自动获取路径和库名称(当构建foo.so时,cabal似乎会这样做),但我不知道如何做,因为我没有使用haskell/cabal的经验。


2
通常,ghc 使用 -Wl,--no-as-needed 选项链接可执行文件,您也应该使用它。(您可以使用 cabal build --ghc-options=-v3 命令检查 ghc 如何链接可执行文件。)您可以在此处找到更多详细信息。我理解的是:当我们需要其符号时(使用命令 readelf -a dist/build/foo.so/foo.so | grep NEEDED 检查),foo.so 需要在运行时根据需要加载 libHSbase-4.10.0.0-ghc8.2.1.so。因此,如果您不调用 foo,则不会加载 base.so。但是,ghc 需要加载所有库(我不知道为什么)。--no-as-needed 选项强制加载所有库。请注意,--no-as-needed 选项与位置有关,因此请将其放置在共享库之前。

好的,我想我必须这样做。你能解释一下为什么需要这样做吗? - leftaroundabout
@leftaroundabout,我不知道所有的细节,最好是问 GHC 开发者。我已经添加了基本解释,但不要依赖它,这可能是我理解上的完全误解。 - Yuras

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接