From dbc865ee67ebe2e586765bebbf6d9f3ace168ca3 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 30 Jun 2025 20:31:07 +0200 Subject: [PATCH] Added macho package --- docs/macho.md | 12 ++++ src/linker/WriteFile.go | 3 + src/macho/Arch.go | 15 +++++ src/macho/CPU.go | 15 +++++ src/macho/Constants.go | 7 +++ src/macho/Header.go | 15 +++++ src/macho/HeaderFlags.go | 32 +++++++++++ src/macho/HeaderType.go | 12 ++++ src/macho/LoadCommand.go | 34 +++++++++++ src/macho/MachO.go | 119 +++++++++++++++++++++++++++++++++++++++ src/macho/MachO_test.go | 15 +++++ src/macho/Prot.go | 9 +++ src/macho/Segment64.go | 18 ++++++ src/macho/Thread.go | 11 ++++ 14 files changed, 317 insertions(+) create mode 100644 docs/macho.md create mode 100644 src/macho/Arch.go create mode 100644 src/macho/CPU.go create mode 100644 src/macho/Constants.go create mode 100644 src/macho/Header.go create mode 100644 src/macho/HeaderFlags.go create mode 100644 src/macho/HeaderType.go create mode 100644 src/macho/LoadCommand.go create mode 100644 src/macho/MachO.go create mode 100644 src/macho/MachO_test.go create mode 100644 src/macho/Prot.go create mode 100644 src/macho/Segment64.go create mode 100644 src/macho/Thread.go diff --git a/docs/macho.md b/docs/macho.md new file mode 100644 index 0000000..0ef40c5 --- /dev/null +++ b/docs/macho.md @@ -0,0 +1,12 @@ +# Mach-O + +## Notes + +MacOS requires including the headers in the __TEXT segment. + +## Links + +- https://github.com/apple-oss-distributions/xnu/blob/main/EXTERNAL_HEADERS/mach-o/loader.h +- https://en.wikipedia.org/wiki/Mach-O +- https://github.com/aidansteele/osx-abi-macho-file-format-reference +- https://stackoverflow.com/questions/39863112/what-is-required-for-a-mach-o-executable-to-load \ No newline at end of file diff --git a/src/linker/WriteFile.go b/src/linker/WriteFile.go index 68f8de3..02dae2f 100644 --- a/src/linker/WriteFile.go +++ b/src/linker/WriteFile.go @@ -8,6 +8,7 @@ import ( "git.urbach.dev/cli/q/src/core" "git.urbach.dev/cli/q/src/data" "git.urbach.dev/cli/q/src/elf" + "git.urbach.dev/cli/q/src/macho" ) // WriteFile writes an executable file to disk. @@ -37,6 +38,8 @@ func WriteFile(executable string, b *build.Build, env *core.Environment) error { switch b.OS { case build.Linux: elf.Write(file, b, code, data) + case build.Mac: + macho.Write(file, b, code, data) } err = file.Close() diff --git a/src/macho/Arch.go b/src/macho/Arch.go new file mode 100644 index 0000000..24f00f6 --- /dev/null +++ b/src/macho/Arch.go @@ -0,0 +1,15 @@ +package macho + +import "git.urbach.dev/cli/q/src/build" + +// Arch returns the CPU architecture used in the Mach-O header. +func Arch(arch build.Arch) (CPU, uint32) { + switch arch { + case build.ARM: + return CPU_ARM_64, CPU_SUBTYPE_ARM64_ALL | 0x80000000 + case build.X86: + return CPU_X86_64, CPU_SUBTYPE_X86_64_ALL | 0x80000000 + default: + return 0, 0 + } +} \ No newline at end of file diff --git a/src/macho/CPU.go b/src/macho/CPU.go new file mode 100644 index 0000000..2614c72 --- /dev/null +++ b/src/macho/CPU.go @@ -0,0 +1,15 @@ +package macho + +type CPU uint32 + +const ( + CPU_X86 CPU = 7 + CPU_X86_64 CPU = CPU_X86 | 0x01000000 + CPU_ARM CPU = 12 + CPU_ARM_64 CPU = CPU_ARM | 0x01000000 +) + +const ( + CPU_SUBTYPE_ARM64_ALL = 0 + CPU_SUBTYPE_X86_64_ALL = 3 +) \ No newline at end of file diff --git a/src/macho/Constants.go b/src/macho/Constants.go new file mode 100644 index 0000000..eb9cc4a --- /dev/null +++ b/src/macho/Constants.go @@ -0,0 +1,7 @@ +package macho + +const ( + BaseAddress = 0x1000000 + SizeCommands = Segment64Size*3 + ThreadSize + HeaderEnd = HeaderSize + SizeCommands +) \ No newline at end of file diff --git a/src/macho/Header.go b/src/macho/Header.go new file mode 100644 index 0000000..1d9b9cc --- /dev/null +++ b/src/macho/Header.go @@ -0,0 +1,15 @@ +package macho + +const HeaderSize = 32 + +// Header contains general information. +type Header struct { + Magic uint32 + Architecture CPU + MicroArchitecture uint32 + Type HeaderType + NumCommands uint32 + SizeCommands uint32 + Flags HeaderFlags + Reserved uint32 +} \ No newline at end of file diff --git a/src/macho/HeaderFlags.go b/src/macho/HeaderFlags.go new file mode 100644 index 0000000..469fec7 --- /dev/null +++ b/src/macho/HeaderFlags.go @@ -0,0 +1,32 @@ +package macho + +type HeaderFlags uint32 + +const ( + FlagNoUndefs HeaderFlags = 0x1 + FlagIncrLink HeaderFlags = 0x2 + FlagDyldLink HeaderFlags = 0x4 + FlagBindAtLoad HeaderFlags = 0x8 + FlagPrebound HeaderFlags = 0x10 + FlagSplitSegs HeaderFlags = 0x20 + FlagLazyInit HeaderFlags = 0x40 + FlagTwoLevel HeaderFlags = 0x80 + FlagForceFlat HeaderFlags = 0x100 + FlagNoMultiDefs HeaderFlags = 0x200 + FlagNoFixPrebinding HeaderFlags = 0x400 + FlagPrebindable HeaderFlags = 0x800 + FlagAllModsBound HeaderFlags = 0x1000 + FlagSubsectionsViaSymbols HeaderFlags = 0x2000 + FlagCanonical HeaderFlags = 0x4000 + FlagWeakDefines HeaderFlags = 0x8000 + FlagBindsToWeak HeaderFlags = 0x10000 + FlagAllowStackExecution HeaderFlags = 0x20000 + FlagRootSafe HeaderFlags = 0x40000 + FlagSetuidSafe HeaderFlags = 0x80000 + FlagNoReexportedDylibs HeaderFlags = 0x100000 + FlagPIE HeaderFlags = 0x200000 + FlagDeadStrippableDylib HeaderFlags = 0x400000 + FlagHasTLVDescriptors HeaderFlags = 0x800000 + FlagNoHeapExecution HeaderFlags = 0x1000000 + FlagAppExtensionSafe HeaderFlags = 0x2000000 +) \ No newline at end of file diff --git a/src/macho/HeaderType.go b/src/macho/HeaderType.go new file mode 100644 index 0000000..8160ebb --- /dev/null +++ b/src/macho/HeaderType.go @@ -0,0 +1,12 @@ +package macho + +type HeaderType uint32 + +const ( + TypeObject HeaderType = 0x1 + TypeExecute HeaderType = 0x2 + TypeCore HeaderType = 0x4 + TypeDylib HeaderType = 0x6 + TypeBundle HeaderType = 0x8 + TypeDsym HeaderType = 0xA +) \ No newline at end of file diff --git a/src/macho/LoadCommand.go b/src/macho/LoadCommand.go new file mode 100644 index 0000000..2504ba2 --- /dev/null +++ b/src/macho/LoadCommand.go @@ -0,0 +1,34 @@ +package macho + +type LoadCommand uint32 + +const ( + LcSegment LoadCommand = 0x1 + LcSymtab LoadCommand = 0x2 + LcThread LoadCommand = 0x4 + LcUnixthread LoadCommand = 0x5 + LcDysymtab LoadCommand = 0xB + LcDylib LoadCommand = 0xC + LcIdDylib LoadCommand = 0xD + LcLoadDylinker LoadCommand = 0xE + LcIdDylinker LoadCommand = 0xF + LcSegment64 LoadCommand = 0x19 + LcUuid LoadCommand = 0x1B + LcCodeSignature LoadCommand = 0x1D + LcSegmentSplitInfo LoadCommand = 0x1E + LcRpath LoadCommand = 0x8000001C + LcEncryptionInfo LoadCommand = 0x21 + LcDyldInfo LoadCommand = 0x22 + LcDyldInfoOnly LoadCommand = 0x80000022 + LcVersionMinMacosx LoadCommand = 0x24 + LcVersionMinIphoneos LoadCommand = 0x25 + LcFunctionStarts LoadCommand = 0x26 + LcDyldEnvironment LoadCommand = 0x27 + LcMain LoadCommand = 0x80000028 + LcDataInCode LoadCommand = 0x29 + LcSourceVersion LoadCommand = 0x2A + LcDylibCodeSignDrs LoadCommand = 0x2B + LcEncryptionInfo64 LoadCommand = 0x2C + LcVersionMinTvos LoadCommand = 0x2F + LcVersionMinWatchos LoadCommand = 0x30 +) \ No newline at end of file diff --git a/src/macho/MachO.go b/src/macho/MachO.go new file mode 100644 index 0000000..eadde8f --- /dev/null +++ b/src/macho/MachO.go @@ -0,0 +1,119 @@ +package macho + +import ( + "encoding/binary" + "io" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/exe" +) + +// MachO is the executable format used on MacOS. +type MachO struct { + Header + PageZero Segment64 + CodeHeader Segment64 + DataHeader Segment64 + UnixThread Thread +} + +// Write writes the Mach-O 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] + arch, microArch := Arch(b.Arch) + entryPoint := BaseAddress + code.MemoryOffset + + m := &MachO{ + Header: Header{ + Magic: 0xFEEDFACF, + Architecture: arch, + MicroArchitecture: microArch, + Type: TypeExecute, + NumCommands: 4, + SizeCommands: SizeCommands, + Flags: FlagNoUndefs | FlagPIE | FlagNoHeapExecution, + Reserved: 0, + }, + PageZero: Segment64{ + LoadCommand: LcSegment64, + Length: 72, + Name: [16]byte{'_', '_', 'P', 'A', 'G', 'E', 'Z', 'E', 'R', 'O'}, + Address: 0, + SizeInMemory: uint64(BaseAddress), + Offset: 0, + SizeInFile: 0, + NumSections: 0, + Flag: 0, + MaxProt: 0, + InitProt: 0, + }, + CodeHeader: Segment64{ + LoadCommand: LcSegment64, + Length: Segment64Size, + Name: [16]byte{'_', '_', 'T', 'E', 'X', 'T'}, + Address: uint64(BaseAddress), + SizeInMemory: uint64(code.MemoryOffset + len(code.Bytes)), + Offset: 0, + SizeInFile: uint64(code.FileOffset + len(code.Bytes)), + NumSections: 0, + Flag: 0, + MaxProt: ProtReadable | ProtExecutable, + InitProt: ProtReadable | ProtExecutable, + }, + DataHeader: Segment64{ + LoadCommand: LcSegment64, + Length: Segment64Size, + Name: [16]byte{'_', '_', 'D', 'A', 'T', 'A'}, + Address: uint64(BaseAddress + data.MemoryOffset), + SizeInMemory: uint64(len(data.Bytes)), + Offset: uint64(data.FileOffset), + SizeInFile: uint64(len(data.Bytes)), + NumSections: 0, + Flag: 0, + MaxProt: ProtReadable, + InitProt: ProtReadable, + }, + UnixThread: Thread{ + LoadCommand: LcUnixthread, + Len: ThreadSize, + Type: 0x4, + Data: [43]uint32{ + 42, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + uint32(entryPoint), 0, + 0, 0, + 0, 0, + 0, 0, + 0, 0, + }, + }, + } + + binary.Write(writer, binary.LittleEndian, &m.Header) + binary.Write(writer, binary.LittleEndian, &m.PageZero) + binary.Write(writer, binary.LittleEndian, &m.CodeHeader) + binary.Write(writer, binary.LittleEndian, &m.DataHeader) + binary.Write(writer, binary.LittleEndian, &m.UnixThread) + writer.Seek(int64(code.Padding), io.SeekCurrent) + writer.Write(code.Bytes) + writer.Seek(int64(data.Padding), io.SeekCurrent) + writer.Write(data.Bytes) +} \ No newline at end of file diff --git a/src/macho/MachO_test.go b/src/macho/MachO_test.go new file mode 100644 index 0000000..4d26a54 --- /dev/null +++ b/src/macho/MachO_test.go @@ -0,0 +1,15 @@ +package macho_test + +import ( + "testing" + + "git.urbach.dev/cli/q/src/build" + "git.urbach.dev/cli/q/src/exe" + "git.urbach.dev/cli/q/src/macho" +) + +func TestWrite(t *testing.T) { + macho.Write(&exe.Discard{}, &build.Build{Arch: build.ARM, FileAlign: 0x4000, MemoryAlign: 0x4000}, nil, nil) + macho.Write(&exe.Discard{}, &build.Build{Arch: build.X86, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil) + macho.Write(&exe.Discard{}, &build.Build{Arch: build.UnknownArch, FileAlign: 0x1000, MemoryAlign: 0x1000}, nil, nil) +} \ No newline at end of file diff --git a/src/macho/Prot.go b/src/macho/Prot.go new file mode 100644 index 0000000..1575d48 --- /dev/null +++ b/src/macho/Prot.go @@ -0,0 +1,9 @@ +package macho + +type Prot uint32 + +const ( + ProtReadable Prot = 0x1 + ProtWritable Prot = 0x2 + ProtExecutable Prot = 0x4 +) \ No newline at end of file diff --git a/src/macho/Segment64.go b/src/macho/Segment64.go new file mode 100644 index 0000000..7a09ba9 --- /dev/null +++ b/src/macho/Segment64.go @@ -0,0 +1,18 @@ +package macho + +const Segment64Size = 72 + +// Segment64 is a segment load command. +type Segment64 struct { + LoadCommand + Length uint32 + Name [16]byte + Address uint64 + SizeInMemory uint64 + Offset uint64 + SizeInFile uint64 + MaxProt Prot + InitProt Prot + NumSections uint32 + Flag uint32 +} \ No newline at end of file diff --git a/src/macho/Thread.go b/src/macho/Thread.go new file mode 100644 index 0000000..879d7e4 --- /dev/null +++ b/src/macho/Thread.go @@ -0,0 +1,11 @@ +package macho + +const ThreadSize = 184 + +// Thread is a thread state load command. +type Thread struct { + LoadCommand + Len uint32 + Type uint32 + Data [43]uint32 +} \ No newline at end of file