「这是我参与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中的entry和main两个函数。
为了编写一个简单的编译器,我们将重写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"