OCaml 调用 Dynlink 导致段错误

OCaml call to Dynlink causes seg fault

我有一个 OCaml 程序,它编写另一个 OCaml 程序,编译它,然后尝试动态加载它。不幸的是,这会导致我的 OSX 10.14 机器 OCaml 4.07.1.

出现分段错误

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

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
module Plugin : PLUGIN_TYPE =
...
end

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

我使用 ocamlbuild 构建主程序,然后再次使用 ocamlbuild 构建插件(这需要与主程序相同的 Helper modules/files)。

当我尝试 运行 时出现段错误,大概是在执行 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

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

我不确定这是否是您的段错误的原因,但您的行:

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

错了。你要写的是:

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

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

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

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

问题

首先,你做的一切都是对的,这是 OCaml 中一个相对 well-known long-term 的错误。尽管如此,它还是 resolved 最近才出现的。别担心,有几个解决方法(如下所述)。此外,仅供参考,如果您没有接触 Obj 模块或玩外部 (C) 存根,并遇到段错误,那么这绝对是 OCaml 系统中的错误,因此您可以直接转到 OCaml 问题跟踪器。幸运的是,这种情况很少发生。

现在,发生了什么事?问题是 OCaml 动态链接器没有检查编译单元是否已经加载。因此,当您加载一个新单元时,它可能已经加载,或者反过来加载另一个已经加载的单元。当一个单元被加载到 OCaml 过程映像中时,单元构造函数(初始化函数)被调用,它设置初始根(全局变量)并初始化帧。如果这个单元已经被初始化,它就会破坏——变量被重置,值被重写。如果幸运的话,您会从垃圾收集器中得到一个分段错误。这就是你的情况。

解决方案

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

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

我将在下面提供两种解决方案。两者都依赖于 Findlib Dynload 设施。当一个程序(或共享对象)被编译时,它会在程序本身内部记录构成它的编译单元列表,以便以后可以参考它并做出决定,是否应该加载该单元,以及它是否是与已加载的单元一致(例如,我们不希望在过程映像中有同一个库的多个版本)。

沙丘

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

OCamlbuild/OASIS

如果由于某些原因您不能将您的项目移动到 Dune,那么您必须自己做一些工作。我们已经实现了自己的插件加载系统作为 BAP project 的一部分,因此您可以基于它构建自己的系统。它遵循 MIT 许可,因此请随意获取您喜欢的任何代码并根据您的喜好进行修改。我们的系统提供的功能比您可能需要的多一点(我们制作插件 self-contained,将它们打包为 zip 文件等),但想法是一样的 - 使用 Fl_dynload 并跟踪内容你正在加载。一如既往,细节决定成败。如果您使用 OASIS 或 ocamlbuild 来构建 non-trivial 项目(如果您的项目很简单,那么只需将其移植到 Dune),那么需要注意的是,当 ocamlbuild 链接一个内部库(即,您的库源代码树)它不会使用 OCamlFind,因此链接的模块不会报告给 Dynload 工具。因此,我们必须编写一个 OCamlBuild 插件来执行此操作。

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

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

2) 我们有 bap_plugins runtime library whih 加载插件,完成它们的依赖关系并确保没有单元被加载两次。

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