Ocaml简单编译器

990 阅读3分钟

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

从打印数字说起

首先看下面一组C代码,用来打印数字

#include <stdio.h>
#include <inttypes.h>

int64_t entry() {
  return 4000000000000;

}

int main(int argc, char **argv) {
  printf("%" PRIi64, entry());
  return 0;

}

编译如下

➜  Ocaml gcc -o test test.c
➜  Ocaml ./test 
4000000000000%                     

如果使用cat来看编译出的程序内容会很复杂。可以使用如下的命令将器编译成汇编程序

➜  Ocaml gcc -S -masm=intel -m64 test.c
➜  Ocaml cat test.s
	.section	__TEXT,__text,regular,pure_instructions
	.build_version macos, 11, 0	sdk_version 12, 0
	.intel_syntax noprefix
	.globl	_entry                          ## -- Begin function entry
	.p2align	4, 0x90
_entry:                                 ## @entry
	.cfi_startproc
## %bb.0:
	push	rbp
	.cfi_def_cfa_offset 16
	.cfi_offset rbp, -16
	mov	rbp, rsp
	.cfi_def_cfa_register rbp
	movabs	rax, 4000000000000
	pop	rbp
	ret
	.cfi_endproc
                                        ## -- End function
	.globl	_main                           ## -- Begin function main
	.p2align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## %bb.0:
	push	rbp
	.cfi_def_cfa_offset 16
	.cfi_offset rbp, -16
	mov	rbp, rsp
	.cfi_def_cfa_register rbp
	sub	rsp, 16
	mov	dword ptr [rbp - 4], 0
	mov	dword ptr [rbp - 8], edi
	mov	qword ptr [rbp - 16], rsi
	call	_entry
	mov	rsi, rax
	lea	rdi, [rip + L_.str]
	mov	al, 0
	call	_printf
	xor	eax, eax
	add	rsp, 16
	pop	rbp
	ret
	.cfi_endproc
                                        ## -- End function
	.section	__TEXT,__cstring,cstring_literals
L_.str:                                 ## @.str
	.asciz	"%lli"

.subsections_via_symbols

在上面的汇编语言上,我们可以发现两个section_entry_main对应于test.c中的entrymain两个函数。

为了编写一个简单的编译器,我们将重写entry函数(我们已经在main中调用,这是我们编译器生成代码的入口点)。

首先,我们修改我们的C程序, runtime.c

#include <stdio.h>
#include <inttypes.h>

extern int64_t entry();

int main(int argc, char **argv) {
  print("%" PRIi64, entry());
  return 0;
}

这个程序作为我们编译器的runtime。这个runtime可以被编译器生成任何的程序所包含。

我们可以编译runtime

➜  Ocaml gcc -c runtime.c -o runtime.o

这里 -c 告诉GCC不需要变易整个程序,而是将其编译成一部分机器代码。回到我们的entry函数,其对应的汇编程序可以简化的看成是

global _entry
_entry:                                 ## @entry
	movabs	rax, 4000000000000
	ret
  • global _entry: 声明即将定义一个函数
  • _entry: 函数的起始位置
  • movabs rax, 4000000000000: 将4000000000000写入rax寄存器
  • ret: 函数范围

我们可以对这个汇编程序进行编译:

➜  Ocaml nasm test.s -f macho64 -o test.o # in linux use -f elf64

然后将两个.o 的文件编译

➜  Ocaml gcc test.o runtime.o -o program
➜  Ocaml ./program 
4000000000000%

编译器

OK, 我们使用Ocaml来完成这件事,其中compile.ml程序如下

let compile (program: string): string =
  String.concat "\n"
  [ "global _entry";
  "_entry:";
  Printf.sprintf "\tmov rax, %s" program;
  "\tret"]

简单的将上面的汇编程序放到ocaml字符串中,在Ocaml环境中,

# #use "compile.ml"
  ;;
val compile : string -> string = <fun>
# print_endline( compile "4000000000000" )
  ;;
global _entry
_entry:
	mov rax, 4000000000000
	ret

我们可以将所有的东西粘合起来

let compile (program: string) : string =
  String.concat "\n" 
  ["global _entry";
  "_entry:";
  Printf.sprintf "\tmov rax, %s" program;
  "\tret"]

let compile_to_file (program: string): unit =
  let file = open_out "program.s" in
  output_string file (compile program);
  close_out file

let compile_and_run (program: string): string =
  compile_to_file program;
  ignore (Unix.system "nasm program.s -f macho64 -o program.o");
  ignore (Unix.system "gcc program.o runtime.o -o program");
  let inp = Unix.open_process_in "./program" in
  let r = input_line inp in
  close_in inp; r

在控制台

>>> #use "compile.ml";;
>>> utop # compile_to_file "42";;
- : unit = ()  

>>> utop # compile_and_run "7";;
- : string = "7"