Added basic support for arm64

This commit is contained in:
2025-03-06 13:40:17 +01:00
parent 14abb8202b
commit 2f09b96f34
25 changed files with 270 additions and 33 deletions

10
src/arm/Call.go Normal file
View File

@ -0,0 +1,10 @@
package arm
import "encoding/binary"
// Call branches to a PC-relative offset, setting the register X30 to PC+4.
// The offset starts from the address of this instruction and is encoded as "imm26" times 4.
// This instruction is also known as BL (branch with link).
func Call(code []byte, offset uint32) []byte {
return binary.LittleEndian.AppendUint32(code, uint32(0b100101<<26)|offset)
}

29
src/arm/Move.go Normal file
View File

@ -0,0 +1,29 @@
package arm
import (
"encoding/binary"
"git.urbach.dev/cli/q/src/cpu"
)
// MoveRegisterNumber moves an integer into the given register.
func MoveRegisterNumber(code []byte, destination cpu.Register, number int) []byte {
return MoveZero(code, destination, 0, uint16(number))
}
// MoveKeep moves a 16-bit integer into the given register and keeps all other bits.
func MoveKeep(code []byte, destination cpu.Register, halfword int, number uint16) []byte {
x := mov(0b11, halfword, number, destination)
return binary.LittleEndian.AppendUint32(code, x)
}
// MoveZero moves a 16-bit integer into the given register and clears all other bits to zero.
func MoveZero(code []byte, destination cpu.Register, halfword int, number uint16) []byte {
x := mov(0b10, halfword, number, destination)
return binary.LittleEndian.AppendUint32(code, x)
}
// mov encodes a generic move instruction.
func mov(opCode uint32, halfword int, number uint16, destination cpu.Register) uint32 {
return (1 << 31) | (opCode << 29) | (0b100101 << 23) | uint32(halfword<<21) | uint32(number<<5) | uint32(destination)
}

43
src/arm/Move_test.go Normal file
View File

@ -0,0 +1,43 @@
package arm_test
import (
"testing"
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/cli/q/src/cpu"
"git.urbach.dev/go/assert"
)
func TestMoveKeep(t *testing.T) {
usagePatterns := []struct {
Register cpu.Register
Number uint16
Code []byte
}{
{arm.X0, 0, []byte{0x00, 0x00, 0x80, 0xF2}},
{arm.X0, 1, []byte{0x20, 0x00, 0x80, 0xF2}},
}
for _, pattern := range usagePatterns {
t.Logf("movk %s, %x", pattern.Register, pattern.Number)
code := arm.MoveKeep(nil, pattern.Register, 0, pattern.Number)
assert.DeepEqual(t, code, pattern.Code)
}
}
func TestMoveZero(t *testing.T) {
usagePatterns := []struct {
Register cpu.Register
Number uint16
Code []byte
}{
{arm.X0, 0, []byte{0x00, 0x00, 0x80, 0xD2}},
{arm.X0, 1, []byte{0x20, 0x00, 0x80, 0xD2}},
}
for _, pattern := range usagePatterns {
t.Logf("movz %s, %x", pattern.Register, pattern.Number)
code := arm.MoveZero(nil, pattern.Register, 0, pattern.Number)
assert.DeepEqual(t, code, pattern.Code)
}
}

6
src/arm/Nop.go Normal file
View File

@ -0,0 +1,6 @@
package arm
// Nop does nothing. This can be used for alignment purposes.
func Nop(code []byte) []byte {
return append(code, 0x1F, 0x20, 0x03, 0xD5)
}

View File

@ -38,7 +38,20 @@ const (
)
var (
GeneralRegisters = []cpu.Register{X9, X10, X11, X12, X13, X14, X15, X16, X17, X18, X19, X20, X21, X22, X23, X24, X25, X26, X27, X28}
InputRegisters = SyscallInputRegisters
OutputRegisters = SyscallInputRegisters
SyscallInputRegisters = []cpu.Register{X8, X0, X1, X2, X3, X4, X5}
SyscallOutputRegisters = []cpu.Register{X0, X1}
WindowsInputRegisters = []cpu.Register{X0, X1, X2, X3, X4, X5, X6, X7}
WindowsOutputRegisters = []cpu.Register{X0, X1}
CPU = cpu.CPU{
General: GeneralRegisters,
Input: InputRegisters,
Output: OutputRegisters,
SyscallInput: SyscallInputRegisters,
SyscallOutput: SyscallOutputRegisters,
NumRegisters: 32,
}
)

