diff --git a/go.mod b/go.mod index e78e48b..2c2b156 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module git.akyoto.dev/cli/q go 1.21 - -require git.akyoto.dev/go/assert v0.1.3 diff --git a/go.sum b/go.sum deleted file mode 100644 index 9fc2547..0000000 --- a/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -git.akyoto.dev/go/assert v0.1.3 h1:QwCUbmG4aZYsNk/OuRBz1zWVKmGlDUHhOnnDBfn8Qw8= -git.akyoto.dev/go/assert v0.1.3/go.mod h1:0GzMaM0eURuDwtGkJJkCsI7r2aUKr+5GmWNTFPgDocM= diff --git a/main_test.go b/main_test.go index cc45c58..4aac58f 100644 --- a/main_test.go +++ b/main_test.go @@ -7,7 +7,6 @@ import ( "git.akyoto.dev/cli/q/src/cli" "git.akyoto.dev/cli/q/src/log" - "git.akyoto.dev/go/assert" ) func TestMain(m *testing.M) { @@ -28,14 +27,18 @@ func TestCLI(t *testing.T) { {[]string{"system"}, 0}, {[]string{"build", "non-existing-directory"}, 1}, {[]string{"build", "examples/hello/hello.q"}, 1}, - {[]string{"build", "examples/hello", "--dry"}, 0}, {[]string{"build", "examples/hello", "--invalid"}, 2}, + {[]string{"build", "examples/hello", "--dry"}, 0}, } for _, test := range tests { - exitCode := cli.Main(test.arguments) t.Log(test.arguments) - assert.Equal(t, exitCode, test.expectedExitCode) + exitCode := cli.Main(test.arguments) + + if exitCode != test.expectedExitCode { + t.Errorf("exit code %d (expected %d)", exitCode, test.expectedExitCode) + t.FailNow() + } } } diff --git a/src/asm/Address.go b/src/asm/Address.go index 9c1651d..8e32c5a 100644 --- a/src/asm/Address.go +++ b/src/asm/Address.go @@ -4,4 +4,4 @@ package asm type Address = uint32 // Index references an instruction by its position. -type Index = uint32 +// type Index = uint32 diff --git a/src/asm/Assembler.go b/src/asm/Assembler.go index a92fc62..552ff91 100644 --- a/src/asm/Assembler.go +++ b/src/asm/Assembler.go @@ -1,74 +1,76 @@ package asm import ( - "bytes" "encoding/binary" - "git.akyoto.dev/cli/q/src/asm/x64" "git.akyoto.dev/cli/q/src/config" "git.akyoto.dev/cli/q/src/log" "git.akyoto.dev/cli/q/src/register" + "git.akyoto.dev/cli/q/src/x64" ) // Assembler contains a list of instructions. type Assembler struct { - instructions []Instruction - labels map[string]Index - verbose bool + Instructions []Instruction + // labels map[string]Index + Verbose bool } // New creates a new assembler. func New() *Assembler { return &Assembler{ - instructions: make([]Instruction, 0, 8), - labels: map[string]Index{}, - verbose: true, + Instructions: make([]Instruction, 0, 8), + // labels: map[string]Index{}, } } // Finalize generates the final machine code. -func (a *Assembler) Finalize() *Result { - result := &Result{} - pointers := map[Address]Address{} - code := bytes.NewBuffer(result.Code) - - for _, x := range a.instructions { - if a.verbose { - log.Info.Println(x.String()) - } +func (a *Assembler) Finalize() ([]byte, []byte) { + code := make([]byte, 0, len(a.Instructions)*8) + data := make(Data, 0, 16) + pointers := []Pointer{} + for _, x := range a.Instructions { switch x.Mnemonic { case MOV: - x64.MoveRegNum32(code, uint8(x.Destination), uint32(x.Number)) + code = x64.MoveRegNum32(code, uint8(x.Destination), uint32(x.Number)) case MOVDATA: - position := result.AddData(x.Data) - x64.MoveRegNum32(code, uint8(x.Destination), 0) - pointers[Address(code.Len()-4)] = position + code = x64.MoveRegNum32(code, uint8(x.Destination), 0) + + pointers = append(pointers, Pointer{ + Position: Address(len(code) - 4), + Address: data.Add(x.Data), + }) case SYSCALL: - x64.Syscall(code) + code = x64.Syscall(code) + } + + if a.Verbose { + log.Info.Println(x.String()) } } - result.Code = code.Bytes() - dataStart := config.BaseAddress + config.CodeOffset + Address(len(result.Code)) + dataStart := config.BaseAddress + config.CodeOffset + Address(len(code)) - for codePos, dataPos := range pointers { - binary.LittleEndian.PutUint32(result.Code[codePos:codePos+4], dataStart+dataPos) + for _, pointer := range pointers { + slice := code[pointer.Position : pointer.Position+4] + address := dataStart + pointer.Address + binary.LittleEndian.PutUint32(slice, address) } - return result + return code, data } // AddLabel creates a new label at the current position. -func (a *Assembler) AddLabel(name string) { - a.labels[name] = Index(len(a.instructions)) -} +// func (a *Assembler) AddLabel(name string) { +// a.labels[name] = Index(len(a.instructions)) +// } // MoveRegisterData moves a data section address into the given register. func (a *Assembler) MoveRegisterData(reg register.ID, data []byte) { - a.instructions = append(a.instructions, Instruction{ + a.Instructions = append(a.Instructions, Instruction{ Mnemonic: MOVDATA, Destination: reg, Data: data, @@ -77,7 +79,7 @@ func (a *Assembler) MoveRegisterData(reg register.ID, data []byte) { // MoveRegisterNumber moves a number into the given register. func (a *Assembler) MoveRegisterNumber(reg register.ID, number uint64) { - a.instructions = append(a.instructions, Instruction{ + a.Instructions = append(a.Instructions, Instruction{ Mnemonic: MOV, Destination: reg, Number: number, @@ -86,7 +88,7 @@ func (a *Assembler) MoveRegisterNumber(reg register.ID, number uint64) { // Syscall executes a kernel function. func (a *Assembler) Syscall() { - a.instructions = append(a.instructions, Instruction{ + a.Instructions = append(a.Instructions, Instruction{ Mnemonic: SYSCALL, }) } diff --git a/src/asm/Data.go b/src/asm/Data.go new file mode 100644 index 0000000..9268c99 --- /dev/null +++ b/src/asm/Data.go @@ -0,0 +1,19 @@ +package asm + +import "bytes" + +// Data represents the static read-only data. +type Data []byte + +// Add adds the given bytes to the data block and returns the address relative to the start of the data section. +func (data *Data) Add(block []byte) Address { + position := bytes.Index(*data, block) + + if position != -1 { + return Address(position) + } + + address := Address(len(*data)) + *data = append(*data, block...) + return address +} diff --git a/src/asm/Pointer.go b/src/asm/Pointer.go new file mode 100644 index 0000000..5d13b4f --- /dev/null +++ b/src/asm/Pointer.go @@ -0,0 +1,9 @@ +package asm + +// Pointer stores a relative memory address that we can later turn into an absolute one. +// Position: The machine code offset where the address was inserted. +// Address: The offset inside the section. +type Pointer struct { + Position uint32 + Address uint32 +} diff --git a/src/asm/Result.go b/src/asm/Result.go deleted file mode 100644 index 8261d82..0000000 --- a/src/asm/Result.go +++ /dev/null @@ -1,22 +0,0 @@ -package asm - -import "bytes" - -// Result is the compilation result and contains the machine code as well as the data. -type Result struct { - Code []byte - Data []byte -} - -// AddData adds the given bytes to the data block and returns the address relative to the start of the data section. -func (result *Result) AddData(block []byte) Address { - position := bytes.Index(result.Data, block) - - if position != -1 { - return Address(position) - } - - address := Address(len(result.Data)) - result.Data = append(result.Data, block...) - return address -} diff --git a/src/asm/x64/Move.go b/src/asm/x64/Move.go deleted file mode 100644 index d130195..0000000 --- a/src/asm/x64/Move.go +++ /dev/null @@ -1,11 +0,0 @@ -package x64 - -import ( - "io" -) - -// MoveRegNum32 moves a 32 bit integer into the given register. -func MoveRegNum32(w io.ByteWriter, register uint8, number uint32) { - w.WriteByte(0xb8 + register) - appendUint32(w, number) -} diff --git a/src/asm/x64/Syscall.go b/src/asm/x64/Syscall.go deleted file mode 100644 index 145f5a3..0000000 --- a/src/asm/x64/Syscall.go +++ /dev/null @@ -1,11 +0,0 @@ -package x64 - -import ( - "io" -) - -// Syscall is the primary way to communicate with the OS kernel. -func Syscall(w io.ByteWriter) { - w.WriteByte(0x0f) - w.WriteByte(0x05) -} diff --git a/src/asm/x64/x64.go b/src/asm/x64/x64.go deleted file mode 100644 index f6ab914..0000000 --- a/src/asm/x64/x64.go +++ /dev/null @@ -1,11 +0,0 @@ -package x64 - -import "io" - -// appendUint32 appends a 32 bit integer in Little Endian to the given writer. -func appendUint32(w io.ByteWriter, number uint32) { - w.WriteByte(byte(number)) - w.WriteByte(byte(number >> 8)) - w.WriteByte(byte(number >> 16)) - w.WriteByte(byte(number >> 24)) -} diff --git a/src/build/Build.go b/src/build/Build.go index 197665e..80d16c5 100644 --- a/src/build/Build.go +++ b/src/build/Build.go @@ -10,27 +10,31 @@ import ( "git.akyoto.dev/cli/q/src/directory" "git.akyoto.dev/cli/q/src/elf" "git.akyoto.dev/cli/q/src/errors" + "git.akyoto.dev/cli/q/src/linux" "git.akyoto.dev/cli/q/src/log" "git.akyoto.dev/cli/q/src/register" - "git.akyoto.dev/cli/q/src/syscall" ) // Build describes a compiler build. type Build struct { - Name string Directory string + Verbose bool WriteExecutable bool } // New creates a new build. func New(directory string) *Build { return &Build{ - Name: filepath.Base(directory), Directory: directory, WriteExecutable: true, } } +var ( + hello = []byte("Hello\n") + world = []byte("World\n") +) + // Run parses the input files and generates an executable file. func (build *Build) Run() error { // err := build.Compile() @@ -39,33 +43,34 @@ func (build *Build) Run() error { // return err // } - a := asm.New() + a := asm.Assembler{ + Instructions: make([]asm.Instruction, 0, 8), + Verbose: build.Verbose, + } - a.MoveRegisterNumber(register.Syscall0, syscall.Write) + a.MoveRegisterNumber(register.Syscall0, linux.Write) a.MoveRegisterNumber(register.Syscall1, 1) - hello := []byte("Hello\n") a.MoveRegisterData(register.Syscall2, hello) a.MoveRegisterNumber(register.Syscall3, uint64(len(hello))) a.Syscall() - a.MoveRegisterNumber(register.Syscall0, syscall.Write) + a.MoveRegisterNumber(register.Syscall0, linux.Write) a.MoveRegisterNumber(register.Syscall1, 1) - world := []byte("World\n") a.MoveRegisterData(register.Syscall2, world) a.MoveRegisterNumber(register.Syscall3, uint64(len(world))) a.Syscall() - a.MoveRegisterNumber(register.Syscall0, syscall.Exit) + a.MoveRegisterNumber(register.Syscall0, linux.Exit) a.MoveRegisterNumber(register.Syscall1, 0) a.Syscall() - result := a.Finalize() + code, data := a.Finalize() if !build.WriteExecutable { return nil } - return writeToDisk(build.Executable(), result.Code, result.Data) + return writeToDisk(build.Executable(), code, data) } // Compile compiles all the functions. @@ -93,7 +98,7 @@ func (build *Build) Compile() error { // Executable returns the path to the executable. func (build *Build) Executable() string { - return filepath.Join(build.Directory, build.Name) + return filepath.Join(build.Directory, filepath.Base(build.Directory)) } // writeToDisk writes the executable file to disk. diff --git a/src/cli/Build.go b/src/cli/Build.go index 47e76db..761d418 100644 --- a/src/cli/Build.go +++ b/src/cli/Build.go @@ -20,6 +20,9 @@ func Build(args []string) int { case "--dry": b.WriteExecutable = false + case "--verbose", "-v": + b.Verbose = true + default: log.Error.Printf("Unknown parameter: %s\n", args[i]) return 2 diff --git a/src/syscall/syscall_linux.go b/src/linux/syscall_linux.go similarity index 99% rename from src/syscall/syscall_linux.go rename to src/linux/syscall_linux.go index 7c5d6f8..c535786 100644 --- a/src/syscall/syscall_linux.go +++ b/src/linux/syscall_linux.go @@ -1,6 +1,5 @@ -package syscall +package linux -// Linux syscalls const ( Read = iota Write diff --git a/src/asm/x64/Call.go b/src/x64/Call.go similarity index 51% rename from src/asm/x64/Call.go rename to src/x64/Call.go index d3958bf..1ada6fd 100644 --- a/src/asm/x64/Call.go +++ b/src/x64/Call.go @@ -1,10 +1,14 @@ 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) +func Call(code []byte, address uint32) []byte { + return append( + code, + 0xe8, + byte(address), + byte(address>>8), + byte(address>>16), + byte(address>>24), + ) } diff --git a/src/x64/Move.go b/src/x64/Move.go new file mode 100644 index 0000000..6d9fd57 --- /dev/null +++ b/src/x64/Move.go @@ -0,0 +1,13 @@ +package x64 + +// MoveRegNum32 moves a 32 bit integer into the given register. +func MoveRegNum32(code []byte, register uint8, number uint32) []byte { + return append( + code, + 0xb8+register, + byte(number), + byte(number>>8), + byte(number>>16), + byte(number>>24), + ) +} diff --git a/src/asm/x64/Return.go b/src/x64/Return.go similarity index 73% rename from src/asm/x64/Return.go rename to src/x64/Return.go index 47a5b34..dc185da 100644 --- a/src/asm/x64/Return.go +++ b/src/x64/Return.go @@ -1,9 +1,7 @@ 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) +func Return(code []byte) []byte { + return append(code, 0xc3) } diff --git a/src/x64/Syscall.go b/src/x64/Syscall.go new file mode 100644 index 0000000..94b07d2 --- /dev/null +++ b/src/x64/Syscall.go @@ -0,0 +1,6 @@ +package x64 + +// Syscall is the primary way to communicate with the OS kernel. +func Syscall(code []byte) []byte { + return append(code, 0x0f, 0x05) +}