Initial commit of gdb-experiment.go
This commit is contained in:
parent
6ebb141b1d
commit
4efc032827
355
gdb-experiment.go
Normal file
355
gdb-experiment.go
Normal file
@ -0,0 +1,355 @@
|
||||
// Non-optimizing Brainfuck compiler generating binaries for Linux on x86-64;
|
||||
// gofmt has been tried, with disappointing results
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const ( RIGHT = iota; LEFT; INC; DEC; IN; OUT; BEGIN; END )
|
||||
|
||||
var info = []struct {
|
||||
grouped bool
|
||||
name string
|
||||
}{
|
||||
{true, "RIGHT"},
|
||||
{true, "LEFT"},
|
||||
{true, "INC"},
|
||||
{true, "DEC"},
|
||||
{false, "IN"},
|
||||
{false, "OUT"},
|
||||
{false, "BEGIN"},
|
||||
{false, "END"},
|
||||
}
|
||||
|
||||
type instruction struct {
|
||||
command int
|
||||
arg int
|
||||
}
|
||||
|
||||
// Dump internal representation to a file for debugging purposes
|
||||
func dump(filename string, irb []instruction) error {
|
||||
out, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
indent := 0
|
||||
for _, x := range irb {
|
||||
if x.command == END {
|
||||
indent--
|
||||
}
|
||||
for i := 0; i < indent; i++ {
|
||||
out.WriteString(" ")
|
||||
}
|
||||
out.WriteString(info[x.command].name)
|
||||
if info[x.command].grouped {
|
||||
fmt.Fprintf(out, " %d", x.arg)
|
||||
}
|
||||
out.WriteString("\n")
|
||||
if x.command == BEGIN {
|
||||
indent++
|
||||
}
|
||||
}
|
||||
if err = out.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode a Brainfuck program into internal representation,
|
||||
// coalescing identical commands together as the most basic optimization
|
||||
func decode(program []byte) (irb []instruction) {
|
||||
for _, c := range program {
|
||||
var command int
|
||||
switch c {
|
||||
case '>': command = RIGHT
|
||||
case '<': command = LEFT
|
||||
case '+': command = INC
|
||||
case '-': command = DEC
|
||||
case '.': command = OUT
|
||||
case ',': command = IN
|
||||
case '[': command = BEGIN
|
||||
case ']': command = END
|
||||
default: continue
|
||||
}
|
||||
|
||||
if len(irb) == 0 || !info[command].grouped ||
|
||||
irb[len(irb)-1].command != command {
|
||||
irb = append(irb, instruction{command, 1})
|
||||
} else {
|
||||
irb[len(irb)-1].arg++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Match loop commands so that we know where to jump
|
||||
func pairLoops(irb []instruction) error {
|
||||
nesting := 0
|
||||
stack := make([]int, len(irb))
|
||||
for i, x := range irb {
|
||||
switch x.command {
|
||||
case BEGIN:
|
||||
stack[nesting] = i
|
||||
nesting++
|
||||
case END:
|
||||
if nesting <= 0 {
|
||||
return errors.New("unbalanced loops")
|
||||
}
|
||||
nesting--
|
||||
irb[stack[nesting]].arg = i + 1
|
||||
irb[i].arg = stack[nesting] + 1
|
||||
}
|
||||
}
|
||||
if nesting != 0 {
|
||||
return errors.New("unbalanced loops")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Code generation ---------------------------------------------------------
|
||||
|
||||
type codegen struct {
|
||||
buf []byte
|
||||
}
|
||||
|
||||
// Convert an arbitrary integral value up to 8 bytes long to little endian
|
||||
func le(unknown interface{}) []byte {
|
||||
// Trying hard to avoid reflect.Value.Int/Uint
|
||||
formatted := fmt.Sprintf("%d", unknown)
|
||||
|
||||
var v uint64
|
||||
if unsigned, err := strconv.ParseUint(formatted, 10, 64); err == nil {
|
||||
v = unsigned
|
||||
} else if signed, err := strconv.ParseInt(formatted, 10, 64); err == nil {
|
||||
v = uint64(signed)
|
||||
} else {
|
||||
panic("cannot convert to number")
|
||||
}
|
||||
return []byte{byte(v), byte(v >> 8), byte(v >> 16), byte(v >> 24),
|
||||
byte(v >> 32), byte(v >> 40), byte(v >> 48), byte(v >> 56)}
|
||||
}
|
||||
|
||||
func (a *codegen) append(v []byte) { a.buf = append(a.buf, v...) }
|
||||
func (a *codegen) code(v string) *codegen { a.append([]byte(v)); return a }
|
||||
func (a *codegen) db(v interface{}) *codegen { a.append(le(v)[:1]); return a }
|
||||
func (a *codegen) dw(v interface{}) *codegen { a.append(le(v)[:2]); return a }
|
||||
func (a *codegen) dd(v interface{}) *codegen { a.append(le(v)[:4]); return a }
|
||||
func (a *codegen) dq(v interface{}) *codegen { a.append(le(v)[:8]); return a }
|
||||
|
||||
const (
|
||||
ElfCodeAddr = 0x400000 // Where the code is loaded in memory
|
||||
ElfDataAddr = 0x800000 // Where the tape is placed in memory
|
||||
)
|
||||
|
||||
const (
|
||||
SYS_READ = 0
|
||||
SYS_WRITE = 1
|
||||
SYS_EXIT = 60
|
||||
)
|
||||
|
||||
func codegenAmd64(irb []instruction) []byte {
|
||||
offsets := make([]int, len(irb)+1)
|
||||
a := codegen{}
|
||||
|
||||
a.code("\xB8").dd(ElfDataAddr) // mov rax, "ElfCodeAddr"
|
||||
a.code("\x30\xDB") // xor bl, bl
|
||||
|
||||
for i, x := range irb {
|
||||
offsets[i] = len(a.buf)
|
||||
if x.command == LEFT || x.command == RIGHT {
|
||||
a.code("\x88\x18") // mov [rax], bl
|
||||
}
|
||||
switch x.command {
|
||||
case RIGHT: a.code("\x48\x05").dd(x.arg) // add rax, "arg"
|
||||
case LEFT: a.code("\x48\x2D").dd(x.arg) // sub rax, "arg"
|
||||
case INC: a.code("\x80\xC3").db(x.arg) // add bl, "arg"
|
||||
case DEC: a.code("\x80\xEB").db(x.arg) // sub bl, "arg"
|
||||
case OUT: a.code("\xE8").dd(0) // call "write"
|
||||
case IN: a.code("\xE8").dd(0) // call "read"
|
||||
case BEGIN:
|
||||
// test bl, bl; jz "offsets[arg]"
|
||||
a.code("\x84\xDB" + "\x0F\x84").dd(0)
|
||||
case END:
|
||||
// test bl, bl; jnz "offsets[arg]"
|
||||
a.code("\x84\xDB" + "\x0F\x85").dd(0)
|
||||
}
|
||||
if x.command == LEFT || x.command == RIGHT {
|
||||
a.code("\x8A\x18") // mov bl, [rax]
|
||||
}
|
||||
}
|
||||
// When there is a loop at the end we need to be able to jump past it
|
||||
offsets[len(irb)] = len(a.buf)
|
||||
|
||||
// Write an epilog which handles all the OS interfacing
|
||||
//
|
||||
// System V x86-64 ABI:
|
||||
// rax <-> both syscall number and return value
|
||||
// args -> rdi, rsi, rdx, r10, r8, r9
|
||||
// trashed <- rcx, r11
|
||||
|
||||
a.code("\xB8").dd(SYS_EXIT) // mov eax, 0x3c
|
||||
a.code("\x48\x31\xFF") // xor rdi, rdi
|
||||
a.code("\x0F\x05") // syscall
|
||||
|
||||
fatal := len(a.buf)
|
||||
a.code("\x48\x89\xF7") // mov rdi, rsi -- use the string in rsi
|
||||
a.code("\x30\xC0") // xor al, al -- look for the nil byte
|
||||
a.code("\x48\x31\xC9") // xor rcx, rcx
|
||||
a.code("\x48\xF7\xD1") // not rcx -- start from -1
|
||||
a.code("\xFC" + "\xF2\xAE") // cld; repne scasb -- decrement until found
|
||||
a.code("\x48\xF7\xD1") // not rcx
|
||||
a.code("\x48\x8D\x51\xFF") // lea rdx, [rcx-1] -- save length in rdx
|
||||
a.code("\xB8").dd(SYS_WRITE) // mov eax, "SYS_WRITE"
|
||||
a.code("\xBF").dd(2) // mov edi, "STDERR_FILENO"
|
||||
a.code("\x0F\x05") // syscall
|
||||
|
||||
a.code("\xB8").dd(SYS_EXIT) // mov eax, "SYS_EXIT"
|
||||
a.code("\xBF").dd(1) // mov edi, "EXIT_FAILURE"
|
||||
a.code("\x0F\x05") // syscall
|
||||
|
||||
read := len(a.buf)
|
||||
a.code("\x50") // push rax -- save tape position
|
||||
a.code("\xB8").dd(SYS_READ) // mov eax, "SYS_READ"
|
||||
a.code("\x48\x89\xC7") // mov rdi, rax -- STDIN_FILENO
|
||||
a.code("\x66\x6A\x00") // push word 0 -- the default value for EOF
|
||||
a.code("\x48\x89\xE6") // mov rsi, rsp -- the char starts at rsp
|
||||
a.code("\xBA").dd(1) // mov edx, 1 -- count
|
||||
a.code("\x0F\x05") // syscall
|
||||
a.code("\x66\x5B") // pop bx
|
||||
|
||||
a.code("\x48\x83\xF8\x00") // cmp rax, 0
|
||||
a.code("\x48\x8D\x35").dd(4) // lea rsi, [rel read_message]
|
||||
a.code("\x7C") // jl "fatal_offset" -- write failure message
|
||||
a.db(fatal - len(a.buf) - 1)
|
||||
a.code("\x58") // pop rax -- restore tape position
|
||||
a.code("\xC3") // ret
|
||||
a.code("fatal: read failed\n\x00")
|
||||
|
||||
write := len(a.buf)
|
||||
a.code("\x50") // push rax -- save tape position
|
||||
a.code("\xB8").dd(SYS_WRITE) // mov eax, "SYS_WRITE"
|
||||
a.code("\x48\x89\xC7") // mov rdi, rax -- STDOUT_FILENO
|
||||
a.code("\x66\x53") // push bx
|
||||
a.code("\x48\x89\xE6") // mov rsi, rsp -- the char starts at rsp
|
||||
a.code("\xBA").dd(1) // mov edx, 1 -- count
|
||||
a.code("\x0F\x05") // syscall
|
||||
a.code("\x66\x5B") // pop bx
|
||||
|
||||
a.code("\x48\x83\xF8\x00") // cmp rax, 0
|
||||
a.code("\x48\x8D\x35").dd(4) // lea rsi, [rel write_message]
|
||||
a.code("\x7C") // jl "fatal_offset" -- write failure message
|
||||
a.db(fatal - len(a.buf) - 1)
|
||||
a.code("\x58") // pop rax -- restore tape position
|
||||
a.code("\xC3") // ret
|
||||
a.code("fatal: write failed\n\x00")
|
||||
|
||||
// Now that we know where each instruction is, fill in relative jumps
|
||||
for i, x := range irb {
|
||||
// This must accurately reflect the code generators
|
||||
target, fixup := 0, offsets[i]
|
||||
if x.command == BEGIN || x.command == END {
|
||||
fixup += 4
|
||||
target = offsets[x.arg]
|
||||
} else if x.command == IN {
|
||||
fixup += 1
|
||||
target = read
|
||||
} else if x.command == OUT {
|
||||
fixup += 1
|
||||
target = write
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
copy(a.buf[fixup:], le(target - fixup - 4)[:4])
|
||||
}
|
||||
return a.buf
|
||||
}
|
||||
|
||||
// --- Main --------------------------------------------------------------------
|
||||
|
||||
func main() {
|
||||
var err error
|
||||
if len(os.Args) > 3 {
|
||||
log.Fatalf("usage: %s [INPUT-FILE] [OUTPUT-FILE]", os.Args[0])
|
||||
}
|
||||
|
||||
input := os.Stdin
|
||||
if len(os.Args) > 1 {
|
||||
if input, err = os.Open(os.Args[1]); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
outputPath := "a.out"
|
||||
if len(os.Args) > 2 {
|
||||
outputPath = os.Args[2]
|
||||
}
|
||||
|
||||
program, err := ioutil.ReadAll(input)
|
||||
input.Close()
|
||||
if err != nil {
|
||||
log.Fatalf("can't read program: %s", err)
|
||||
}
|
||||
|
||||
irb := decode(program)
|
||||
// ... various optimizations could be performed here if we give up brevity
|
||||
pairLoops(irb)
|
||||
dump("ir-dump.txt", irb)
|
||||
|
||||
code := codegenAmd64(irb)
|
||||
a := codegen{}
|
||||
|
||||
// TODO: also use the constants in package "debug/elf"
|
||||
|
||||
const (
|
||||
ElfHeaderSize = 64 // size of the ELF header
|
||||
ElfProgramEntrySize = 56 // size of a program header
|
||||
ElfSectionEntrySize = 64 // size of a section header
|
||||
ElfPrologSize = ElfHeaderSize + 2*ElfProgramEntrySize
|
||||
)
|
||||
|
||||
// ELF header
|
||||
a.code("\x7FELF\x02\x01\x01") // ELF, 64-bit, little endian, v1
|
||||
// Unix System V ABI, v0, padding
|
||||
a.code("\x00\x00" + "\x00\x00\x00\x00\x00\x00\x00")
|
||||
a.dw(2).dw(62).dd(1) // executable, x86-64, v1
|
||||
a.dq(ElfCodeAddr + ElfPrologSize) // entry point address
|
||||
|
||||
// We only append section headers with debugging info with DEBUG
|
||||
a.dq(ElfHeaderSize).dq(0) // program, section header offset
|
||||
a.dd(0) // no processor-specific flags
|
||||
a.dw(ElfHeaderSize) // ELF header size
|
||||
a.dw(ElfProgramEntrySize).dw(2) // program hdr tbl entry size, count
|
||||
a.dw(ElfSectionEntrySize).dw(0) // section hdr tbl entry size, count
|
||||
a.dw(0) // no section index for strings
|
||||
|
||||
// Program header for code
|
||||
// The entry point address seems to require alignment, so map start of file
|
||||
a.dd(1).dd(5) // PT_LOAD, PF_R | PF_X
|
||||
a.dq(0) // offset within the file
|
||||
a.dq(ElfCodeAddr) // address in virtual memory
|
||||
a.dq(ElfCodeAddr) // address in physical memory
|
||||
a.dq(ElfPrologSize + len(code)) // length within the file
|
||||
a.dq(ElfPrologSize + len(code)) // length within memory
|
||||
a.dq(4096) // segment alignment
|
||||
|
||||
// Program header for the tape
|
||||
a.dd(1).dd(6) // PT_LOAD, PF_R | PF_W
|
||||
a.dq(0) // offset within the file
|
||||
a.dq(ElfDataAddr) // address in virtual memory
|
||||
a.dq(ElfDataAddr) // address in physical memory
|
||||
a.dq(0) // length within the file
|
||||
a.dq(1 << 20) // one megabyte of memory
|
||||
a.dq(4096) // segment alignment
|
||||
|
||||
a.buf = append(a.buf, code...)
|
||||
if err = ioutil.WriteFile(outputPath, a.buf, 0777); err != nil {
|
||||
log.Fatalf("%s", err)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user