Added Windows support
All checks were successful
/ test (push) Successful in 15s

This commit is contained in:
Eduard Urbach 2025-07-01 13:37:40 +02:00
parent dbc865ee67
commit 562c839835
Signed by: akyoto
GPG key ID: 49226B848C78F6C8
37 changed files with 742 additions and 45 deletions

27
docs/pe.md Normal file
View file

@ -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

View file

@ -1,28 +1,23 @@
init() { init() {
// kernel32.SetConsoleCP(cp.utf8) utf8 := 65001
// kernel32.SetConsoleOutputCP(cp.utf8) kernel32.SetConsoleCP(utf8)
kernel32.SetConsoleOutputCP(utf8)
main.main() main.main()
exit() exit()
} }
exit() { exit() {
// kernel32.ExitProcess(0) kernel32.ExitProcess(0)
} }
crash() { crash() {
// kernel32.ExitProcess(1) kernel32.ExitProcess(1)
} }
// const { extern {
// cp { kernel32 {
// utf8 65001 SetConsoleCP(cp uint)
// } SetConsoleOutputCP(cp uint)
// } ExitProcess(code uint)
}
// extern { }
// kernel32 {
// SetConsoleCP(cp uint)
// SetConsoleOutputCP(cp uint)
// ExitProcess(code uint)
// }
// }

View file

@ -1,3 +1,10 @@
write(_ []byte) -> (written int) { write(_ []byte) -> (written int) {
return 0 return 0
} }
extern {
kernel32 {
GetStdHandle(handle int64) -> int64
WriteConsoleA(fd int64, buffer *byte, length uint32, written *uint32) -> bool
}
}

View file

@ -44,4 +44,5 @@ const (
var CPU = cpu.CPU{ var CPU = cpu.CPU{
Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6}, Call: []cpu.Register{X0, X1, X2, X3, X4, X5, X6},
Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5}, Syscall: []cpu.Register{X8, X0, X1, X2, X3, X4, X5},
ExternCall: []cpu.Register{X0, X1, X2, X3, X4, X5, X6, X7},
} }

View file

@ -5,6 +5,7 @@ import (
"git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/build"
"git.urbach.dev/cli/q/src/data" "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/elf"
"git.urbach.dev/cli/q/src/exe" "git.urbach.dev/cli/q/src/exe"
) )
@ -13,6 +14,7 @@ import (
type Assembler struct { type Assembler struct {
Data data.Data Data data.Data
Instructions []Instruction Instructions []Instruction
Libraries dll.List
} }
// Append adds another instruction. // Append adds another instruction.
@ -30,7 +32,7 @@ func (a *Assembler) Last() Instruction {
} }
// Compile compiles the instructions to machine code. // 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() data, dataLabels := a.Data.Finalize()
c := compiler{ c := compiler{
@ -38,6 +40,7 @@ func (a *Assembler) Compile(b *build.Build) (code []byte, data []byte) {
data: data, data: data,
dataLabels: dataLabels, dataLabels: dataLabels,
labels: make(map[string]Address, 32), labels: make(map[string]Address, 32),
libraries: a.Libraries,
} }
switch b.Arch { 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 := 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 dataSectionOffset := x.Sections[1].MemoryOffset - x.Sections[0].MemoryOffset
for dataLabel, address := range dataLabels { for dataLabel, address := range dataLabels {
c.labels[dataLabel] = dataSectionOffset + address 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 { for _, call := range c.deferred {
call() call()
} }
return c.code, c.data return c.code, c.data, c.libraries
} }
// Merge combines the contents of this assembler with another one. // 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:]...) a.Instructions = append(a.Instructions, b.Instructions[skip:]...)
maps.Copy(a.Data, b.Data) 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. // SetData sets the data for the given label.

View file

@ -37,9 +37,9 @@ func TestAssembler(t *testing.T) {
final.Merge(b) final.Merge(b)
final.Merge(c) final.Merge(c)
code, _ := final.Compile(&build.Build{Arch: build.ARM}) code, _, _ := final.Compile(&build.Build{Arch: build.ARM})
assert.NotNil(t, code) assert.NotNil(t, code)
code, _ = final.Compile(&build.Build{Arch: build.X86}) code, _, _ = final.Compile(&build.Build{Arch: build.X86})
assert.NotNil(t, code) assert.NotNil(t, code)
} }

View file

@ -4,10 +4,21 @@ import "git.urbach.dev/cli/q/src/cpu"
type Instruction interface{} type Instruction interface{}
type AndRegisterNumber struct {
Destination cpu.Register
Source cpu.Register
Number int
}
type Call struct { type Call struct {
Label string Label string
} }
type CallExtern struct {
Library string
Function string
}
type FunctionStart struct{} type FunctionStart struct{}
type FunctionEnd struct{} type FunctionEnd struct{}
@ -35,4 +46,11 @@ type MoveRegisterRegister struct {
} }
type Return struct{} type Return struct{}
type SubRegisterNumber struct {
Destination cpu.Register
Source cpu.Register
Number int
}
type Syscall struct{} type Syscall struct{}

