OCaml调用Dynlink导致段故障

时间:2019-07-11 06:46:45

标签: ocaml dynamic-linking ocamlbuild

我有一个OCaml程序,该程序编写另一个OCaml程序,对其进行编译,然后尝试动态加载它。不幸的是,这会导致OSX 10.14计算机OCaml 4.07.1出现分段错误。

特别是我的程序结构如下:

  • 文件A静态加载一堆模块(称其为Helper模块以供参考),并定义:1.描述将动态加载的模块的模块签名; 2.该模块的选项的引用,以由已加载的插件3.设置的另一个使用此引用的模块。
open Helper
module type PLUGIN_TYPE = sig ... end

let plugin = ref None
let get_plugin () : (module PLUGIN_TYPE) =
  match !plugin with
  | Some x -> x
  | None -> failwith "No plugin loaded"

module Test
struct =
... get_plugin () ...
end
  • 文件B是加载程序,简而言之,它运行Dynlink.loadfile

  • 文件C是生成的OCaml文件,该文件还使用Helper模块并定义了PLUGIN_TYPE类型的模块并设置了插件引用。

module Plugin : PLUGIN_TYPE =
...
end

let () = A.plugin := Some (module Plugin : PLUGIN_TYPE)

我使用ocamlbuild来构建主程序,然后再次使用ocamlbuild来构建插件(它需要与主程序相同的Helper模块/文件)。

当我尝试运行此代码时,可能会在执行Dynlink.loadfile时遇到段错误。我不确定自己在做什么错,我同时将Helper模块与主程序和插件链接在一起的事实使我感到不舒服,但是我不确定如何解决它。

附加LLDB跟踪:

* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x0)
  * frame #0: 0x00000001002624da Main.native`caml_oldify_local_roots at roots.c:286 [opt]
    frame #1: 0x00000001002664fb Main.native`caml_empty_minor_heap at minor_gc.c:352 [opt]
    frame #2: 0x0000000100266cc5 Main.native`caml_gc_dispatch at minor_gc.c:446 [opt]
    frame #3: 0x000000010026dca6 Main.native`caml_make_vect(len=<unavailable>, init=<unavailable>) at array.c:335 [opt]
    frame #4: 0x0000000100114eb9 Main.native`camlLru_cache__init_inner_2624 + 89
    frame #5: 0x0000000100087ea6 Main.native`camlSyntax__memoize_7621 + 38
    frame #6: 0x000000010312d317 Plugin.cmxs`camlInterp__entry + 311
    frame #7: 0x0000000100283424 Main.native`caml_start_program + 92
    frame #8: 0x000000010027ad19 Main.native`caml_callback(closure=<unavailable>, arg=<unavailable>) at callback.c:173 [opt]
    frame #9: 0x000000010027f6a0 Main.native`caml_natdynlink_run(handle_v=4345299456, symbol=72181230668639817) at natdynlink.c:141 [opt]
    frame #10: 0x000000010009d727 Main.native`camlDynlink__fun_2440 + 23
    frame #11: 0x0000000100183581 Main.native`camlStdlib__list__iter_1148 + 33
    frame #12: 0x000000010009d5bc Main.native`camlDynlink__loadunits_2288 + 332
    frame #13: 0x000000010009d788 Main.native`camlDynlink__load_2301 + 72
    frame #14: 0x000000010000552c Main.native`camlLoader__load_plugin_1002 + 268
    frame #15: 0x00000001000055d8 Main.native`camlLoader__simulate_1056 + 120
    frame #16: 0x00000001000052c8 Main.native`camlMain__entry + 280
    frame #17: 0x0000000100002489 Main.native`caml_program + 3481
    frame #18: 0x0000000100283424 Main.native`caml_start_program + 92
    frame #19: 0x00000001002617dc Main.native`caml_startup_common(argv=0x00007ffeefbff538, pooling=<unavailable>) at startup.c:157 [opt]
    frame #20: 0x000000010026184b Main.native`caml_main [inlined] caml_startup_exn(argv=<unavailable>) at startup.c:162 [opt]
    frame #21: 0x0000000100261844 Main.native`caml_main [inlined] caml_startup(argv=<unavailable>) at startup.c:167 [opt]
    frame #22: 0x0000000100261844 Main.native`caml_main(argv=<unavailable>) at startup.c:174 [opt]
    frame #23: 0x00000001002618bc Main.native`main(argc=<unavailable>, argv=<unavailable>) at main.c:44 [opt]
    frame #24: 0x00007fff6d4f1ed9 libdyld.dylib`start + 1
    frame #25: 0x00007fff6d4f1ed9 libdyld.dylib`start + 1

对于它们的价值,这些是我所谓的Helper模块的一部分:

    frame #4: 0x0000000100114eb9 Main.native`camlLru_cache__init_inner_2624 + 89
    frame #5: 0x0000000100087ea6 Main.native`camlSyntax__memoize_7621 + 38

关于我在做什么错的任何线索吗?

2 个答案:

答案 0 :(得分:1)

TL; DR;已知的错误。尽可能使用沙丘。如果没有,请手动使用Findlib Dynlink。需要做一些工作但可行。您不是第一个遇到此问题的人。

