【问题标题】:Compiled Haskell program to LLVM IR is missing main编译到 LLVM IR 的 Haskell 程序缺少 main
【发布时间】:2018-08-29 03:21:24
【问题描述】:

关注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 运行它时,我收到以下关于缺少 main 的错误:

$ 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]

【问题讨论】:

  • 也许this old answer of mine 会帮助你。
  • @K.A.Buhr 谢谢它的工作!我仍然有点困惑,因为创建的 *.ll 文件必须包含某种入口点,对吗?显然它没有被称为 main ,但它必须存在,不是吗?这个“简单”的 haskell 快速排序在 *.ll 文件中有 1706 行长,所以我在那里找不到任何东西。另外,能否请您发布答案让我接受?
  • 有入口点但需要链接到ghc运行时。
  • @DanRobertson 我怎样才能找到它?我已经为“入口点”添加了 *.ll 文件,当我将标准 C 文件编译为 *.bc 然后编译为 *.ll 时,它总是出现......但是那里没有这样的东西......也为@987654329 grepped @(而不是 main)确实找到了一些结果,但它们似乎都无关紧要
  • @OrenIshShalom 恐怕我不知道。必须有某种方法可以在.ll 文件中找到函数,但我不知道它是什么

标签: haskell compilation llvm-ir lli


【解决方案1】:

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

请注意,此对象文件有一个对mainundefined 引用,它将链接到您在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,并不是你程序的真正的入口点。这仍然由 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 不是你的 Haskell mainmain 是 GHC 在链接时动态创建的程序中的 C main 函数。你可以通过运行来查看这样的程序:

$ 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 入口点,无论你是使用 vanilla 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 入口点。

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

【讨论】:

  • 非常详细的答案!!分步命令确实有助于理解。感谢您花时间编写它,这绝对是我今年在 SO 中阅读的最佳答案之一。
猜你喜欢
  • 1970-01-01
  • 2023-03-10
  • 2012-11-05
  • 1970-01-01
  • 2014-07-15
  • 1970-01-01
  • 1970-01-01
  • 2012-02-27
  • 1970-01-01
相关资源
最近更新 更多