View file

@ -1,10 +1,14 @@
package asm package asm
import "git.urbach.dev/cli/q/src/dll"
type compiler struct { type compiler struct {
code []byte code []byte
data []byte data []byte
dataLabels map[string]Address dataLabels map[string]Address
labels map[string]Address labels map[string]Address
libraries dll.List
importsStart int
deferred []func() deferred []func()
} }

View file

@ -2,6 +2,7 @@ package asm
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"git.urbach.dev/cli/q/src/sizeof" "git.urbach.dev/cli/q/src/sizeof"
"git.urbach.dev/cli/q/src/x86" "git.urbach.dev/cli/q/src/x86"
@ -13,6 +14,18 @@ type compilerX86 struct {
func (c *compilerX86) Compile(instr Instruction) { func (c *compilerX86) Compile(instr Instruction) {
switch instr := instr.(type) { 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: case *Call:
c.code = x86.Call(c.code, 0) c.code = x86.Call(c.code, 0)
end := len(c.code) end := len(c.code)
@ -24,6 +37,21 @@ func (c *compilerX86) Compile(instr Instruction) {
panic("unknown label: " + instr.Label) 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 offset := address - end
binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset)) binary.LittleEndian.PutUint32(c.code[end-4:end], uint32(offset))
}) })

View file

@ -7,6 +7,7 @@ import (
"git.urbach.dev/cli/q/src/cpu" "git.urbach.dev/cli/q/src/cpu"
"git.urbach.dev/cli/q/src/ssa" "git.urbach.dev/cli/q/src/ssa"
"git.urbach.dev/cli/q/src/types" "git.urbach.dev/cli/q/src/types"
"git.urbach.dev/cli/q/src/x86"
) )
// Compile turns a function into machine code. // Compile turns a function into machine code.
@ -72,11 +73,19 @@ func (f *Function) Compile() {
for instr := range f.Values { for instr := range f.Values {
switch instr := instr.(type) { switch instr := instr.(type) {
case *ssa.Call: 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) { if fn.IsExtern() {
case *ssa.Function: f.mv(instr.Arguments[1:], f.CPU.ExternCall)
f.Assembler.Instructions = append(f.Assembler.Instructions, &asm.Call{Label: arg.UniqueName}) 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: case *ssa.Syscall:

View file

@ -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) return nil, errors.New(&UnknownIdentifier{Name: label}, f.File, left.Token.Position)
} }
if function.IsExtern() {
f.Assembler.Libraries = f.Assembler.Libraries.Append(function.Package, function.Name)
} else {
f.Dependencies.Add(function) f.Dependencies.Add(function)
}
v := f.AppendFunction(function.UniqueName, function.Type) v := f.AppendFunction(function.UniqueName, function.Type)
v.Source = ssa.Source(expr.Source) v.Source = ssa.Source(expr.Source)
return v, nil return v, nil

View file

@ -16,6 +16,7 @@ import (
type Function struct { type Function struct {
ssa.IR ssa.IR
Name string Name string
Package string
UniqueName string UniqueName string
File *fs.File File *fs.File
Input []*ssa.Parameter Input []*ssa.Parameter
@ -31,11 +32,12 @@ type Function struct {
} }
// NewFunction creates a new function. // 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{ return &Function{
Name: name, Name: name,
Package: pkg,
UniqueName: fmt.Sprintf("%s.%s", pkg, name),
File: file, File: file,
UniqueName: fmt.Sprintf("%s.%s", file.Package, name),
Identifiers: make(map[string]ssa.Value, 8), Identifiers: make(map[string]ssa.Value, 8),
IR: ssa.IR{ IR: ssa.IR{
Blocks: []*ssa.Block{ Blocks: []*ssa.Block{

View file

@ -4,4 +4,5 @@ package cpu
type CPU struct { type CPU struct {
Call []Register Call []Register
Syscall []Register Syscall []Register
ExternCall []Register
} }

6
src/dll/Library.go Normal file
View file

@ -0,0 +1,6 @@
package dll
type Library struct {
Name string
Functions []string
}

55
src/dll/List.go Normal file
View file

@ -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
}

34
src/dll/List_test.go Normal file
View file

@ -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"))
}

View file

@ -9,6 +9,7 @@ import (
"git.urbach.dev/cli/q/src/data" "git.urbach.dev/cli/q/src/data"
"git.urbach.dev/cli/q/src/elf" "git.urbach.dev/cli/q/src/elf"
"git.urbach.dev/cli/q/src/macho" "git.urbach.dev/cli/q/src/macho"
"git.urbach.dev/cli/q/src/pe"
) )
// WriteFile writes an executable file to disk. // 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) final.Merge(&f.Assembler)
}) })
code, data := final.Compile(b) code, data, libs := final.Compile(b)
switch b.OS { switch b.OS {
case build.Linux: case build.Linux:
elf.Write(file, b, code, data) elf.Write(file, b, code, data)
case build.Mac: case build.Mac:
macho.Write(file, b, code, data) macho.Write(file, b, code, data)
case build.Windows:
pe.Write(file, b, code, data, libs)
} }
err = file.Close() err = file.Close()

23
src/pe/Arch.go Normal file
View file

@ -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
}
}

