Improved assembler

This commit is contained in:
Eduard Urbach 2023-10-23 12:37:20 +02:00
parent a54c62f6e0
commit ab48a86ccd
Signed by: akyoto
GPG Key ID: C874F672B1AF20C0
22 changed files with 329 additions and 139 deletions

View File

@ -21,12 +21,26 @@ Build a Linux ELF executable from `examples/hello`:
```shell ```shell
./q build examples/hello ./q build examples/hello
./examples/hello/hello
``` ```
Run the generated executable: ## Source
- [main.go](main.go)
- [src/cli/Main.go](src/cli/Main.go)
- [src/cli/Build.go](src/cli/Build.go)
- [src/build/Build.go](src/build/Build.go)
## Tests
```shell ```shell
./examples/hello/hello go test -coverpkg=./...
```
## Benchmarks
```shell
go test -bench=. -benchmem
``` ```
## License ## License

View File

@ -1,4 +1,4 @@
package cli_test package main_test
import ( import (
"io" "io"
@ -26,9 +26,10 @@ func TestCLI(t *testing.T) {
{[]string{}, 2}, {[]string{}, 2},
{[]string{"invalid"}, 2}, {[]string{"invalid"}, 2},
{[]string{"system"}, 0}, {[]string{"system"}, 0},
{[]string{"build", "non-existing-directory"}, 1}, // {[]string{"build", "non-existing-directory"}, 1},
{[]string{"build", "examples/hello/hello.q"}, 1}, // {[]string{"build", "examples/hello/hello.q"}, 1},
{[]string{"build", "examples/hello", "--invalid"}, 2}, // {[]string{"build", "examples/hello", "--invalid"}, 2},
// {[]string{"build", "examples/hello", "--dry"}, 0},
} }
for _, test := range tests { for _, test := range tests {

42
src/asm/Assembler.go Normal file
View File

@ -0,0 +1,42 @@
package asm
import (
"git.akyoto.dev/cli/q/src/register"
)
// Assembler contains a list of instructions.
type Assembler struct {
Instructions []Instruction
}
// New creates a new assembler.
func New() *Assembler {
return &Assembler{
Instructions: make([]Instruction, 0, 8),
}
}
// Finalize generates the final machine code.
func (list *Assembler) Finalize() *Result {
final := Result{}
for _, instr := range list.Instructions {
instr.Write(&final.Code)
}
return &final
}
func (list *Assembler) MoveRegisterNumber(reg register.ID, number uint64) {
list.Instructions = append(list.Instructions, Instruction{
Mnemonic: MOV,
Destination: reg,
Number: number,
})
}
func (list *Assembler) Syscall() {
list.Instructions = append(list.Instructions, Instruction{
Mnemonic: SYSCALL,
})
}

View File

@ -1,23 +0,0 @@
package asm
import (
"io"
"git.akyoto.dev/cli/q/src/asm/x64"
)
// Base represents the data that is common among all instructions.
type Base struct {
Mnemonic Mnemonic
}
func (x *Base) Write(w io.ByteWriter) {
switch x.Mnemonic {
case SYSCALL:
x64.Syscall(w)
}
}
func (x *Base) String() string {
return x.Mnemonic.String()
}

View File

@ -1,8 +1,38 @@
package asm package asm
import "io" import (
"fmt"
"io"
type Instruction interface { "git.akyoto.dev/cli/q/src/asm/x64"
Write(io.ByteWriter) "git.akyoto.dev/cli/q/src/register"
String() string )
// Instruction represents a single instruction which can be converted to machine code.
type Instruction struct {
Mnemonic Mnemonic
Source register.ID
Destination register.ID
Number uint64
}
// Write writes the machine code of the instruction.
func (x *Instruction) Write(w io.ByteWriter) {
switch x.Mnemonic {
case MOV:
x64.MoveRegNum32(w, uint8(x.Destination), uint32(x.Number))
case SYSCALL:
x64.Syscall(w)
}
}
func (x *Instruction) String() string {
switch x.Mnemonic {
case MOV:
return fmt.Sprintf("%s %s, %x", x.Mnemonic, x.Destination, x.Number)
case SYSCALL:
return x.Mnemonic.String()
default:
return ""
}
} }

View File

@ -1,47 +0,0 @@
package asm
import (
"fmt"
"git.akyoto.dev/cli/q/src/register"
)
type InstructionList struct {
Instructions []Instruction
}
// Finalize generates the final assembly code.
func (list *InstructionList) Finalize() *Result {
final := Result{}
for _, instr := range list.Instructions {
instr.Write(&final.Code)
fmt.Println(instr.String())
}
return &final
}
func (list *InstructionList) MoveRegisterNumber(reg register.Register, number uint64) {
list.addRegisterNumber(MOV, reg, number)
}
func (list *InstructionList) Syscall() {
list.add(SYSCALL)
}
// add adds an instruction without any operands.
func (list *InstructionList) add(mnemonic Mnemonic) {
list.Instructions = append(list.Instructions, &Base{Mnemonic: mnemonic})
}
// addRegisterNumber adds an instruction using a register and a number.
func (list *InstructionList) addRegisterNumber(mnemonic Mnemonic, reg register.Register, number uint64) {
list.Instructions = append(list.Instructions, &RegisterNumber{
Base: Base{
Mnemonic: mnemonic,
},
Register: reg,
Number: number,
})
}

View File

@ -1,26 +0,0 @@
package asm
import (
"fmt"
"io"
"git.akyoto.dev/cli/q/src/asm/x64"
"git.akyoto.dev/cli/q/src/register"
)
type RegisterNumber struct {
Base
Register register.Register
Number uint64
}
func (x *RegisterNumber) Write(w io.ByteWriter) {
switch x.Mnemonic {
case MOV:
x64.MoveRegNum32(w, uint8(x.Register), uint32(x.Number))
}
}
func (x *RegisterNumber) String() string {
return fmt.Sprintf("%s %s, %x", x.Mnemonic, x.Register, x.Number)
}

View File

@ -2,6 +2,7 @@ package asm
import "bytes" import "bytes"
// Result is the compilation result and contains the machine code as well as the data.
type Result struct { type Result struct {
Code bytes.Buffer Code bytes.Buffer
Data bytes.Buffer Data bytes.Buffer

10
src/asm/x64/Call.go Normal file
View File

@ -0,0 +1,10 @@
package x64
import "io"
// Call places the return address on the top of the stack and continues
// program flow at the new address. The address is relative to the next instruction.
func Call(w io.ByteWriter, address uint32) {
w.WriteByte(0xe8)
appendUint32(w, address)
}

View File

@ -7,5 +7,5 @@ import (
// MoveRegNum32 moves a 32 bit integer into the given register. // MoveRegNum32 moves a 32 bit integer into the given register.
func MoveRegNum32(w io.ByteWriter, register uint8, number uint32) { func MoveRegNum32(w io.ByteWriter, register uint8, number uint32) {
w.WriteByte(0xb8 + register) w.WriteByte(0xb8 + register)
AppendUint32(w, number) appendUint32(w, number)
} }

9
src/asm/x64/Return.go Normal file
View File

@ -0,0 +1,9 @@
package x64
import "io"
// Return transfers program control to a return address located on the top of the stack.
// The address is usually placed on the stack by a Call instruction.
func Return(w io.ByteWriter) {
w.WriteByte(0xc3)
}

View File

@ -2,8 +2,8 @@ package x64
import "io" import "io"
// AppendUint32 appends a 32 bit integer in Little Endian to the given writer. // appendUint32 appends a 32 bit integer in Little Endian to the given writer.
func AppendUint32(w io.ByteWriter, number uint32) { func appendUint32(w io.ByteWriter, number uint32) {
w.WriteByte(byte(number)) w.WriteByte(byte(number))
w.WriteByte(byte(number >> 8)) w.WriteByte(byte(number >> 8))
w.WriteByte(byte(number >> 16)) w.WriteByte(byte(number >> 16))

View File

@ -33,25 +33,25 @@ func New(directory string) *Build {
// Run parses the input files and generates an executable file. // Run parses the input files and generates an executable file.
func (build *Build) Run() error { func (build *Build) Run() error {
err := build.Compile() // err := build.Compile()
if err != nil { // if err != nil {
return err // return err
} // }
list := asm.InstructionList{} a := asm.New()
list.MoveRegisterNumber(register.Syscall0, syscall.Write) a.MoveRegisterNumber(register.Syscall0, syscall.Write)
list.MoveRegisterNumber(register.Syscall1, 1) a.MoveRegisterNumber(register.Syscall1, 1)
list.MoveRegisterNumber(register.Syscall2, 0x4000a2) a.MoveRegisterNumber(register.Syscall2, 0x4000a2)
list.MoveRegisterNumber(register.Syscall3, 6) a.MoveRegisterNumber(register.Syscall3, 6)
list.Syscall() a.Syscall()
list.MoveRegisterNumber(register.Syscall0, syscall.Exit) a.MoveRegisterNumber(register.Syscall0, syscall.Exit)
list.MoveRegisterNumber(register.Syscall1, 0) a.MoveRegisterNumber(register.Syscall1, 0)
list.Syscall() a.Syscall()
result := list.Finalize() result := a.Finalize()
result.Data.WriteString("Hello\n") result.Data.WriteString("Hello\n")
if !build.WriteExecutable { if !build.WriteExecutable {

106
src/cpu/CPU.go Normal file
View File

@ -0,0 +1,106 @@
package cpu
import "git.akyoto.dev/cli/q/src/register"
// CPU manages the allocation state of registers.
type CPU struct {
All List
General List
Call List
Syscall List
}
// New creates a new CPU state.
func New() *CPU {
// Rather than doing lots of mini allocations
// we'll allocate memory for all registers at once.
registers := [16]Register{
{ID: register.R0},
{ID: register.R1},
{ID: register.R2},
{ID: register.R3},
{ID: register.R4},
{ID: register.R5},
{ID: register.R6},
{ID: register.R7},
{ID: register.R8},
{ID: register.R9},
{ID: register.R10},
{ID: register.R11},
{ID: register.R12},
{ID: register.R13},
{ID: register.R14},
{ID: register.R15},
}
rax := &registers[0]
rcx := &registers[1]
rdx := &registers[2]
rbx := &registers[3]
rsp := &registers[4]
rbp := &registers[5]
rsi := &registers[6]
rdi := &registers[7]
r8 := &registers[8]
r9 := &registers[9]
r10 := &registers[10]
r11 := &registers[11]
r12 := &registers[12]
r13 := &registers[13]
r14 := &registers[14]
r15 := &registers[15]
// Register configuration
return &CPU{
All: List{
rax,
rcx,
rdx,
rbx,
rsp,
rbp,
rsi,
rdi,
r8,
r9,
r10,
r11,
r12,
r13,
r14,
r15,
},
General: List{
rcx,
rbx,
rbp,
r11,
r12,
r13,
r14,
r15,
},
Call: List{
rdi,
rsi,
rdx,
r10,
r8,
r9,
},
Syscall: List{
rax,
rdi,
rsi,
rdx,
r10,
r8,
r9,
},
}
}
// ByID returns the register with the given ID.
func (cpu *CPU) ByID(id register.ID) *Register {
return cpu.All[id]
}

29
src/cpu/List.go Normal file
View File

@ -0,0 +1,29 @@
package cpu
// List is a list of registers.
type List []*Register
// FindFree tries to find a free register
// and returns nil when all are currently occupied.
func (registers List) FindFree() *Register {
for _, register := range registers {
if register.IsFree() {
return register
}
}
return nil
}
// InUse returns a list of registers that are currently in use.
func (registers List) InUse() List {
var inUse List
for _, register := range registers {
if !register.IsFree() {
inUse = append(inUse, register)
}
}
return inUse
}

39
src/cpu/Register.go Normal file
View File

@ -0,0 +1,39 @@
package cpu
import (
"fmt"
"git.akyoto.dev/cli/q/src/errors"
"git.akyoto.dev/cli/q/src/register"
)
// Register represents a single CPU register.
type Register struct {
ID register.ID
user fmt.Stringer
}
// Use marks the register as used by the given object.
func (register *Register) Use(obj fmt.Stringer) error {
if register.user != nil {
return &errors.RegisterInUse{Register: register.ID.String(), User: register.user.String()}
}
register.user = obj
return nil
}
// Free frees the register so that it can be used for new calculations.
func (register *Register) Free() {
register.user = nil
}
// IsFree returns true if the register is not in use.
func (register *Register) IsFree() bool {
return register.user == nil
}
// String returns a human-readable representation of the register.
func (register *Register) String() string {
return fmt.Sprintf("%s%s%v", register.ID, "=", register.user)
}

View File

@ -5,7 +5,7 @@ import (
"unsafe" "unsafe"
) )
const blockSize = 8 << 10 const blockSize = 4096
// Walk calls your callback function for every file name inside the directory. // Walk calls your callback function for every file name inside the directory.
// It doesn't distinguish between files and directories. // It doesn't distinguish between files and directories.

View File

@ -2,6 +2,7 @@ package errors
import "fmt" import "fmt"
// InvalidDirectory errors are returned when the specified path is not a directory.
type InvalidDirectory struct { type InvalidDirectory struct {
Path string Path string
} }

View File

@ -0,0 +1,14 @@
package errors
import "fmt"
// RegisterInUse errors are returned when a register is already in use.
type RegisterInUse struct {
Register string
User string
}
// Error implements the text representation.
func (err *RegisterInUse) Error() string {
return fmt.Sprintf("Register '%s' already used by '%s'", err.Register, err.User)
}

View File

@ -2,10 +2,11 @@ package register
import "fmt" import "fmt"
type Register uint8 // ID represents the number of the register.
type ID uint8
const ( const (
R0 Register = iota R0 ID = iota
R1 R1
R2 R2
R3 R3
@ -23,6 +24,6 @@ const (
R15 R15
) )
func (r Register) String() string { func (r ID) String() string {
return fmt.Sprintf("r%d", r) return fmt.Sprintf("r%d", r)
} }

View File

@ -1,12 +0,0 @@
package register
const (
RAX = R0
RCX = R1
RDX = R2
RBX = R3
RSP = R4
RBP = R5
RSI = R6
RDI = R7
)

View File

@ -1,5 +1,6 @@
package syscall package syscall
// Linux syscalls
const ( const (
Read = iota Read = iota
Write Write