From 12e0ccf7b2dc4eb5e5656ae590ac2a08f7a8ad79 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Tue, 1 Jul 2025 13:37:40 +0200 Subject: [PATCH] Added Windows support --- docs/pe.md | 27 ++++++ lib/core/core_windows.q | 29 +++--- lib/io/write_windows.q | 7 ++ src/asm/Assembler.go | 19 +++- src/asm/Assembler_test.go | 4 +- src/asm/Instruction.go | 18 ++++ src/asm/compiler.go | 14 +-- src/asm/compilerX86.go | 28 ++++++ src/core/Compile.go | 17 +++- src/core/Evaluate.go | 7 +- src/core/Function.go | 6 +- src/cpu/CPU.go | 5 +- src/dll/Library.go | 6 ++ src/dll/List.go | 55 ++++++++++++ src/dll/List_test.go | 34 +++++++ src/linker/WriteFile.go | 5 +- src/pe/Arch.go | 23 +++++ src/pe/Characteristics.go | 21 +++++ src/pe/Constants.go | 7 ++ src/pe/DLLImport.go | 11 +++ src/pe/DOSHeader.go | 11 +++ src/pe/DataDirectory.go | 6 ++ src/pe/DllCharacteristics.go | 17 ++++ src/pe/EXE.go | 147 +++++++++++++++++++++++++++++++ src/pe/EXE_test.go | 15 ++++ src/pe/NTHeader.go | 14 +++ src/pe/OptionalHeader64.go | 36 ++++++++ src/pe/SectionCharacteristics.go | 14 +++ src/pe/SectionHeader.go | 16 ++++ src/pe/Subsystem.go | 20 +++++ src/pe/importLibraries.go | 60 +++++++++++++ src/scanner/scanExtern.go | 68 ++++++++++++++ src/scanner/scanFile.go | 2 + src/scanner/scanFunction.go | 2 +- src/scanner/scanSignature.go | 4 +- src/x86/Registers.go | 5 +- 36 files changed, 738 insertions(+), 42 deletions(-) create mode 100644 docs/pe.md create mode 100644 src/dll/Library.go create mode 100644 src/dll/List.go create mode 100644 src/dll/List_test.go create mode 100644 src/pe/Arch.go create mode 100644 src/pe/Characteristics.go create mode 100644 src/pe/Constants.go create mode 100644 src/pe/DLLImport.go create mode 100644 src/pe/DOSHeader.go create mode 100644 src/pe/DataDirectory.go create mode 100644 src/pe/DllCharacteristics.go create mode 100644 src/pe/EXE.go create mode 100644 src/pe/EXE_test.go create mode 100644 src/pe/NTHeader.go create mode 100644 src/pe/OptionalHeader64.go create mode 100644 src/pe/SectionCharacteristics.go create mode 100644 src/pe/SectionHeader.go create mode 100644 src/pe/Subsystem.go create mode 100644 src/pe/importLibraries.go create mode 100644 src/scanner/scanExtern.go diff --git a/docs/pe.md b/docs/pe.md new file mode 100644 index 0000000..7d51d75 --- /dev/null +++ b/docs/pe.md @@ -0,0 +1,27 @@ +# Portable Executable + +## Notes + +Unlike Linux, Windows does not ignore zero-length sections at the end of a file and will +fail loading them because they don't exist within the file. Adding a single byte to the +section can fix this problem, but it's easier to just remove the section header entirely. +The solution used here is to guarantee that the data section is never empty by always +importing a few core functions from "kernel32.dll". + +## DLL function pointers + +The section where the DLL function pointers are stored does not need to be marked as writable. +The Windows executable loader resolves the pointers before they are loaded into memory. + +The stack must be 16 byte aligned before a DLL function is called. + +## Links + +- https://learn.microsoft.com/en-us/windows/win32/debug/pe-format +- https://learn.microsoft.com/en-us/previous-versions/ms809762(v=msdn.10) +- https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/february/inside-windows-win32-portable-executable-file-format-in-detail +- https://learn.microsoft.com/en-us/archive/msdn-magazine/2002/march/inside-windows-an-in-depth-look-into-the-win32-portable-executable-file-format-part-2 +- https://blog.kowalczyk.info/articles/pefileformat.html +- https://keyj.emphy.de/win32-pe/ +- https://corkamiwiki.github.io/PE +- https://github.com/ayaka14732/TinyPE-on-Win10 \ No newline at end of file diff --git a/lib/core/core_windows.q b/lib/core/core_windows.q index e0f564d..db8093b 100644 --- a/lib/core/core_windows.q +++ b/lib/core/core_windows.q @@ -1,28 +1,23 @@ init() { - // kernel32.SetConsoleCP(cp.utf8) - // kernel32.SetConsoleOutputCP(cp.utf8) + utf8 := 65001 + kernel32.SetConsoleCP(utf8) + kernel32.SetConsoleOutputCP(utf8) main.main() exit() } exit() { - // kernel32.ExitProcess(0) + kernel32.ExitProcess(0) } crash() { - // kernel32.ExitProcess(1) + kernel32.ExitProcess(1) } -// const { -// cp { -// utf8 65001 -// } -// } - -// extern { -// kernel32 { -// SetConsoleCP(cp uint) -// SetConsoleOutputCP(cp uint) -// ExitProcess(code uint) -// } -// } \ No newline at end of file +extern { + kernel32 { + SetConsoleCP(cp uint) + SetConsoleOutputCP(cp uint) + ExitProcess(code uint) + } +} \ No newline at end of file diff --git a/lib/io/write_windows.q b/lib/io/write_windows.q index ef06d49..2342b94 100644 --- a/lib/io/write_windows.q +++ b/lib/io/write_windows.q @@ -1,3 +1,10 @@ write(_ []byte) -> (written int) { return 0 +} + +extern { + kernel32 { + GetStdHandle(handle int64) -> int64 + WriteConsoleA(fd int64, buffer *byte, length uint32, written *uint32) -> bool + } } \ No newline at end of file diff --git a/src/asm/Assembler.go b/src/asm/Assembler.go index eb4d53e..7c24823 100644 --- a/src/asm/Assembler.go +++ b/src/asm/Assembler.go @@ -5,6 +5,7 @@ import ( "git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/data" + "git.urbach.dev/cli/q/src/dll" "git.urbach.dev/cli/q/src/elf" "git.urbach.dev/cli/q/src/exe" ) @@ -13,6 +14,7 @@ import ( type Assembler struct { Data data.Data Instructions []Instruction + Libraries dll.List } // Append adds another instruction. @@ -30,7 +32,7 @@ func (a *Assembler) Last() Instruction { } // Compile compiles the instructions to machine code. -func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) { +func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte, libs dll.List) { data, dataLabels := a.Data.Finalize() c := compiler{ @@ -38,6 +40,7 @@ func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) { data: data, dataLabels: dataLabels, labels: make(map[string]Address, 32), + libraries: a.Libraries, } switch b.Arch { @@ -57,18 +60,22 @@ func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) { } x := exe.New(elf.HeaderEnd, b.FileAlign, b.MemoryAlign) - x.InitSections(c.code, c.data) + x.InitSections(c.code, c.data, nil) dataSectionOffset := x.Sections[1].MemoryOffset - x.Sections[0].MemoryOffset for dataLabel, address := range dataLabels { c.labels[dataLabel] = dataSectionOffset + address } + if b.OS == build.Windows { + c.importsStart = x.Sections[2].MemoryOffset - x.Sections[0].MemoryOffset + } + for _, call := range c.deferred { call() } - return c.code, c.data + return c.code, c.data, c.libraries } // Merge combines the contents of this assembler with another one. @@ -81,6 +88,12 @@ func (a *Assembler) Merge(b *Assembler) { a.Instructions = append(a.Instructions, b.Instructions[skip:]...) maps.Copy(a.Data, b.Data) + + for _, library := range b.Libraries { + for _, fn := range library.Functions { + a.Libraries = a.Libraries.Append(library.Name, fn) + } + } } // SetData sets the data for the given label. diff --git a/src/asm/Assembler_test.go b/src/asm/Assembler_test.go index 3a3383d..730a24a 100644 --- a/src/asm/Assembler_test.go +++ b/src/asm/Assembler_test.go @@ -37,9 +37,9 @@ func TestAssembler(t *testing.T) { final.Merge(b) final.Merge(c) - code, _ := final.Compile(&build.Build{Arch: build.ARM}) + code, _, _ := final.Compile(&build.Build{Arch: build.ARM}) assert.NotNil(t, code) - code, _ = final.Compile(&build.Build{Arch: build.X86}) + code, _, _ = final.Compile(&build.Build{Arch: build.X86}) assert.NotNil(t, code) } \ No newline at end of file diff --git a/src/asm/Instruction.go b/src/asm/Instruction.go index 31446ba..5990c20 100644 --- a/src/asm/Instruction.go +++ b/src/asm/Instruction.go @@ -4,10 +4,21 @@ import "git.urbach.dev/cli/q/src/cpu" type Instruction interface{} +type AndRegisterNumber struct { + Destination cpu.Register + Source cpu.Register + Number int +} + type Call struct { Label string } +type CallExtern struct { + Library string + Function string +} + type FunctionStart struct{} type FunctionEnd struct{} @@ -35,4 +46,11 @@ type MoveRegisterRegister struct { } type Return struct{} + +type SubRegisterNumber struct { + Destination cpu.Register + Source cpu.Register + Number int +} + type Syscall struct{} \ No newline at end of file diff --git a/src/asm/compiler.go b/src/asm/compiler.go index 8e0ef18..3950988 100644 --- a/src/asm/compiler.go +++ b/src/asm/compiler.go @@ -1,11 +1,15 @@ package asm +import "git.urbach.dev/cli/q/src/dll" + type compiler struct { - code []byte - data []byte - dataLabels map[string]Address - labels map[string]Address - deferred []func() + code []byte + data []byte + dataLabels map[string]Address + labels map[string]Address + libraries dll.List + importsStart int + deferred []func() } func (c *compiler) Defer(call func()) { diff --git a/src/asm/compilerX86.go b/src/asm/compilerX86.go index 57fc56c..aee625e 100644 --- a/src/asm/compilerX86.go +++ b/src/asm/compilerX86.go @@ -2,6 +2,7 @@ package asm import ( "encoding/binary" + "fmt" "git.urbach.dev/cli/q/src/sizeof" "git.urbach.dev/cli/q/src/x86" @@ -13,6 +14,18 @@ type compilerX86 struct { func (c *compilerX86) Compile(instr Instruction) { switch instr := instr.(type) { + case *AndRegisterNumber: + if instr.Destination != instr.Source { + c.code = x86.MoveRegisterRegister(c.code, instr.Destination, instr.Source) + } + + c.code = x86.AndRegisterNumber(c.code, instr.Destination, instr.Number) + case *SubRegisterNumber: + if instr.Destination != instr.Source { + c.code = x86.MoveRegisterRegister(c.code, instr.Destination, instr.Source) + } + + c.code = x86.SubRegisterNumber(c.code, instr.Destination, instr.Number) case *Call: c.code = x86.Call(c.code, 0) end := len(c.code) @@ -24,6 +37,21 @@ func (c *compilerX86) Compile(instr Instruction) { panic("unknown label: " + instr.Label) } + offset := address - end + binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset)) + }) + case *CallExtern: + c.code = x86.CallAt(c.code, 0) + end := len(c.code) + + c.Defer(func() { + index := c.libraries.Index(instr.Library, instr.Function) + + if index == -1 { + panic(fmt.Sprintf("unknown extern function '%s' in library '%s'", instr.Function, instr.Library)) + } + + address := c.importsStart + index*8 offset := address - end binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset)) }) diff --git a/src/core/Compile.go b/src/core/Compile.go index 23e6838..1910faf 100644 --- a/src/core/Compile.go +++ b/src/core/Compile.go @@ -7,6 +7,7 @@ import ( "git.urbach.dev/cli/q/src/cpu" "git.urbach.dev/cli/q/src/ssa" "git.urbach.dev/cli/q/src/types" + "git.urbach.dev/cli/q/src/x86" ) // Compile turns a function into machine code. @@ -72,11 +73,19 @@ func (f *Function) Compile() { for instr := range f.Values { switch instr := instr.(type) { case *ssa.Call: - f.mv(instr.Arguments[1:], f.CPU.Call) + arg := instr.Arguments[0].(*ssa.Function) + fn := f.All.Functions[arg.UniqueName] - switch arg := instr.Arguments[0].(type) { - case *ssa.Function: - f.Assembler.Instructions = append(f.Assembler.Instructions, &asm.Call{Label: arg.UniqueName}) + if fn.IsExtern() { + f.mv(instr.Arguments[1:], f.CPU.ExternCall) + f.Assembler.Append(&asm.MoveRegisterRegister{Destination: x86.R5, Source: x86.SP}) + f.Assembler.Append(&asm.AndRegisterNumber{Destination: x86.SP, Source: x86.SP, Number: -16}) + f.Assembler.Append(&asm.SubRegisterNumber{Destination: x86.SP, Source: x86.SP, Number: 32}) + f.Assembler.Append(&asm.CallExtern{Library: fn.Package, Function: fn.Name}) + f.Assembler.Append(&asm.MoveRegisterRegister{Destination: x86.SP, Source: x86.R5}) + } else { + f.mv(instr.Arguments[1:], f.CPU.Call) + f.Assembler.Append(&asm.Call{Label: fn.UniqueName}) } case *ssa.Syscall: diff --git a/src/core/Evaluate.go b/src/core/Evaluate.go index 47dca6c..3a56c46 100644 --- a/src/core/Evaluate.go +++ b/src/core/Evaluate.go @@ -137,7 +137,12 @@ func (f *Function) Evaluate(expr *expression.Expression) (ssa.Value, error) { return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position) } - f.Dependencies.Add(function) + if function.IsExtern() { + f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name) + } else { + f.Dependencies.Add(function) + } + v := f.AppendFunction(function.UniqueName, function.Type) v.Source = ssa.Source(expr.Source) return v, nil diff --git a/src/core/Function.go b/src/core/Function.go index 07555f2..4c25ff5 100644 --- a/src/core/Function.go +++ b/src/core/Function.go @@ -16,6 +16,7 @@ import ( type Function struct { ssa.IR Name string + Package string UniqueName string File *fs.File Input []*ssa.Parameter @@ -31,11 +32,12 @@ type Function struct { } // NewFunction creates a new function. -func NewFunction(name string, file *fs.File) *Function { +func NewFunction(name string, pkg string, file *fs.File) *Function { return &Function{ Name: name, + Package: pkg, + UniqueName: fmt.Sprintf("%s.%s", pkg, name), File: file, - UniqueName: fmt.Sprintf("%s.%s", file.Package, name), Identifiers: make(map[string]ssa.Value, 8), IR: ssa.IR{ Blocks: []*ssa.Block{ diff --git a/src/cpu/CPU.go b/src/cpu/CPU.go index d13220d..30f81dc 100644 --- a/src/cpu/CPU.go +++ b/src/cpu/CPU.go @@ -2,6 +2,7 @@ package cpu // CPU represents the processor. type CPU struct { - Call []Register - Syscall []Register + Call []Register + Syscall []Register + ExternCall []Register } \ No newline at end of file diff --git a/src/dll/Library.go b/src/dll/Library.go new file mode 100644 index 0000000..931a0c4 --- /dev/null +++ b/src/dll/Library.go @@ -0,0 +1,6 @@ +package dll + +type Library struct { + Name string + Functions []string +} \ No newline at end of file diff --git a/src/dll/List.go b/src/dll/List.go new file mode 100644 index 0000000..ea61753 --- /dev/null +++ b/src/dll/List.go @@ -0,0 +1,55 @@ +package dll + +import "slices" + +// List is a slice of DLLs. +type List []Library + +// Append adds a function for the given DLL if it doesn't exist yet. +func (list List) Append(dllName string, funcName string) List { + for i, dll := range list { + if dll.Name != dllName { + continue + } + + if slices.Contains(dll.Functions, funcName) { + return list + } + + list[i].Functions = append(list[i].Functions, funcName) + return list + } + + return append(list, Library{Name: dllName, Functions: []string{funcName}}) +} + +// Contains returns true if the library exists. +func (list List) Contains(dllName string) bool { + for _, dll := range list { + if dll.Name == dllName { + return true + } + } + + return false +} + +// Index returns the position of the given function name. +func (list List) Index(dllName string, funcName string) int { + index := 0 + + for _, dll := range list { + if dll.Name != dllName { + index += len(dll.Functions) + 1 + continue + } + + for i, fn := range dll.Functions { + if fn == funcName { + return index + i + } + } + } + + return -1 +} \ No newline at end of file diff --git a/src/dll/List_test.go b/src/dll/List_test.go new file mode 100644 index 0000000..216e51a --- /dev/null +++ b/src/dll/List_test.go @@ -0,0 +1,34 @@ +package dll_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/dll" + "git.urbach.dev/go/assert" +) + +func TestList(t *testing.T) { + libs := dll.List{} + assert.False(t, libs.Contains("kernel32")) + assert.Equal(t, -1, libs.Index("kernel32", "ExitProcess")) + + libs = libs.Append("kernel32", "ExitProcess") + assert.True(t, libs.Contains("kernel32")) + assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess")) + + libs = libs.Append("user32", "MessageBox") + assert.True(t, libs.Contains("kernel32")) + assert.True(t, libs.Contains("user32")) + assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess")) + assert.Equal(t, 2, libs.Index("user32", "MessageBox")) + + libs = libs.Append("kernel32", "SetConsoleCP") + assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess")) + assert.Equal(t, 1, libs.Index("kernel32", "SetConsoleCP")) + assert.Equal(t, 3, libs.Index("user32", "MessageBox")) + + libs = libs.Append("kernel32", "ExitProcess") + assert.Equal(t, 0, libs.Index("kernel32", "ExitProcess")) + assert.Equal(t, 1, libs.Index("kernel32", "SetConsoleCP")) + assert.Equal(t, 3, libs.Index("user32", "MessageBox")) +} \ No newline at end of file diff --git a/src/linker/WriteFile.go b/src/linker/WriteFile.go index 02dae2f..b50eddc 100644 --- a/src/linker/WriteFile.go +++ b/src/linker/WriteFile.go @@ -9,6 +9,7 @@ import ( "git.urbach.dev/cli/q/src/data" "git.urbach.dev/cli/q/src/elf" "git.urbach.dev/cli/q/src/macho" + "git.urbach.dev/cli/q/src/pe" ) // WriteFile writes an executable file to disk. @@ -33,13 +34,15 @@ func WriteFile(executable string, b *build.Build, env *core.Environment) error { final.Merge(&f.Assembler) }) - code, data := final.Compile(b) + code, data, libs := final.Compile(b) switch b.OS { case build.Linux: elf.Write(file, b, code, data) case build.Mac: macho.Write(file, b, code, data) + case build.Windows: + pe.Write(file, b, code, data, libs) } err = file.Close() diff --git a/src/pe/Arch.go b/src/pe/Arch.go new file mode 100644 index 0000000..ee3d0ab --- /dev/null +++ b/src/pe/Arch.go @@ -0,0 +1,23 @@ +package pe + +import ( + "git.urbach.dev/cli/q/src/build" +) + +const ( + IMAGE_FILE_MACHINE_AMD64 = 0x8664 + IMAGE_FILE_MACHINE_ARM64 = 0xAA64 + IMAGE_FILE_MACHINE_RISCV64 = 0x5064 +) + +// Arch returns the CPU architecture used in the PE header. +func Arch(arch build.Arch) uint16 { + switch arch { + case build.ARM: + return IMAGE_FILE_MACHINE_ARM64 + case build.X86: + return IMAGE_FILE_MACHINE_AMD64 + default: + return 0 + } +} \ No newline at end of file diff --git a/src/pe/Characteristics.go b/src/pe/Characteristics.go new file mode 100644 index 0000000..6d1042c --- /dev/null +++ b/src/pe/Characteristics.go @@ -0,0 +1,21 @@ +package pe + +type Characteristics uint16 + +const ( + IMAGE_FILE_RELOCS_STRIPPED Characteristics = 0x0001 + IMAGE_FILE_EXECUTABLE_IMAGE Characteristics = 0x0002 + IMAGE_FILE_LINE_NUMS_STRIPPED Characteristics = 0x0004 + IMAGE_FILE_LOCAL_SYMS_STRIPPED Characteristics = 0x0008 + IMAGE_FILE_AGGRESSIVE_WS_TRIM Characteristics = 0x0010 + IMAGE_FILE_LARGE_ADDRESS_AWARE Characteristics = 0x0020 + IMAGE_FILE_BYTES_REVERSED_LO Characteristics = 0x0080 + IMAGE_FILE_32BIT_MACHINE Characteristics = 0x0100 + IMAGE_FILE_DEBUG_STRIPPED Characteristics = 0x0200 + IMAGE_FILE_REMOVABLE_RUN_FROM_SWAP Characteristics = 0x0400 + IMAGE_FILE_NET_RUN_FROM_SWAP Characteristics = 0x0800 + IMAGE_FILE_SYSTEM Characteristics = 0x1000 + IMAGE_FILE_DLL Characteristics = 0x2000 + IMAGE_FILE_UP_SYSTEM_ONLY Characteristics = 0x4000 + IMAGE_FILE_BYTES_REVERSED_HI Characteristics = 0x8000 +) \ No newline at end of file diff --git a/src/pe/Constants.go b/src/pe/Constants.go new file mode 100644 index 0000000..97150bd --- /dev/null +++ b/src/pe/Constants.go @@ -0,0 +1,7 @@ +package pe + +const ( + BaseAddress = 0x400000 + NumSections = 3 + HeaderEnd = DOSHeaderSize + NTHeaderSize + OptionalHeader64Size + SectionHeaderSize*NumSections +) \ No newline at end of file diff --git a/src/pe/DLLImport.go b/src/pe/DLLImport.go new file mode 100644 index 0000000..ad84fbc --- /dev/null +++ b/src/pe/DLLImport.go @@ -0,0 +1,11 @@ +package pe + +const DLLImportSize = 20 + +type DLLImport struct { + RvaFunctionNameList uint32 + TimeDateStamp uint32 + ForwarderChain uint32 + RvaModuleName uint32 + RvaFunctionAddressList uint32 +} \ No newline at end of file diff --git a/src/pe/DOSHeader.go b/src/pe/DOSHeader.go new file mode 100644 index 0000000..9c89739 --- /dev/null +++ b/src/pe/DOSHeader.go @@ -0,0 +1,11 @@ +package pe + +const DOSHeaderSize = 64 + +// DOSHeader is at the beginning of each EXE file and nowadays just points +// to the PEHeader using an absolute file offset. +type DOSHeader struct { + Magic [4]byte + _ [56]byte + NTHeaderOffset uint32 +} \ No newline at end of file diff --git a/src/pe/DataDirectory.go b/src/pe/DataDirectory.go new file mode 100644 index 0000000..94df164 --- /dev/null +++ b/src/pe/DataDirectory.go @@ -0,0 +1,6 @@ +package pe + +type DataDirectory struct { + VirtualAddress uint32 + Size uint32 +} \ No newline at end of file diff --git a/src/pe/DllCharacteristics.go b/src/pe/DllCharacteristics.go new file mode 100644 index 0000000..77820d6 --- /dev/null +++ b/src/pe/DllCharacteristics.go @@ -0,0 +1,17 @@ +package pe + +type DllCharacteristics uint16 + +const ( + IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA DllCharacteristics = 0x0020 + IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE DllCharacteristics = 0x0040 + IMAGE_DLLCHARACTERISTICS_FORCE_INTEGRITY DllCharacteristics = 0x0080 + IMAGE_DLLCHARACTERISTICS_NX_COMPAT DllCharacteristics = 0x0100 + IMAGE_DLLCHARACTERISTICS_NO_ISOLATION DllCharacteristics = 0x0200 + IMAGE_DLLCHARACTERISTICS_NO_SEH DllCharacteristics = 0x0400 + IMAGE_DLLCHARACTERISTICS_NO_BIND DllCharacteristics = 0x0800 + IMAGE_DLLCHARACTERISTICS_APPCONTAINER DllCharacteristics = 0x1000 + IMAGE_DLLCHARACTERISTICS_WDM_DRIVER DllCharacteristics = 0x2000 + IMAGE_DLLCHARACTERISTICS_GUARD_CF DllCharacteristics = 0x4000 + IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE DllCharacteristics = 0x8000 +) \ No newline at end of file diff --git a/src/pe/EXE.go b/src/pe/EXE.go new file mode 100644 index 0000000..1a520b9 --- /dev/null +++ b/src/pe/EXE.go @@ -0,0 +1,147 @@ +package pe + +import ( + "bytes" + "encoding/binary" + "io" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/dll" + "git.urbach.dev/cli/q/src/exe" +) + +// EXE is the portable executable format used on Windows. +type EXE struct { + DOSHeader + NTHeader + OptionalHeader64 + Sections []SectionHeader +} + +// Write writes the EXE file to the given writer. +func Write(writer io.WriteSeeker, b *build.Build, codeBytes []byte, dataBytes []byte, libs dll.List) { + x := exe.New(HeaderEnd, b.FileAlign, b.MemoryAlign) + x.InitSections(codeBytes, dataBytes, nil) + code := x.Sections[0] + data := x.Sections[1] + imports := x.Sections[2] + subSystem := IMAGE_SUBSYSTEM_WINDOWS_CUI + arch := Arch(b.Arch) + + importsData, dllData, dllImports, dllDataStart := importLibraries(libs, imports.FileOffset) + buffer := bytes.Buffer{} + binary.Write(&buffer, binary.LittleEndian, &importsData) + binary.Write(&buffer, binary.LittleEndian, &dllData) + binary.Write(&buffer, binary.LittleEndian, &dllImports) + imports.Bytes = buffer.Bytes() + importDirectoryStart := dllDataStart + len(dllData) + importDirectorySize := DLLImportSize * len(dllImports) + imageSize := exe.Align(imports.MemoryOffset+len(imports.Bytes), b.MemoryAlign) + + if libs.Contains("user32") { + subSystem = IMAGE_SUBSYSTEM_WINDOWS_GUI + } + + pe := &EXE{ + DOSHeader: DOSHeader{ + Magic: [4]byte{'M', 'Z', 0, 0}, + NTHeaderOffset: DOSHeaderSize, + }, + NTHeader: NTHeader{ + Signature: [4]byte{'P', 'E', 0, 0}, + Machine: arch, + NumberOfSections: uint16(NumSections), + TimeDateStamp: 0, + PointerToSymbolTable: 0, + NumberOfSymbols: 0, + SizeOfOptionalHeader: OptionalHeader64Size, + Characteristics: IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE, + }, + OptionalHeader64: OptionalHeader64{ + Magic: 0x020B, // PE32+ / 64-bit executable + MajorLinkerVersion: 0x0E, + MinorLinkerVersion: 0x16, + SizeOfCode: uint32(len(code.Bytes)), + SizeOfInitializedData: 0, + SizeOfUninitializedData: 0, + AddressOfEntryPoint: uint32(code.MemoryOffset), + BaseOfCode: uint32(code.MemoryOffset), + ImageBase: BaseAddress, + SectionAlignment: uint32(b.MemoryAlign), // power of 2, must be greater than or equal to FileAlignment + FileAlignment: uint32(b.FileAlign), // power of 2 + MajorOperatingSystemVersion: 0x06, + MinorOperatingSystemVersion: 0, + MajorImageVersion: 0, + MinorImageVersion: 0, + MajorSubsystemVersion: 0x06, + MinorSubsystemVersion: 0, + Win32VersionValue: 0, + SizeOfImage: uint32(imageSize), // a multiple of SectionAlignment + SizeOfHeaders: uint32(code.FileOffset), // section bodies begin here + CheckSum: 0, + Subsystem: subSystem, + DllCharacteristics: IMAGE_DLLCHARACTERISTICS_HIGH_ENTROPY_VA | IMAGE_DLLCHARACTERISTICS_DYNAMIC_BASE | IMAGE_DLLCHARACTERISTICS_NX_COMPAT | IMAGE_DLLCHARACTERISTICS_TERMINAL_SERVER_AWARE, + SizeOfStackReserve: 0x100000, + SizeOfStackCommit: 0x1000, + SizeOfHeapReserve: 0x100000, + SizeOfHeapCommit: 0x1000, + LoaderFlags: 0, + NumberOfRvaAndSizes: 16, + DataDirectory: [16]DataDirectory{ + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: uint32(importDirectoryStart), Size: uint32(importDirectorySize)}, // RVA of the imported function table + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: uint32(imports.MemoryOffset), Size: uint32(len(importsData) * 8)}, // RVA of the import address table + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + {VirtualAddress: 0, Size: 0}, + }, + }, + Sections: []SectionHeader{ + { + Name: [8]byte{'.', 't', 'e', 'x', 't'}, + VirtualSize: uint32(len(code.Bytes)), + VirtualAddress: uint32(code.MemoryOffset), + RawSize: uint32(len(code.Bytes)), // must be a multiple of FileAlignment + RawAddress: uint32(code.FileOffset), // must be a multiple of FileAlignment + Characteristics: IMAGE_SCN_CNT_CODE | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ, + }, + { + Name: [8]byte{'.', 'r', 'd', 'a', 't', 'a'}, + VirtualSize: uint32(len(data.Bytes)), + VirtualAddress: uint32(data.MemoryOffset), + RawSize: uint32(len(data.Bytes)), + RawAddress: uint32(data.FileOffset), + Characteristics: IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ, + }, + { + Name: [8]byte{'.', 'i', 'd', 'a', 't', 'a'}, + VirtualSize: uint32(len(imports.Bytes)), + VirtualAddress: uint32(imports.MemoryOffset), + RawSize: uint32(len(imports.Bytes)), + RawAddress: uint32(imports.FileOffset), + Characteristics: IMAGE_SCN_CNT_INITIALIZED_DATA | IMAGE_SCN_MEM_READ, + }, + }, + } + + binary.Write(writer, binary.LittleEndian, &pe.DOSHeader) + binary.Write(writer, binary.LittleEndian, &pe.NTHeader) + binary.Write(writer, binary.LittleEndian, &pe.OptionalHeader64) + binary.Write(writer, binary.LittleEndian, &pe.Sections) + writer.Seek(int64(code.Padding), io.SeekCurrent) + writer.Write(code.Bytes) + writer.Seek(int64(data.Padding), io.SeekCurrent) + writer.Write(data.Bytes) + writer.Seek(int64(imports.Padding), io.SeekCurrent) + writer.Write(imports.Bytes) +} \ No newline at end of file diff --git a/src/pe/EXE_test.go b/src/pe/EXE_test.go new file mode 100644 index 0000000..b9e7c7e --- /dev/null +++ b/src/pe/EXE_test.go @@ -0,0 +1,15 @@ +package pe_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/exe" + "git.urbach.dev/cli/q/src/pe" +) + +func TestWrite(t *testing.T) { + pe.Write(&exe.Discard{}, &build.Build{Arch: build.ARM, FileAlign: 0x4000, MemoryAlign: 0x4000}, nil, nil, nil) + pe.Write(&exe.Discard{}, &build.Build{Arch: build.X86, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil, nil) + pe.Write(&exe.Discard{}, &build.Build{Arch: build.UnknownArch, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil, nil) +} \ No newline at end of file diff --git a/src/pe/NTHeader.go b/src/pe/NTHeader.go new file mode 100644 index 0000000..a0a9529 --- /dev/null +++ b/src/pe/NTHeader.go @@ -0,0 +1,14 @@ +package pe + +const NTHeaderSize = 24 + +type NTHeader struct { + Signature [4]byte + Machine uint16 + NumberOfSections uint16 + TimeDateStamp uint32 + PointerToSymbolTable uint32 + NumberOfSymbols uint32 + SizeOfOptionalHeader uint16 + Characteristics Characteristics +} \ No newline at end of file diff --git a/src/pe/OptionalHeader64.go b/src/pe/OptionalHeader64.go new file mode 100644 index 0000000..8506d4e --- /dev/null +++ b/src/pe/OptionalHeader64.go @@ -0,0 +1,36 @@ +package pe + +const OptionalHeader64Size = 240 + +type OptionalHeader64 struct { + Magic uint16 + MajorLinkerVersion uint8 + MinorLinkerVersion uint8 + SizeOfCode uint32 + SizeOfInitializedData uint32 + SizeOfUninitializedData uint32 + AddressOfEntryPoint uint32 + BaseOfCode uint32 + ImageBase uint64 + SectionAlignment uint32 + FileAlignment uint32 + MajorOperatingSystemVersion uint16 + MinorOperatingSystemVersion uint16 + MajorImageVersion uint16 + MinorImageVersion uint16 + MajorSubsystemVersion uint16 + MinorSubsystemVersion uint16 + Win32VersionValue uint32 + SizeOfImage uint32 + SizeOfHeaders uint32 + CheckSum uint32 + Subsystem Subsystem + DllCharacteristics DllCharacteristics + SizeOfStackReserve uint64 + SizeOfStackCommit uint64 + SizeOfHeapReserve uint64 + SizeOfHeapCommit uint64 + LoaderFlags uint32 + NumberOfRvaAndSizes uint32 + DataDirectory [16]DataDirectory +} \ No newline at end of file diff --git a/src/pe/SectionCharacteristics.go b/src/pe/SectionCharacteristics.go new file mode 100644 index 0000000..2ab3111 --- /dev/null +++ b/src/pe/SectionCharacteristics.go @@ -0,0 +1,14 @@ +package pe + +type SectionCharacteristics uint32 + +const ( + IMAGE_SCN_CNT_CODE SectionCharacteristics = 0x00000020 + IMAGE_SCN_CNT_INITIALIZED_DATA SectionCharacteristics = 0x00000040 + IMAGE_SCN_CNT_UNINITIALIZED_DATA SectionCharacteristics = 0x00000080 + IMAGE_SCN_LNK_COMDAT SectionCharacteristics = 0x00001000 + IMAGE_SCN_MEM_DISCARDABLE SectionCharacteristics = 0x02000000 + IMAGE_SCN_MEM_EXECUTE SectionCharacteristics = 0x20000000 + IMAGE_SCN_MEM_READ SectionCharacteristics = 0x40000000 + IMAGE_SCN_MEM_WRITE SectionCharacteristics = 0x80000000 +) \ No newline at end of file diff --git a/src/pe/SectionHeader.go b/src/pe/SectionHeader.go new file mode 100644 index 0000000..039360f --- /dev/null +++ b/src/pe/SectionHeader.go @@ -0,0 +1,16 @@ +package pe + +const SectionHeaderSize = 40 + +type SectionHeader struct { + Name [8]byte + VirtualSize uint32 + VirtualAddress uint32 + RawSize uint32 + RawAddress uint32 + PointerToRelocations uint32 + PointerToLineNumbers uint32 + NumberOfRelocations uint16 + NumberOfLineNumbers uint16 + Characteristics SectionCharacteristics +} \ No newline at end of file diff --git a/src/pe/Subsystem.go b/src/pe/Subsystem.go new file mode 100644 index 0000000..2741531 --- /dev/null +++ b/src/pe/Subsystem.go @@ -0,0 +1,20 @@ +package pe + +type Subsystem uint16 + +const ( + IMAGE_SUBSYSTEM_UNKNOWN Subsystem = 0 + IMAGE_SUBSYSTEM_NATIVE Subsystem = 1 + IMAGE_SUBSYSTEM_WINDOWS_GUI Subsystem = 2 + IMAGE_SUBSYSTEM_WINDOWS_CUI Subsystem = 3 + IMAGE_SUBSYSTEM_OS2_CUI Subsystem = 5 + IMAGE_SUBSYSTEM_POSIX_CUI Subsystem = 7 + IMAGE_SUBSYSTEM_NATIVE_WINDOWS Subsystem = 8 + IMAGE_SUBSYSTEM_WINDOWS_CE_GUI Subsystem = 9 + IMAGE_SUBSYSTEM_EFI_APPLICATION Subsystem = 10 + IMAGE_SUBSYSTEM_EFI_BOOT_SERVICE_DRIVER Subsystem = 11 + IMAGE_SUBSYSTEM_EFI_RUNTIME_DRIVER Subsystem = 12 + IMAGE_SUBSYSTEM_EFI_ROM Subsystem = 13 + IMAGE_SUBSYSTEM_XBOX Subsystem = 14 + IMAGE_SUBSYSTEM_WINDOWS_BOOT_APPLICATION Subsystem = 16 +) \ No newline at end of file diff --git a/src/pe/importLibraries.go b/src/pe/importLibraries.go new file mode 100644 index 0000000..d715f18 --- /dev/null +++ b/src/pe/importLibraries.go @@ -0,0 +1,60 @@ +package pe + +import "git.urbach.dev/cli/q/src/dll" + +// importLibraries generates the import address table which contains the addresses of functions imported from DLLs. +func importLibraries(dlls dll.List, importsStart int) ([]uint64, []byte, []DLLImport, int) { + imports := make([]uint64, 0) + dllData := make([]byte, 0) + dllImports := []DLLImport{} + + for _, library := range dlls { + functionsStart := len(imports) * 8 + dllNamePos := len(dllData) + dllData = append(dllData, library.Name...) + dllData = append(dllData, ".dll"...) + dllData = append(dllData, 0x00) + + dllImports = append(dllImports, DLLImport{ + RvaFunctionNameList: uint32(importsStart + functionsStart), + TimeDateStamp: 0, + ForwarderChain: 0, + RvaModuleName: uint32(dllNamePos), + RvaFunctionAddressList: uint32(importsStart + functionsStart), + }) + + for _, fn := range library.Functions { + if len(dllData)&1 != 0 { + dllData = append(dllData, 0x00) // align the next entry on an even boundary + } + + offset := len(dllData) + dllData = append(dllData, 0x00, 0x00) + dllData = append(dllData, fn...) + dllData = append(dllData, 0x00) + + imports = append(imports, uint64(offset)) + } + + imports = append(imports, 0) + } + + dllDataStart := importsStart + len(imports)*8 + + for i := range imports { + if imports[i] == 0 { + continue + } + + imports[i] += uint64(dllDataStart) + } + + for i := range dllImports { + dllImports[i].RvaModuleName += uint32(dllDataStart) + } + + // a zeroed structure marks the end of the list + dllImports = append(dllImports, DLLImport{}) + + return imports, dllData, dllImports, dllDataStart +} \ No newline at end of file diff --git a/src/scanner/scanExtern.go b/src/scanner/scanExtern.go new file mode 100644 index 0000000..0cfc9fd --- /dev/null +++ b/src/scanner/scanExtern.go @@ -0,0 +1,68 @@ +package scanner + +import ( + "git.urbach.dev/cli/q/src/errors" + "git.urbach.dev/cli/q/src/fs" + "git.urbach.dev/cli/q/src/token" +) + +// scanExtern scans a block of external libraries. +func (s *scanner) scanExtern(file *fs.File, tokens token.List, i int) (int, error) { + i++ + + if tokens[i].Kind != token.BlockStart { + return i, errors.New(MissingBlockStart, file, tokens[i].Position) + } + + for i < len(tokens) { + switch tokens[i].Kind { + case token.Identifier: + var err error + i, err = s.scanExternLibrary(file, tokens, i) + + if err != nil { + return i, err + } + + case token.BlockEnd: + return i, nil + } + + i++ + } + + return i, errors.New(MissingBlockEnd, file, tokens[i].Position) +} + +// scanExternLibrary scans a block of external function declarations. +func (s *scanner) scanExternLibrary(file *fs.File, tokens token.List, i int) (int, error) { + dllName := tokens[i].String(file.Bytes) + i++ + + if tokens[i].Kind != token.BlockStart { + return i, errors.New(MissingBlockStart, file, tokens[i].Position) + } + + i++ + + for i < len(tokens) { + switch tokens[i].Kind { + case token.Identifier: + function, j, err := scanSignature(file, dllName, tokens, i, token.NewLine) + + if err != nil { + return j, err + } + + i = j + s.functions <- function + + case token.BlockEnd: + return i, nil + } + + i++ + } + + return i, errors.New(MissingBlockEnd, file, tokens[i].Position) +} \ No newline at end of file diff --git a/src/scanner/scanFile.go b/src/scanner/scanFile.go index 35e0784..d324ea7 100644 --- a/src/scanner/scanFile.go +++ b/src/scanner/scanFile.go @@ -33,6 +33,8 @@ func (s *scanner) scanFile(path string, pkg string) error { case token.Comment: case token.Identifier: i, err = s.scanFunction(file, tokens, i) + case token.Extern: + i, err = s.scanExtern(file, tokens, i) case token.Import: i, err = s.scanImport(file, tokens, i) case token.EOF: diff --git a/src/scanner/scanFunction.go b/src/scanner/scanFunction.go index 1a90692..259c011 100644 --- a/src/scanner/scanFunction.go +++ b/src/scanner/scanFunction.go @@ -11,7 +11,7 @@ import ( // scanFunction scans a function. func (s *scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, error) { - function, i, err := scanSignature(file, tokens, i, token.BlockStart) + function, i, err := scanSignature(file, file.Package, tokens, i, token.BlockStart) if err != nil { return i, err diff --git a/src/scanner/scanSignature.go b/src/scanner/scanSignature.go index 518a654..afcd954 100644 --- a/src/scanner/scanSignature.go +++ b/src/scanner/scanSignature.go @@ -9,7 +9,7 @@ import ( ) // scanSignature scans only the function signature without the body. -func scanSignature(file *fs.File, tokens token.List, i int, delimiter token.Kind) (*core.Function, int, error) { +func scanSignature(file *fs.File, pkg string, tokens token.List, i int, delimiter token.Kind) (*core.Function, int, error) { var ( groupLevel = 0 nameStart = i @@ -83,7 +83,7 @@ func scanSignature(file *fs.File, tokens token.List, i int, delimiter token.Kind } name := tokens[nameStart].String(file.Bytes) - function := core.NewFunction(name, file) + function := core.NewFunction(name, pkg, file) parameters := tokens[inputStart:inputEnd] for param := range parameters.Split { diff --git a/src/x86/Registers.go b/src/x86/Registers.go index 7933258..1c02bc8 100644 --- a/src/x86/Registers.go +++ b/src/x86/Registers.go @@ -22,6 +22,7 @@ const ( ) var CPU = cpu.CPU{ - Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, - Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, + Call: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, + Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9}, + ExternCall: []cpu.Register{R1, R2, R8, R9}, } \ No newline at end of file