6
src/arm/Return.go Normal file
View File

@ -0,0 +1,6 @@
package arm
// Return transfers program control to the caller.
func Return(code []byte) []byte {
return append(code, 0xC0, 0x03, 0x5F, 0xD6)
}

6
src/arm/Syscall.go Normal file
View File

@ -0,0 +1,6 @@
package arm
// Syscall is the primary way to communicate with the OS kernel.
func Syscall(code []byte) []byte {
return append(code, 0x01, 0x00, 0x00, 0xD4)
}

16
src/arm/arm_test.go Normal file
View File

@ -0,0 +1,16 @@
package arm_test
import (
"testing"
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/go/assert"
)
func TestARM(t *testing.T) {
assert.DeepEqual(t, arm.Call(nil, 0), []byte{0x00, 0x00, 0x00, 0x94})
assert.DeepEqual(t, arm.MoveRegisterNumber(nil, arm.X0, 42), arm.MoveZero(nil, arm.X0, 0, 42))
assert.DeepEqual(t, arm.Nop(nil), []byte{0x1F, 0x20, 0x03, 0xD5})
assert.DeepEqual(t, arm.Return(nil), []byte{0xC0, 0x03, 0x5F, 0xD6})
assert.DeepEqual(t, arm.Syscall(nil), []byte{0x01, 0x00, 0x00, 0xD4})
}

View File