21
src/pe/Characteristics.go Normal file
View file

@ -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
)

7
src/pe/Constants.go Normal file
View file

@ -0,0 +1,7 @@
package pe
const (
BaseAddress = 0x400000
NumSections = 3
HeaderEnd = DOSHeaderSize + NTHeaderSize + OptionalHeader64Size + SectionHeaderSize*NumSections
)

11
src/pe/DLLImport.go Normal file
View file

@ -0,0 +1,11 @@
package pe
const DLLImportSize = 20
type DLLImport struct {
RvaFunctionNameList uint32
TimeDateStamp uint32
ForwarderChain uint32
RvaModuleName uint32
RvaFunctionAddressList uint32
}

11
src/pe/DOSHeader.go Normal file
View file

@ -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
}

6
src/pe/DataDirectory.go Normal file
View file

@ -0,0 +1,6 @@
package pe
type DataDirectory struct {
VirtualAddress uint32
Size uint32
}

View file

@ -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
)

147
src/pe/EXE.go Normal file
View file

@ -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)
}

15
src/pe/EXE_test.go Normal file
View file

@ -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)
}

14
src/pe/NTHeader.go Normal file
View file

@ -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
}

View file

@ -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
}

View file

@ -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
)

16
src/pe/SectionHeader.go Normal file
View file

@ -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
}

20
src/pe/Subsystem.go Normal file
View file

@ -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
)

60
src/pe/importLibraries.go Normal file
View file

@ -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
}

68
src/scanner/scanExtern.go Normal file
View file

@ -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)
}

View file

@ -33,6 +33,8 @@ func (s *scanner) scanFile(path string, pkg string) error {
case token.Comment: case token.Comment:
case token.Identifier: case token.Identifier:
i, err = s.scanFunction(file, tokens, i) i, err = s.scanFunction(file, tokens, i)
case token.Extern:
i, err = s.scanExtern(file, tokens, i)
case token.Import: case token.Import:
i, err = s.scanImport(file, tokens, i) i, err = s.scanImport(file, tokens, i)
case token.EOF: case token.EOF:

View file

@ -11,7 +11,7 @@ import (
// scanFunction scans a function. // scanFunction scans a function.
func (s *scanner) scanFunction(file *fs.File, tokens token.List, i int) (int, error) { 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 { if err != nil {
return i, err return i, err

View file

@ -9,7 +9,7 @@ import (
) )
// scanSignature scans only the function signature without the body. // 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 ( var (
groupLevel = 0 groupLevel = 0
nameStart = i 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) name := tokens[nameStart].String(file.Bytes)
function := core.NewFunction(name, file) function := core.NewFunction(name, pkg, file)
parameters := tokens[inputStart:inputEnd] parameters := tokens[inputStart:inputEnd]
for param := range parameters.Split { for param := range parameters.Split {

View file

@ -24,4 +24,5 @@ const (
var CPU = cpu.CPU{ var CPU = cpu.CPU{
Call: []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}, Syscall: []cpu.Register{R0, R7, R6, R2, R10, R8, R9},
ExternCall: []cpu.Register{R1, R2, R8, R9},
} }