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



关注这篇关于Haskell程序编译的SO帖子 对于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 运行时解释的,并且它直接对应于main.hs程序中main :: IO ()函数的 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 运行时"中,这是一个小文件,当你实际创建可执行文件时,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未定义引用,该引用将链接到hello.o中所谓的入口点。 正是这个小存根定义了真正的切入点,即_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,它不是程序的真正入口点。 这仍然由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
$ 

但这main不是你的哈斯克尔main. 此main是 GHC 在链接时动态创建的程序中的 Cmain函数。 您可以通过运行以下命令查看这样的程序:

$ 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}>
...

现在,它不是一个"功能"。 它是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文件中,你可以通过跳转到它的入口点来以某种方式直接运行,那么我有一些坏消息要告诉你...... ;)

最新更新