tl;dr.入口点(可能)被命名为ZCMain_main_closure
,它是一个引用代码块的数据结构,而不是代码块本身。尽管如此,它仍然可以被 Haskell 运行时解释,并且它直接对应于函数的 Haskell“值”main :: IO ()
在你的main.hs
程序。
较长的答案涉及的内容比您想了解的有关链接程序的内容还要多,但事情是这样的。当你使用如下 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
。正是这个小存根定义了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
file 只是 C 函数main
,而且这不是real你的程序的入口点。这仍然是由crt1.o
stub.
现在,如果我们(最终)从谈论 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
$
but this main
不是你的 Haskellmain
. This main
is a C main
GHC 在链接时动态创建的程序中的函数。您可以通过运行以下命令来查看这样的程序:
$ 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 入口点,你应该在main.o
,无论您是使用普通 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
您可以通过跳转到其入口点来直接运行的文件,那么我有一些坏消息要告诉您...;)