编译为 LLVM IR 的 Haskell 程序缺少 main

Compiled Haskell program to LLVM IR is missing main

以下 关于 Haskell 程序的编译 对于 LLVM IR,我采用了相同的 Haskell 程序并尝试 运行 其生成的 LLVM IR 代码:

quicksort [] = []
quicksort (p:xs) = (quicksort lesser) ++ [p] ++ (quicksort greater)
  where
    lesser  = filter (<  p) xs
    greater = filter (>= p) xs

main = print(quicksort([5,2,1,0,8,3]))

我首先用

将它编译成 LLVM IR
$ ghc -keep-llvm-files main.hs

然后我将它转换为位码:

$ llvm-as main.ll

但是,当我尝试使用 lli 运行 它时,我收到以下关于缺少主线程的错误:

$ lli main.bc
'main' function not found in module.

我是不是做错了什么?谢谢。

编辑:(来自 K. A. Buhr 的回答)

$ ls -l main*
main.hs
$ ghc -keep-llvm-files main.hs
[1 of 1] Compiling Main             ( main.hs, main.o )
Linking main ...
$ ls -l main*
main
main.hi
main.hs
main.ll
main.o
$ rm main main.hi main.o
$ llvm-as main.ll
$ llc main.bc -filetype=obj -o main.o
$ ghc -o main main.o
$ ./main
[0,1,2,3,5,8]

tl;dr. 入口点(可能)被命名为 ZCMain_main_closure,它是一个引用代码块的数据结构,而不是引用块代码本身。尽管如此,它还是被Haskell运行时间解释table,它直接对应于main :: IO ()中函数Haskell"value"main.hs 程序.

关于 linking 程序,较长的答案涉及的内容比您想知道的要多,但事情就是这样。当你使用 C 程序时:

#include <stdio.h>
int main()
{
        printf("I like C!\n");
}

gcc将它编译成目标文件:

$ gcc -Wall -c hello.c

并检查目标文件的符号 table:

$ nm hello.o
0000000000000000 T main
                 U printf

您会看到它包含符号 main 的定义和对外部符号 printf.

的(未定义)引用

现在,您可能会想象 main 就是这个程序的 "entry point"。哈哈哈哈!你这样想真是天真愚蠢!

事实上,真正的 Linux 专家都知道您的程序的入口点根本不在目标文件 hello.o 中。它在哪里?好吧,它在 "C runtime" 中,当您实际创建 executable:

时,link 被 gcc 编辑的一个小文件
$ nm /usr/lib/x86_64-linux-gnu/crt1.o
0000000000000000 D __data_start
0000000000000000 W data_start
0000000000000000 R _IO_stdin_used
                 U __libc_csu_fini
                 U __libc_csu_init
                 U __libc_start_main
                 U main
0000000000000000 T _start
$

请注意,此目标文件有一个 未定义main 的引用,它将 linked 到您在 [=29 中所谓的入口点=].正是这个小存根定义了 真正的 入口点,即 _start。你可以看出这是实际的入口点,因为如果你 link 程序进入 executable,你会看到 _start 符号的位置和 ELF 入口点(是当你 execve() 你的程序重合时内核实际首先转移控制的地址:

$ gcc -o hello hello.o
$ nm hello | egrep 'T _start'
0000000000400430 T _start
$ readelf -h hello | egrep Entry
Entry point address:               0x400430

也就是说,程序的"entry point"其实是一个很复杂的概念。

当您使用 LLVM 工具链而不是 GCC 编译和 运行 C 程序时,情况非常相似。这是为了让一切都与 GCC 兼容而设计的。你的hello.ll文件中所谓的入口点只是C函数main,而不是你程序的真正的入口点。这仍然由 crt1.o 存根提供。

现在,如果我们(最终)从讨论 C 切换到讨论 Haskell,Haskell 运行时间显然比C 运行time,但它是建立在 C 运行time 之上的。因此,当您以正常方式编译 Haskell 程序时:

$ ghc main.hs
stack ghc -- main.hs
[1 of 1] Compiling Main             ( main.hs, main.o )
Linking main ...
$

你可以看到 executable 有一个名为 _start:

的入口点
$ nm main | egrep 'T _start'
0000000000406560 T _start

实际上与之前调用 C 入口点的 C 运行时间存根相同:

$ nm main | egrep 'T main'
0000000000406dc4 T main
$ 

但是这个main不是你的Haskellmainmain 是 GHC 在 link 时动态创建的程序中的 C main 函数。你可以通过运行ning看这样一个程序:

$ ghc -v -keep-tmp-files -fforce-recomp main.hs

并在 /tmp 子目录中的某处查找名为 ghc_4.c 的文件:

$ cat /tmp/ghc10915_0/ghc_4.c
#include "Rts.h"
extern StgClosure ZCMain_main_closure;
int main(int argc, char *argv[])
{
 RtsConfig __conf = defaultRtsConfig;
 __conf.rts_opts_enabled = RtsOptsSafeOnly;
 __conf.rts_opts_suggestions = true;
 __conf.rts_hs_main = true;
 return hs_main(argc,argv,&ZCMain_main_closure,__conf);
}

现在,您看到对 ZCMain_main_closure 的外部引用了吗?不管你信不信,那是你程序的 Haskell 入口点,你应该在 main.o 中找到它,无论你是使用 vanilla GHC 管道还是通过 LLVM 后端编译:

$ egrep ZCMain_main_closure main.ll
%ZCMain_main_closure_struct = type <{i64, i64, i64, i64}>
...

现在,它不是 "function"。它是 Haskell 运行 时间系统理解的特殊格式的数据结构(闭包)。上面的 hs_main() 函数(又一个入口点!)是进入 Haskell 运行time:

的主要入口点
$ nm ~/.stack/programs/x86_64-linux/ghc-8.4.3/lib/ghc-8.4.3/rts/libHSrts.a | egrep hs_main
0000000000000000 T hs_main
$

它接受一个 Haskell 主函数的闭包作为 Haskell 入口点开始执行你的程序。

因此,如果您经历了所有这些麻烦,希望在 *.ll 文件中隔离一个 Haskell 程序,您可以通过跳转到其入口点以某种方式 运行 直接, 那么我有一些坏消息要告诉你... ;)