@ -1,6 +1,7 @@
package asmc
import (
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/cli/q/src/asm"
"git.urbach.dev/cli/q/src/config"
"git.urbach.dev/cli/q/src/dll"
@ -24,8 +25,20 @@ func Finalize(a asm.Assembler, dlls dll.List) ([]byte, []byte) {
dlls: dlls,
}
for _, x := range a.Instructions {
c.compile(x)
switch config.TargetArch {
case config.ARM:
for _, x := range a.Instructions {
c.compileARM(x)
}
c.code = arm.MoveRegisterNumber(c.code, arm.X0, 0)
c.code = arm.MoveRegisterNumber(c.code, arm.X8, 0x5D)
c.code = arm.Syscall(c.code)
case config.X86:
for _, x := range a.Instructions {
c.compileX86(x)
}
}
c.resolvePointers()

25
src/asmc/compileARM.go Normal file
View File

@ -0,0 +1,25 @@
package asmc
import (
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/cli/q/src/asm"
)
func (c *compiler) compileARM(x asm.Instruction) {
switch x.Mnemonic {
// case asm.MOVE:
// switch operands := x.Data.(type) {
// case *asm.RegisterNumber:
// c.code = arm.MoveRegisterNumber(c.code, operands.Register, operands.Number)
// }
// case asm.RETURN:
// c.code = arm.Return(c.code)
// case asm.SYSCALL:
// c.code = arm.Syscall(c.code)
default:
c.code = arm.Nop(c.code)
}
}

View File

@ -5,7 +5,7 @@ import (
"git.urbach.dev/cli/q/src/x86"
)
func (c *compiler) compile(x asm.Instruction) {
func (c *compiler) compileX86(x asm.Instruction) {
switch x.Mnemonic {
case asm.ADD:
switch operands := x.Data.(type) {

View File

@ -48,7 +48,14 @@ func buildExecutable(args []string) (*build.Build, error) {
return b, &ExpectedParameterError{Parameter: "arch"}
}
config.TargetArch = args[i]
switch args[i] {
case "arm":
config.TargetArch = config.ARM
case "x86":
config.TargetArch = config.X86
default:
return b, &InvalidValueError{Value: args[i], Parameter: "arch"}
}
case "--os":
i++
@ -77,7 +84,7 @@ func buildExecutable(args []string) (*build.Build, error) {
}
}
if config.TargetOS == config.Unknown {
if config.TargetOS == config.UnknownOS {
return b, &InvalidValueError{Value: runtime.GOOS, Parameter: "os"}
}

View File

@ -14,7 +14,7 @@ func Help(w io.Writer, code int) int {
Commands:
build [directory | file] build an executable from a file or directory
--arch [arch] cross-compile for another CPU architecture [x86|arm|riscv]
--arch [arch] cross-compile for another CPU architecture [x86|arm]
--assembly, -a show assembly instructions
--dry, -d skip writing the executable to disk
--os [os] cross-compile for another OS [linux|mac|windows]

9
src/config/arch.go Normal file
View File

@ -0,0 +1,9 @@
package config
type Arch uint8
const (
UnknownArch Arch = iota
ARM
X86
)

View File

@ -3,12 +3,12 @@ package config
import "runtime"
var (
ConstantFold bool // Calculates the result of operations on constants at compile time.
Dry bool // Skips writing the executable to disk.
ShowAssembly bool // Shows assembly instructions at the end.
ShowStatistics bool // Shows statistics at the end.
TargetArch string // Target architecture.
TargetOS OS // Target platform.
ConstantFold bool // Calculates the result of operations on constants at compile time.
Dry bool // Skips writing the executable to disk.
ShowAssembly bool // Shows assembly instructions at the end.
ShowStatistics bool // Shows statistics at the end.
TargetArch Arch // Target architecture.
TargetOS OS // Target platform.
)
// Reset resets the configuration to its default values.
@ -16,7 +16,15 @@ func Reset() {
ShowAssembly = false
ShowStatistics = false
Dry = false
TargetArch = runtime.GOARCH
switch runtime.GOARCH {
case "amd64":
TargetArch = X86
case "arm":
TargetArch = ARM
default:
TargetArch = UnknownArch
}
switch runtime.GOOS {
case "linux":
@ -26,7 +34,7 @@ func Reset() {
case "windows":
TargetOS = Windows
default:
TargetOS = Unknown
TargetOS = UnknownOS
}
Optimize(true)

View File

@ -3,7 +3,7 @@ package config
type OS uint8
const (
Unknown OS = iota
UnknownOS OS = iota
Linux
Mac
Windows

View File

@ -1,7 +1,9 @@
package core
import (
"git.urbach.dev/cli/q/src/arm"
"git.urbach.dev/cli/q/src/asm"
"git.urbach.dev/cli/q/src/config"
"git.urbach.dev/cli/q/src/cpu"
"git.urbach.dev/cli/q/src/fs"
"git.urbach.dev/cli/q/src/register"
@ -11,6 +13,15 @@ import (
// NewFunction creates a new function.
func NewFunction(pkg string, name string, file *fs.File) *Function {
var cpu *cpu.CPU
switch config.TargetArch {
case config.ARM:
cpu = &arm.CPU
case config.X86:
cpu = &x86.CPU
}
return &Function{
Package: pkg,
Name: name,
@ -23,14 +34,7 @@ func NewFunction(pkg string, name string, file *fs.File) *Function {
Stack: scope.Stack{
Scopes: []*scope.Scope{{}},
},
CPU: cpu.CPU{
General: x86.GeneralRegisters,
Input: x86.InputRegisters,
Output: x86.OutputRegisters,
SyscallInput: x86.SyscallInputRegisters,
SyscallOutput: x86.SyscallOutputRegisters,
NumRegisters: 16,
},
CPU: cpu,
},
}
}

View File

@ -4,6 +4,7 @@ const (
LittleEndian = 1
TypeExecutable = 2
ArchitectureAMD64 = 0x3E
ArchitectureARM64 = 0xB7
)
type ProgramType int32

View File

@ -23,8 +23,16 @@ func Write(writer io.Writer, code []byte, data []byte) {
var (
codeStart, codePadding = fs.Align(HeaderEnd, config.Align)
dataStart, dataPadding = fs.Align(codeStart+len(code), config.Align)
arch int16
)
switch config.TargetArch {
case config.ARM:
arch = ArchitectureARM64
case config.X86:
arch = ArchitectureAMD64
}
elf := &ELF{
Header: Header{
Magic: [4]byte{0x7F, 'E', 'L', 'F'},
@ -34,7 +42,7 @@ func Write(writer io.Writer, code []byte, data []byte) {
OSABI: 0,
ABIVersion: 0,
Type: TypeExecutable,
Architecture: ArchitectureAMD64,
Architecture: arch,
FileVersion: 1,
EntryPointInMemory: int64(config.BaseAddress + codeStart),
ProgramHeaderOffset: HeaderSize,

View File

@ -9,6 +9,11 @@ const (
CPU_ARM_64 CPU = CPU_ARM | 0x01000000
)
const (
CPU_SUBTYPE_ARM64_ALL = 0
CPU_SUBTYPE_X86_64_ALL = 3
)
type Prot uint32
const (

View File

@ -28,13 +28,24 @@ func Write(writer io.Writer, code []byte, data []byte) {
var (
codeStart, codePadding = fs.Align(HeaderEnd, config.Align)
dataStart, dataPadding = fs.Align(codeStart+len(code), config.Align)
arch CPU
microArch uint32
)
switch config.TargetArch {
case config.ARM:
arch = CPU_ARM_64
microArch = CPU_SUBTYPE_ARM64_ALL | 0x80000000
case config.X86:
arch = CPU_X86_64
microArch = CPU_SUBTYPE_X86_64_ALL | 0x80000000
}
m := &MachO{
Header: Header{
Magic: 0xFEEDFACF,
Architecture: CPU_X86_64,
MicroArchitecture: 3 | 0x80000000,
Architecture: arch,
MicroArchitecture: microArch,
Type: TypeExecute,
NumCommands: 4,
SizeCommands: SizeCommands,

View File

@ -35,12 +35,20 @@ func Write(writer io.Writer, code []byte, data []byte, dlls dll.List) {
importDirectorySize = DLLImportSize * len(dllImports)
importSectionSize = len(imports)*8 + len(dllData) + importDirectorySize
imageSize, _ = fs.Align(importsStart+importSectionSize, config.Align)
arch uint16
)
if dlls.Contains("user32") {
subSystem = IMAGE_SUBSYSTEM_WINDOWS_GUI
}
switch config.TargetArch {
case config.ARM:
arch = IMAGE_FILE_MACHINE_ARM64
case config.X86:
arch = IMAGE_FILE_MACHINE_AMD64
}
pe := &EXE{
DOSHeader: DOSHeader{
Magic: [4]byte{'M', 'Z', 0, 0},
@ -48,7 +56,7 @@ func Write(writer io.Writer, code []byte, data []byte, dlls dll.List) {
},
NTHeader: NTHeader{
Signature: [4]byte{'P', 'E', 0, 0},
Machine: IMAGE_FILE_MACHINE_AMD64,
Machine: arch,
NumberOfSections: uint16(NumSections),
TimeDateStamp: 0,
PointerToSymbolTable: 0,

View File

@ -10,6 +10,6 @@ import (
type Machine struct {
scope.Stack
Assembler asm.Assembler
CPU cpu.CPU
CPU *cpu.CPU
RegisterHistory []uint64
}

View File

@ -5,14 +5,14 @@ import "git.urbach.dev/cli/q/src/cpu"
// 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(code []byte, address uint32) []byte {
func Call(code []byte, offset uint32) []byte {
return append(
code,
0xE8,
byte(address),
byte(address>>8),
byte(address>>16),
byte(address>>24),
byte(offset),
byte(offset>>8),
byte(offset>>16),
byte(offset>>24),
)
}

View File

@ -30,4 +30,13 @@ var (
WindowsInputRegisters = []cpu.Register{RCX, RDX, R8, R9}
WindowsOutputRegisters = []cpu.Register{RAX}
WindowsVolatileRegisters = []cpu.Register{RCX, RDX, R8, R9, R10, R11}
CPU = cpu.CPU{
General: GeneralRegisters,
Input: InputRegisters,
Output: OutputRegisters,
SyscallInput: SyscallInputRegisters,
SyscallOutput: SyscallOutputRegisters,
NumRegisters: 16,
}
)