问题

首先,您做对了所有事情,这是OCaml中一个相对知名的长期错误。尽管如此,resolved才是最近的。不用担心,有两种解决方法(如下所述)。此外,仅供参考,如果您不接触Obj模块或不使用外部(C)存根并获得段错误,那么这绝对是OCaml系统中的错误,因此您可以直接使用OCaml问题跟踪器。幸运的是,这种情况很少发生。

现在,这是怎么回事?问题是OCaml动态链接器没有检查编译单元是否已加载。因此,当您加载一个新单元时,它可能已经被加载,或者依次加载了另一个已经加载的单元。将单元加载到OCaml过程映像时,将调用单元构造函数(初始化函数),该构造函数设置初始根(全局变量)并初始化框架。如果设备已经初始化,则可以避免破坏-重置变量,重写值。如果幸运的话,您会从垃圾收集器中遇到分段错误。这就是您的情况。

解决方案

此修复程序已合并到OCaml 4.08版本中,但您可能对此并不满意。是的,您不会遇到段错误,但是,您的程序会因错误而优雅地失败,该错误指示您正在尝试加载过程映像中已存在的编译单元(Dynlink.Error (Module_already_loaded "module name")异常)。因此,插件系统开发人员有责任维护已加载的模块列表。

您很可能不想开发新系统。好消息是这种系统已经开发出来了(它们甚至适用于OCaml的旧版本,因此它们很强大,可以防止OCaml进行段错误)。

我将在下面提供两种解决方案。两者都依靠Findlib Dynload工具。编译程序(或共享库)时,哪个记录将记录构成程序内部的编译单元的列表,以便以后可以查阅并做出决定,是否应加载该单元以及是否将其加载。与已经加载的单元保持一致(例如,我们不想在过程映像中具有同一库的多个版本)。

沙丘

第一个解决方案是使用Dune。好吧,至少因为它需要最少的工作。沙丘是使用Findlib从零开始实现到work correctly的,因此一切都应立即可用。您只需要将项目移植到Dune,将findlib.dynload指定为宿主程序(加载插件的程序)的依赖项,然后使用Fl_dynload.load_packages加载插件。

OCamlbuild / OASIS

如果由于某些原因您不能将项目移至Dune,则必须自己做一些工作。我们已经在BAP project中实现了自己的插件加载系统,因此您可以基于它构建自己的系统。它是由MIT许可的,因此可以随意获取您喜欢的任何代码并根据自己的喜好对其进行修改。我们的系统提供的功能超出了您的需要(我们使插件自包含,将它们打包为zip文件,等等),但是想法是相同的-使用Fl_dynload并跟踪您的操作重新加载。与往常一样,细节决定成败。如果您使用OASIS或ocamlbuild来构建非平凡的项目(并且您的项目是平凡的,只需将其移植到Dune),则需要注意的是,当ocamlbuild链接内部libraru(即,源代码树中的库)时),它将不使用OCamlFind,因此链接的模块不会报告给Dynload工具。因此,我们必须编写一个OCamlBuild插件来做到这一点。

基本上,您的加载器必须跟踪已经加载了哪些编译单元,并且您的插件必须包含元信息,以告知加载器它需要和提供哪些编译单元。这就需要各方面的合作。这是在BAP中的工作方式:

1)我们拥有bapbuild工具,它是ocamlbuild的增强版,它具有(ocamlbuild)plugin,它知道如何生成*.plugin文件。 .plugin文件是引擎盖下面的zip文件,具有固定的布局(在我们的说法中为bundle)。它包含一个MANIFEST文件,其中包括所需库的列表和提供的单元的列表,以及一些元信息,当然还有代码本身的cmx(和cma)。 (可选)捆绑软件可以包括所有从属库(以使插件在未提供所需库的环境中可加载)。 bapbuild工具将默认打包所有依赖项,并且由于OPAM Universe中的某些库根本不提供cmxs,因此它还将为其构建cmx并将其打包到插件中。注意,

2)我们有bap_plugins runtime library可以加载插件,满足其依赖性并确保没有单元被加载两次。

3)由于主机程序(加载插件)可能会(并且将在其中)包含一些编译单元,因为它将从项目树本地的或来自外部的某些编译单元集进行链接库。因此,我们需要构建系统的一些合作,这些合作将告诉我们已经加载了哪些单元(或者,我们可以解析主机二进制文件的ELF结构,但这听起来不是一个非常便携式且健壮的解决方案)。我们使用ocamlfind.dynlink库,该库通过在内部数据结构中存储用于构建二进制文件的库和包的列表来实现这种合作。我们编写了一个小的pocamlbuild插件] 6,使之能够实现,其余工作由ocamlfind完成(实际上是生成文件并将其链接到主机二进制文件)。

答案 1 :(得分:0)

我不确定这是否是造成段错误的原因,但您的线路:

let A.plugin = Some (module Plugin : PLUGIN_TYPE)

是错误的。您要写的是:

let () = A.plugin := Some (module Plugin : PLUGIN_TYPE)

理想情况下,我建议您在register_plugin中创建函数A,以避免此类错误。

此外,您可能想知道插件的构建是否失败并正确处理。