将Haskell程序编译为LLVM IR缺少主程序

时间:2018-08-29 03:21:25

标签: haskell compilation llvm-ir lli

关于this SO post,关于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]

1 个答案:

答案 0 :(得分:3)

tl; dr。。入口点(可能)名为ZCMain_main_closure,它是引用代码块而不是代码块本身的数据结构。尽管如此,它仍可以由Haskell运行时解释,它直接对应于main :: IO ()程序中函数main.hs的Haskell“值”。

更长的答案涉及到比链接程序更多的知识,但这就是问题。当您使用C语言程序时:

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

使用gcc将其编译为目标文件:

$ gcc -Wall -c hello.c

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

$ nm hello.o
0000000000000000 T main
                 U printf

您将看到它包含符号main的定义和对外部符号printf的(未定义)引用。

现在,您可能会想到main是该程序的“入口点”。哈哈哈哈!您认为这是天真又愚蠢的事情!

实际上,真正的Linux专家知道您程序的入口点根本不在目标文件hello.o中。它在哪里?好吧,它位于"C runtime"中,这是一个小文件,在您实际创建可执行文件时会被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 undefined 引用,该引用将链接到hello.o中的所谓入口点。这个小存根定义了 real 入口点,即_start。您可以说出这是实际的入口点,因为如果将程序链接到可执行文件中,则会看到_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

这就是说,程序的“入口”实际上是一个非常复杂的概念。

当您使用LLVM工具链而不是GCC编译并运行C程序时,情况都非常相似。这是设计使所有内容与GCC兼容。 hello.ll文件中的所谓入口点就是C函数main,而不是程序的 real 入口点。 crt1.o存根仍提供该功能。

现在,如果我们(最终)从谈论C切换到谈论Haskell,则Haskell运行时显然比C运行时复杂约十亿倍,但它是在C运行时之上构建的。因此,当您以常规方式编译Haskell程序时:

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

您会看到可执行文件具有一个名为_start的入口点:

$ nm main | egrep 'T _start'
0000000000406560 T _start

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

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

但是 this main不是您的Haskell main。此main是GHC在链接时动态创建的程序中的C main函数。您可以通过运行以下程序来查看此类程序:

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

然后在ghc_4.c子目录中的某个地方翻阅一个名为/tmp的文件:

$ 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入口点,无论您是使用普通GHC管道还是通过LLVM后端进行编译,都应在main.o中找到它:

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

现在,它不是“功能”。 Haskell运行时系统可以理解这是一种特殊格式的数据结构(闭包)。上面的hs_main()函数(还有另一个入口点!)是Haskell运行时的主要入口点:

$ 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入口点,开始执行程序。

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