Improved assembler
This commit is contained in:
parent
a54c62f6e0
commit
ab48a86ccd
18
README.md
18
README.md
@ -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
|
||||||
|
@ -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
42
src/asm/Assembler.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -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 ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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
10
src/asm/x64/Call.go
Normal 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)
|
||||||
|
}
|
@ -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
9
src/asm/x64/Return.go
Normal 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)
|
||||||
|
}
|
@ -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))
|
@ -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
106
src/cpu/CPU.go
Normal 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 := ®isters[0]
|
||||||
|
rcx := ®isters[1]
|
||||||
|
rdx := ®isters[2]
|
||||||
|
rbx := ®isters[3]
|
||||||
|
rsp := ®isters[4]
|
||||||
|
rbp := ®isters[5]
|
||||||
|
rsi := ®isters[6]
|
||||||
|
rdi := ®isters[7]
|
||||||
|
r8 := ®isters[8]
|
||||||
|
r9 := ®isters[9]
|
||||||
|
r10 := ®isters[10]
|
||||||
|
r11 := ®isters[11]
|
||||||
|
r12 := ®isters[12]
|
||||||
|
r13 := ®isters[13]
|
||||||
|
r14 := ®isters[14]
|
||||||
|
r15 := ®isters[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
29
src/cpu/List.go
Normal 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
39
src/cpu/Register.go
Normal 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)
|
||||||
|
}
|
@ -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.
|
||||||
|
@ -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
|
||||||
}
|
}
|
14
src/errors/RegisterInUse.go
Normal file
14
src/errors/RegisterInUse.go
Normal 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)
|
||||||
|
}
|
@ -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)
|
||||||
}
|
}
|
@ -1,12 +0,0 @@
|
|||||||
package register
|
|
||||||
|
|
||||||
const (
|
|
||||||
RAX = R0
|
|
||||||
RCX = R1
|
|
||||||
RDX = R2
|
|
||||||
RBX = R3
|
|
||||||
RSP = R4
|
|
||||||
RBP = R5
|
|
||||||
RSI = R6
|
|
||||||
RDI = R7
|
|
||||||
)
|
|
@ -1,5 +1,6 @@
|
|||||||
package syscall
|
package syscall
|
||||||
|
|
||||||
|
// Linux syscalls
|
||||||
const (
|
const (
|
||||||
Read = iota
|
Read = iota
|
||||||
Write
|
Write
|
||||||
|
Loading…
Reference in New Issue
Block a user