From cc2e98ca49ebd34c85f5af480425ebd3cb5f660f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 23 Jun 2025 16:19:16 +0200 Subject: [PATCH] Added a linker --- docs/elf.md | 37 ++++++++++++++ src/build/Build.go | 12 +++-- src/build/New.go | 4 +- src/build/SetArch.go | 15 ++++++ src/cli/build.go | 14 ++++-- src/cli/exit.go | 4 ++ src/elf/AddSections.go | 37 ++++++++++++++ src/elf/Arch.go | 15 ++++++ src/elf/Constants.go | 11 +++++ src/elf/ELF.go | 79 ++++++++++++++++++++++++++++++ src/elf/ELF_test.go | 15 ++++++ src/elf/Header.go | 28 +++++++++++ src/elf/ProgramFlags.go | 10 ++++ src/elf/ProgramHeader.go | 16 ++++++ src/elf/ProgramType.go | 15 ++++++ src/elf/SectionFlags.go | 12 +++++ src/elf/SectionHeader.go | 18 +++++++ src/elf/SectionType.go | 19 +++++++ src/exe/Align.go | 12 +++++ src/exe/Discard.go | 7 +++ src/exe/Discard_test.go | 14 ++++++ src/exe/Executable.go | 42 ++++++++++++++++ src/exe/Executable_test.go | 19 +++++++ src/exe/Section.go | 9 ++++ src/linker/WriteExecutable.go | 50 +++++++++++++++++++ src/linker/WriteExecutable_test.go | 38 ++++++++++++++ 26 files changed, 541 insertions(+), 11 deletions(-) create mode 100644 docs/elf.md create mode 100644 src/build/SetArch.go create mode 100644 src/elf/AddSections.go create mode 100644 src/elf/Arch.go create mode 100644 src/elf/Constants.go create mode 100644 src/elf/ELF.go create mode 100644 src/elf/ELF_test.go create mode 100644 src/elf/Header.go create mode 100644 src/elf/ProgramFlags.go create mode 100644 src/elf/ProgramHeader.go create mode 100644 src/elf/ProgramType.go create mode 100644 src/elf/SectionFlags.go create mode 100644 src/elf/SectionHeader.go create mode 100644 src/elf/SectionType.go create mode 100644 src/exe/Align.go create mode 100644 src/exe/Discard.go create mode 100644 src/exe/Discard_test.go create mode 100644 src/exe/Executable.go create mode 100644 src/exe/Executable_test.go create mode 100644 src/exe/Section.go create mode 100644 src/linker/WriteExecutable.go create mode 100644 src/linker/WriteExecutable_test.go diff --git a/docs/elf.md b/docs/elf.md new file mode 100644 index 0000000..993c088 --- /dev/null +++ b/docs/elf.md @@ -0,0 +1,37 @@ +# ELF + +## Basic structure + +1. ELF header [0x00 : 0x40] +2. Program headers [0x40 : 0xB0] +3. Padding +4. Executable code [0x1000 : ...] +5. Padding +6. Read-only data [0x2000 : ...] +7. String table +8. Section headers + +## Entry point + +The executables are compiled as position-independent executables (PIE). Therefore the entry point is defined as a file offset instead of a static virtual address. + +## Padding + +To ensure that execution permissions are properly applied, +the code section and the data section are aligned on page boundaries. Permissions like read, write and execute can only be applied to an entire page in memory. + +## Initialization in Linux + +ELF loader: +https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/fs/binfmt_elf.c + +ELF register definitions: +https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/arch/x86/include/asm/elf.h + +## Links + +- https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/tree/include/uapi/linux/elf.h +- https://lwn.net/Articles/631631/ +- https://en.wikipedia.org/wiki/Executable_and_Linkable_Format +- https://www.muppetlabs.com/~breadbox/software/tiny/teensy.html +- https://nathanotterness.com/2021/10/tiny_elf_modernized.html \ No newline at end of file diff --git a/src/build/Build.go b/src/build/Build.go index 002c738..a5d00f5 100644 --- a/src/build/Build.go +++ b/src/build/Build.go @@ -2,9 +2,11 @@ package build // Build describes the parameters for the "build" command. type Build struct { - Files []string - Arch Arch - OS OS - Dry bool - ShowSSA bool + Files []string + Arch Arch + OS OS + FileAlign int + MemoryAlign int + Dry bool + ShowSSA bool } \ No newline at end of file diff --git a/src/build/New.go b/src/build/New.go index d7ed351..aa286e4 100644 --- a/src/build/New.go +++ b/src/build/New.go @@ -12,9 +12,9 @@ func New(files ...string) *Build { switch global.Arch { case "amd64": - b.Arch = X86 + b.SetArch(X86) case "arm64": - b.Arch = ARM + b.SetArch(ARM) } switch global.OS { diff --git a/src/build/SetArch.go b/src/build/SetArch.go new file mode 100644 index 0000000..92abdcb --- /dev/null +++ b/src/build/SetArch.go @@ -0,0 +1,15 @@ +package build + +// SetArch sets the architecture which also influences the default alignment. +func (build *Build) SetArch(arch Arch) { + build.Arch = arch + + switch arch { + case ARM: + build.MemoryAlign = 0x4000 + default: + build.MemoryAlign = 0x1000 + } + + build.FileAlign = build.MemoryAlign +} \ No newline at end of file diff --git a/src/cli/build.go b/src/cli/build.go index cc0c829..3bdd6c6 100644 --- a/src/cli/build.go +++ b/src/cli/build.go @@ -6,6 +6,7 @@ import ( "git.urbach.dev/cli/q/src/build" "git.urbach.dev/cli/q/src/compiler" + "git.urbach.dev/cli/q/src/linker" ) // _build parses the arguments and creates a build. @@ -16,13 +17,18 @@ func _build(args []string) int { return exit(err) } - _, err = compiler.Compile(b) + result, err := compiler.Compile(b) if err != nil { return exit(err) } - return 0 + if b.Dry { + return 0 + } + + err = linker.WriteExecutable(b, result) + return exit(err) } // newBuildFromArgs creates a new build with the given arguments. @@ -40,9 +46,9 @@ func newBuildFromArgs(args []string) (*build.Build, error) { switch args[i] { case "arm": - b.Arch = build.ARM + b.SetArch(build.ARM) case "x86": - b.Arch = build.X86 + b.SetArch(build.X86) default: return b, &invalidValueError{Value: args[i], Parameter: "arch"} } diff --git a/src/cli/exit.go b/src/cli/exit.go index dd1353f..3d92b7c 100644 --- a/src/cli/exit.go +++ b/src/cli/exit.go @@ -9,6 +9,10 @@ import ( // exit returns the exit code depending on the error type. func exit(err error) int { + if err == nil { + return 0 + } + fmt.Fprintln(os.Stderr, err) var ( diff --git a/src/elf/AddSections.go b/src/elf/AddSections.go new file mode 100644 index 0000000..06fce5f --- /dev/null +++ b/src/elf/AddSections.go @@ -0,0 +1,37 @@ +package elf + +import "bytes" + +// AddSections adds section headers to the ELF file. +func (elf *ELF) AddSections() { + elf.StringTable = []byte("\000.text\000.shstrtab\000") + stringTableStart := elf.DataHeader.Offset + elf.DataHeader.SizeInFile + sectionHeaderStart := stringTableStart + int64(len(elf.StringTable)) + + elf.SectionHeaders = []SectionHeader{ + { + Type: SectionTypeNULL, + }, + { + NameIndex: int32(bytes.Index(elf.StringTable, []byte(".text\000"))), + Type: SectionTypePROGBITS, + Flags: SectionFlagsAllocate | SectionFlagsExecutable, + VirtualAddress: elf.CodeHeader.VirtualAddress, + Offset: elf.CodeHeader.Offset, + SizeInFile: elf.CodeHeader.SizeInFile, + Align: elf.CodeHeader.Align, + }, + { + NameIndex: int32(bytes.Index(elf.StringTable, []byte(".shstrtab\000"))), + Type: SectionTypeSTRTAB, + Offset: int64(stringTableStart), + SizeInFile: int64(len(elf.StringTable)), + Align: 1, + }, + } + + elf.SectionHeaderEntrySize = SectionHeaderSize + elf.SectionHeaderEntryCount = int16(len(elf.SectionHeaders)) + elf.SectionHeaderOffset = int64(sectionHeaderStart) + elf.SectionNameStringTableIndex = 2 +} \ No newline at end of file diff --git a/src/elf/Arch.go b/src/elf/Arch.go new file mode 100644 index 0000000..1a7089d --- /dev/null +++ b/src/elf/Arch.go @@ -0,0 +1,15 @@ +package elf + +import "git.urbach.dev/cli/q/src/build" + +// Arch converts the architecture variable to an ELF-specific constant. +func Arch(arch build.Arch) int16 { + switch arch { + case build.ARM: + return ArchitectureARM64 + case build.X86: + return ArchitectureAMD64 + default: + return 0 + } +} \ No newline at end of file diff --git a/src/elf/Constants.go b/src/elf/Constants.go new file mode 100644 index 0000000..f5002e8 --- /dev/null +++ b/src/elf/Constants.go @@ -0,0 +1,11 @@ +package elf + +const ( + LittleEndian = 1 + TypeExecutable = 2 + TypeDynamic = 3 + ArchitectureAMD64 = 0x3E + ArchitectureARM64 = 0xB7 + ArchitectureRISCV = 0xF3 + HeaderEnd = HeaderSize + ProgramHeaderSize*2 +) \ No newline at end of file diff --git a/src/elf/ELF.go b/src/elf/ELF.go new file mode 100644 index 0000000..ed4db81 --- /dev/null +++ b/src/elf/ELF.go @@ -0,0 +1,79 @@ +package elf + +import ( + "encoding/binary" + "io" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/exe" +) + +// ELF represents an ELF file. +type ELF struct { + Header + CodeHeader ProgramHeader + DataHeader ProgramHeader + SectionHeaders []SectionHeader + StringTable []byte +} + +// Write writes the ELF64 format to the given writer. +func Write(writer io.WriteSeeker, b *build.Build, codeBytes []byte, dataBytes []byte) { + x := exe.New(HeaderEnd, b.FileAlign, b.MemoryAlign) + x.InitSections(codeBytes, dataBytes) + code := x.Sections[0] + data := x.Sections[1] + + elf := &ELF{ + Header: Header{ + Magic: [4]byte{0x7F, 'E', 'L', 'F'}, + Class: 2, + Endianness: LittleEndian, + Version: 1, + OSABI: 0, + ABIVersion: 0, + Type: TypeDynamic, + Architecture: Arch(b.Arch), + FileVersion: 1, + EntryPointInMemory: int64(code.MemoryOffset), + ProgramHeaderOffset: HeaderSize, + SectionHeaderOffset: 0, + Flags: 0, + Size: HeaderSize, + ProgramHeaderEntrySize: ProgramHeaderSize, + ProgramHeaderEntryCount: 2, + SectionHeaderEntrySize: 0, + SectionHeaderEntryCount: 0, + SectionNameStringTableIndex: 0, + }, + CodeHeader: ProgramHeader{ + Type: ProgramTypeLOAD, + Flags: ProgramFlagsExecutable | ProgramFlagsReadable, + Offset: int64(code.FileOffset), + VirtualAddress: int64(code.MemoryOffset), + SizeInFile: int64(len(code.Bytes)), + SizeInMemory: int64(len(code.Bytes)), + Align: int64(b.MemoryAlign), + }, + DataHeader: ProgramHeader{ + Type: ProgramTypeLOAD, + Flags: ProgramFlagsReadable, + Offset: int64(data.FileOffset), + VirtualAddress: int64(data.MemoryOffset), + SizeInFile: int64(len(data.Bytes)), + SizeInMemory: int64(len(data.Bytes)), + Align: int64(b.MemoryAlign), + }, + } + + elf.AddSections() + binary.Write(writer, binary.LittleEndian, &elf.Header) + binary.Write(writer, binary.LittleEndian, &elf.CodeHeader) + binary.Write(writer, binary.LittleEndian, &elf.DataHeader) + writer.Seek(int64(code.Padding), io.SeekCurrent) + writer.Write(code.Bytes) + writer.Seek(int64(data.Padding), io.SeekCurrent) + writer.Write(data.Bytes) + writer.Write(elf.StringTable) + binary.Write(writer, binary.LittleEndian, &elf.SectionHeaders) +} \ No newline at end of file diff --git a/src/elf/ELF_test.go b/src/elf/ELF_test.go new file mode 100644 index 0000000..b7708a6 --- /dev/null +++ b/src/elf/ELF_test.go @@ -0,0 +1,15 @@ +package elf_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/elf" + "git.urbach.dev/cli/q/src/exe" +) + +func TestWrite(t *testing.T) { + elf.Write(&exe.Discard{}, &build.Build{Arch: build.ARM, FileAlign: 0x4000, MemoryAlign: 0x4000}, nil, nil) + elf.Write(&exe.Discard{}, &build.Build{Arch: build.X86, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil) + elf.Write(&exe.Discard{}, &build.Build{Arch: build.UnknownArch, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil) +} \ No newline at end of file diff --git a/src/elf/Header.go b/src/elf/Header.go new file mode 100644 index 0000000..890ed5a --- /dev/null +++ b/src/elf/Header.go @@ -0,0 +1,28 @@ +package elf + +// HeaderSize is equal to the size of a header in bytes. +const HeaderSize = 64 + +// Header contains general information. +type Header struct { + Magic [4]byte + Class byte + Endianness byte + Version byte + OSABI byte + ABIVersion byte + _ [7]byte + Type int16 + Architecture int16 + FileVersion int32 + EntryPointInMemory int64 + ProgramHeaderOffset int64 + SectionHeaderOffset int64 + Flags int32 + Size int16 + ProgramHeaderEntrySize int16 + ProgramHeaderEntryCount int16 + SectionHeaderEntrySize int16 + SectionHeaderEntryCount int16 + SectionNameStringTableIndex int16 +} \ No newline at end of file diff --git a/src/elf/ProgramFlags.go b/src/elf/ProgramFlags.go new file mode 100644 index 0000000..98e9644 --- /dev/null +++ b/src/elf/ProgramFlags.go @@ -0,0 +1,10 @@ +package elf + +// ProgramFlags specifies the permissions for a segment. +type ProgramFlags int32 + +const ( + ProgramFlagsExecutable ProgramFlags = 0x1 + ProgramFlagsWritable ProgramFlags = 0x2 + ProgramFlagsReadable ProgramFlags = 0x4 +) \ No newline at end of file diff --git a/src/elf/ProgramHeader.go b/src/elf/ProgramHeader.go new file mode 100644 index 0000000..9902982 --- /dev/null +++ b/src/elf/ProgramHeader.go @@ -0,0 +1,16 @@ +package elf + +// ProgramHeaderSize is equal to the size of a program header in bytes. +const ProgramHeaderSize = 56 + +// ProgramHeader points to the executable part of our program. +type ProgramHeader struct { + Type ProgramType + Flags ProgramFlags + Offset int64 + VirtualAddress int64 + PhysicalAddress int64 + SizeInFile int64 + SizeInMemory int64 + Align int64 +} \ No newline at end of file diff --git a/src/elf/ProgramType.go b/src/elf/ProgramType.go new file mode 100644 index 0000000..3e4fefb --- /dev/null +++ b/src/elf/ProgramType.go @@ -0,0 +1,15 @@ +package elf + +// ProgramType indicates the program type. +type ProgramType int32 + +const ( + ProgramTypeNULL ProgramType = 0 + ProgramTypeLOAD ProgramType = 1 + ProgramTypeDYNAMIC ProgramType = 2 + ProgramTypeINTERP ProgramType = 3 + ProgramTypeNOTE ProgramType = 4 + ProgramTypeSHLIB ProgramType = 5 + ProgramTypePHDR ProgramType = 6 + ProgramTypeTLS ProgramType = 7 +) \ No newline at end of file diff --git a/src/elf/SectionFlags.go b/src/elf/SectionFlags.go new file mode 100644 index 0000000..e20b9d0 --- /dev/null +++ b/src/elf/SectionFlags.go @@ -0,0 +1,12 @@ +package elf + +// SectionFlags defines flags for sections. +type SectionFlags int64 + +const ( + SectionFlagsWritable SectionFlags = 1 << 0 + SectionFlagsAllocate SectionFlags = 1 << 1 + SectionFlagsExecutable SectionFlags = 1 << 2 + SectionFlagsStrings SectionFlags = 1 << 5 + SectionFlagsTLS SectionFlags = 1 << 10 +) \ No newline at end of file diff --git a/src/elf/SectionHeader.go b/src/elf/SectionHeader.go new file mode 100644 index 0000000..2035f5b --- /dev/null +++ b/src/elf/SectionHeader.go @@ -0,0 +1,18 @@ +package elf + +// SectionHeaderSize is equal to the size of a section header in bytes. +const SectionHeaderSize = 64 + +// SectionHeader points to the data sections of our program. +type SectionHeader struct { + NameIndex int32 + Type SectionType + Flags SectionFlags + VirtualAddress int64 + Offset int64 + SizeInFile int64 + Link int32 + Info int32 + Align int64 + EntrySize int64 +} \ No newline at end of file diff --git a/src/elf/SectionType.go b/src/elf/SectionType.go new file mode 100644 index 0000000..0868aba --- /dev/null +++ b/src/elf/SectionType.go @@ -0,0 +1,19 @@ +package elf + +// SectionType defines the type of the section. +type SectionType int32 + +const ( + SectionTypeNULL SectionType = 0 + SectionTypePROGBITS SectionType = 1 + SectionTypeSYMTAB SectionType = 2 + SectionTypeSTRTAB SectionType = 3 + SectionTypeRELA SectionType = 4 + SectionTypeHASH SectionType = 5 + SectionTypeDYNAMIC SectionType = 6 + SectionTypeNOTE SectionType = 7 + SectionTypeNOBITS SectionType = 8 + SectionTypeREL SectionType = 9 + SectionTypeSHLIB SectionType = 10 + SectionTypeDYNSYM SectionType = 11 +) \ No newline at end of file diff --git a/src/exe/Align.go b/src/exe/Align.go new file mode 100644 index 0000000..bc2f32e --- /dev/null +++ b/src/exe/Align.go @@ -0,0 +1,12 @@ +package exe + +// Align calculates the next aligned address. +func Align[T int | uint | int64 | uint64 | int32 | uint32](n T, alignment T) T { + return (n + (alignment - 1)) & ^(alignment - 1) +} + +// AlignPad calculates the next aligned address and the padding needed. +func AlignPad[T int | uint | int64 | uint64 | int32 | uint32](n T, alignment T) (T, T) { + aligned := Align(n, alignment) + return aligned, aligned - n +} \ No newline at end of file diff --git a/src/exe/Discard.go b/src/exe/Discard.go new file mode 100644 index 0000000..41044c3 --- /dev/null +++ b/src/exe/Discard.go @@ -0,0 +1,7 @@ +package exe + +// Discard implements a no-op WriteSeeker. +type Discard struct{} + +func (w *Discard) Write(_ []byte) (int, error) { return 0, nil } +func (w *Discard) Seek(_ int64, _ int) (int64, error) { return 0, nil } \ No newline at end of file diff --git a/src/exe/Discard_test.go b/src/exe/Discard_test.go new file mode 100644 index 0000000..344a8f0 --- /dev/null +++ b/src/exe/Discard_test.go @@ -0,0 +1,14 @@ +package exe_test + +import ( + "io" + "testing" + + "git.urbach.dev/cli/q/src/exe" +) + +func TestDiscard(t *testing.T) { + discard := exe.Discard{} + discard.Write(nil) + discard.Seek(0, io.SeekCurrent) +} \ No newline at end of file diff --git a/src/exe/Executable.go b/src/exe/Executable.go new file mode 100644 index 0000000..20655f3 --- /dev/null +++ b/src/exe/Executable.go @@ -0,0 +1,42 @@ +package exe + +// Executable is a generic definition of the binary that later gets translated to OS-specific formats. +type Executable struct { + Sections []*Section + headerEnd int + fileAlign int + memoryAlign int +} + +// New creates a new executable. +func New(headerEnd int, fileAlign int, memoryAlign int) *Executable { + return &Executable{ + headerEnd: headerEnd, + fileAlign: fileAlign, + memoryAlign: memoryAlign, + } +} + +// InitSections generates sections from raw byte slices. +func (exe *Executable) InitSections(raw ...[]byte) { + exe.Sections = make([]*Section, len(raw)) + + for i, data := range raw { + exe.Sections[i] = &Section{Bytes: data} + } + + exe.Update() +} + +// Update recalculates all section offsets. +func (exe *Executable) Update() { + first := exe.Sections[0] + first.FileOffset, first.Padding = AlignPad(exe.headerEnd, exe.fileAlign) + first.MemoryOffset = Align(exe.headerEnd, exe.memoryAlign) + + for i, section := range exe.Sections[1:] { + previous := exe.Sections[i] + section.FileOffset, section.Padding = AlignPad(previous.FileOffset+len(previous.Bytes), exe.fileAlign) + section.MemoryOffset = Align(previous.MemoryOffset+len(previous.Bytes), exe.memoryAlign) + } +} \ No newline at end of file diff --git a/src/exe/Executable_test.go b/src/exe/Executable_test.go new file mode 100644 index 0000000..a00c6f6 --- /dev/null +++ b/src/exe/Executable_test.go @@ -0,0 +1,19 @@ +package exe_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/exe" + "git.urbach.dev/go/assert" +) + +func TestExecutable(t *testing.T) { + align := 0x1000 + x := exe.New(1, align, align) + x.InitSections([]byte{1}, []byte{1}) + assert.Equal(t, len(x.Sections), 2) + assert.Equal(t, x.Sections[0].Padding, align-1) + assert.Equal(t, x.Sections[0].FileOffset, align) + assert.Equal(t, x.Sections[1].Padding, align-1) + assert.Equal(t, x.Sections[1].FileOffset, align*2) +} \ No newline at end of file diff --git a/src/exe/Section.go b/src/exe/Section.go new file mode 100644 index 0000000..e66f71f --- /dev/null +++ b/src/exe/Section.go @@ -0,0 +1,9 @@ +package exe + +// Section represents some data within the executable that will also be loaded into memory. +type Section struct { + Bytes []byte + FileOffset int + Padding int + MemoryOffset int +} \ No newline at end of file diff --git a/src/linker/WriteExecutable.go b/src/linker/WriteExecutable.go new file mode 100644 index 0000000..f41f0f9 --- /dev/null +++ b/src/linker/WriteExecutable.go @@ -0,0 +1,50 @@ +package linker + +import ( + "encoding/binary" + "os" + + "git.urbach.dev/cli/q/src/arm" + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/core" + "git.urbach.dev/cli/q/src/elf" + "git.urbach.dev/cli/q/src/x86" +) + +// WriteExecutable writes an executable file to disk. +func WriteExecutable(b *build.Build, result *core.Environment) error { + executable := b.Executable() + file, err := os.Create(executable) + + if err != nil { + return err + } + + code := []byte{} + data := []byte{} + + switch b.Arch { + case build.ARM: + code = arm.MoveRegisterNumber(code, arm.X8, 93) + code = arm.MoveRegisterNumber(code, arm.X0, 0) + code = binary.LittleEndian.AppendUint32(code, arm.Syscall()) + + case build.X86: + code = x86.MoveRegisterNumber(code, x86.R0, 60) + code = x86.MoveRegisterNumber(code, x86.R7, 0) + code = x86.Syscall(code) + } + + switch b.OS { + case build.Linux: + elf.Write(file, b, code, data) + } + + err = file.Close() + + if err != nil { + return err + } + + return os.Chmod(executable, 0755) +} \ No newline at end of file diff --git a/src/linker/WriteExecutable_test.go b/src/linker/WriteExecutable_test.go new file mode 100644 index 0000000..62c2541 --- /dev/null +++ b/src/linker/WriteExecutable_test.go @@ -0,0 +1,38 @@ +package linker_test + +import ( + "os" + "path/filepath" + "testing" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/compiler" + "git.urbach.dev/cli/q/src/linker" + "git.urbach.dev/go/assert" +) + +func TestWriteExecutable(t *testing.T) { + tmpDir := filepath.Join(os.TempDir(), "q", "tests") + err := os.MkdirAll(tmpDir, 0755) + assert.Nil(t, err) + + fromPath := "../../examples/hello/hello.q" + contents, err := os.ReadFile(fromPath) + assert.Nil(t, err) + + toPath := filepath.Join(tmpDir, "hello.q") + err = os.WriteFile(toPath, contents, 0755) + assert.Nil(t, err) + + b := build.New(toPath) + env, err := compiler.Compile(b) + assert.Nil(t, err) + + b.SetArch(build.ARM) + err = linker.WriteExecutable(b, env) + assert.Nil(t, err) + + b.SetArch(build.X86) + err = linker.WriteExecutable(b, env) + assert.Nil(t, err) +} \ No newline at end of file