diff --git a/doc/readme.md b/doc/readme.md
index 160e24f..4c5dd07 100644
--- a/doc/readme.md
+++ b/doc/readme.md
@@ -5,6 +5,7 @@ This is the manual of LibObjectFile with the following API covered:
- [ELF Object File Format](#elf-object-file-format) via the `ElfFile` API
- [Archive ar File Format](#archive-ar-file-format) via the `ArArchiveFile` API
- [PE File Format](#pe-object-file-format) via the `PEFile` API
+- [Mach-O File Format](#mach-o-object-file-format) via the `MachOFile` API
## ELF Object File Format
@@ -545,3 +546,150 @@ Assert.AreEqual(156, process.ExitCode);
### Links
- [PE and COFF Specification](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format)
+
+## Mach-O Object File Format
+
+### Overview
+
+The main entry-point for reading/writing a Mach-O file is the [`MachOFile`](https://github.com/xoofx/LibObjectFile/blob/master/src/LibObjectFile/MachO/MachOFile.cs) class.
+
+This class is the equivalent of the Mach-O header and contains the load commands, which describe everything else in the image, along with the content those commands point at.
+
+#### Content
+
+Everything in a Mach-O image is a `MachOContent` in the ordered `MachOFile.Content` list: the header, the load command table, the padding after it, the bytes of each section, each table in `__LINKEDIT`, and any alignment padding in between. Every byte of the file belongs to one of them, so writing the list back out reproduces the file.
+
+Padding is kept as the bytes that were read rather than regenerated, because a linker pads executable sections with `nop` rather than zeros.
+
+#### Addresses and what a layout may move
+
+A section's address is its segment's address plus the section's distance from the segment's file offset:
+
+```
+section.Address - segment.VmAddress == section.FileOffset - segment.FileOffset
+```
+
+Moving a section in the file therefore moves it in memory, and every instruction and relocation referring to it becomes wrong. Content carrying an address is `IsPositionPinned` and a layout leaves it where it is. Only what nothing addresses, which in practice is `__LINKEDIT` and the file tail, is free to move.
+
+This is why a `MachOSegment` describes a mapping rather than owning bytes, and why adding a load command is bounded by the padding the linker left after the command table. `MachOFile.AvailableLoadCommandSpace` reports how much of it is left.
+
+#### Universal binaries
+
+A universal binary holds one image per architecture and is read with [`MachOFatFile`](https://github.com/xoofx/LibObjectFile/blob/master/src/LibObjectFile/MachO/MachOFatFile.cs). Use `MachOFatFile.IsFat` to tell the two apart:
+
+```csharp
+if (MachOFatFile.IsFat(inputStream))
+{
+ var fat = MachOFatFile.Read(inputStream);
+ foreach (var slice in fat.Slices)
+ {
+ Console.WriteLine($"{slice.CpuType}: {slice.File!.FileType}");
+ }
+}
+```
+
+Its header and slice table are stored big-endian whatever the architectures inside are, which is the one place the format departs from the image's own byte order.
+
+### Reading a Mach-O File
+
+The Mach-O API allows to read from a `System.IO.Stream` via the method `MachOFile.Read`:
+
+```csharp
+MachOFile machO = MachOFile.Read(inputStream);
+foreach (var segment in machO.Segments)
+{
+ Console.WriteLine($"{segment.Name} at 0x{segment.VmAddress:X}");
+}
+```
+
+Load commands this library does not model are kept as a `MachOUnknownLoadCommand` and written back verbatim, so an image built by a newer linker still round-trips.
+
+The symbol table, the indirect symbol table and the relocations of a section are decoded on demand:
+
+```csharp
+foreach (var symbol in machO.ReadSymbolTable())
+{
+ Console.WriteLine($"{symbol.Name} {symbol.Kind}");
+}
+```
+
+### Writing a Mach-O File
+
+A `MachOFile` is written back with `MachOFile.Write`:
+
+```csharp
+machO.Write(outputStream);
+```
+
+Content is written at the offset recorded for it, so an image that has not been edited comes back byte for byte. `Write` verifies first and throws if anything is wrong; `TryWrite` reports through a `DiagnosticBag` instead. `MachOFile.Verify` checks an image on its own.
+
+A universal binary is written the same way through `MachOFatFile.Write` or `MachOFatFile.TryWrite`, which lay out each slice before placing it, so a slice that changed size is recorded at the size it actually writes.
+
+The reader requires the load commands to end exactly where the header's `sizeofcmds` says. dyld is looser, stopping once it has walked `ncmds` commands and ignoring any slack after them. The stricter reading is deliberate: writing an image back means reproducing that slack, and an image whose two counts disagree is one where it is not clear which the loader will believe.
+
+### Editing load commands
+
+`MachOFile` offers the operations `install_name_tool` provides, appending to the padding the linker left so that no content moves:
+
+```csharp
+machO.AddLoadDylib("@executable_path/../Frameworks/mylib.dylib");
+machO.AddRPath("@executable_path/../Frameworks");
+machO.RemoveRPath("@loader_path/../Frameworks");
+machO.ChangeDylibName("/usr/lib/libfoo.dylib", "@rpath/libfoo.dylib");
+machO.SetInstallName("@rpath/libmylib.dylib");
+```
+
+`SetInstallName` writes the `LC_ID_DYLIB` name a library reports for itself, which is what the images linking against it record. It applies to a dylib rather than an executable.
+
+A dependency is appended rather than inserted, because dyld identifies a library by the position of its command among the others and the symbol table binds against that number. Removing a dependency is not offered for the same reason. A run path carries no such numbering, so `RemoveRPath` is available.
+
+When the padding runs out these throw, naming the shortfall, rather than moving content and invalidating the image. There is no way around that short of relinking with `-headerpad_max_install_names`, which is also why `install_name_tool` fails in the same situation.
+
+### Code signing
+
+Apple Silicon refuses to execute an unsigned image, so an edited `arm64` binary has to be signed again to stay runnable:
+
+```csharp
+machO.AddRPath("@executable_path/../Frameworks");
+machO.AdHocSign("mytool");
+machO.Write(outputStream);
+```
+
+An ad-hoc signature carries no certificate. It states only that the image hashes to what its code directory says, which is what lets the kernel give the process a stable identity.
+
+Signing has to be the last thing done before writing. Editing afterwards leaves the signature covering bytes that are no longer there, so `MachOFile.IsCodeSignatureStale` records it and writing fails until the image is signed again.
+
+#### Gatekeeper
+
+There are two checks on the way to running a binary, and an ad-hoc signature only gets past one of them.
+
+The kernel is the first. On Apple Silicon it will not execute an unsigned image, and an ad-hoc signature is enough to satisfy it. That is the check an edited `arm64` binary needs signing for.
+
+Gatekeeper is the second, and ad-hoc will not get past it. There is no Developer ID behind the signature and it cannot be notarized. Anything downloaded carries a quarantine attribute, and Gatekeeper refuses it however it was signed here. Clearing the attribute is what lets it launch:
+
+```sh
+xattr -d com.apple.quarantine MyApp.app
+```
+
+One thing worth knowing before reaching for this on a shipping app: re-signing throws away any notarization it came with, and signing it again will not bring that back.
+
+#### Sealing a bundle
+
+`AdHocSign` signs a single Mach-O image. A bundle seals the rest of its files separately, in `Contents/_CodeSignature/CodeResources`, and this library does not touch that seal.
+
+Whether that matters depends on what you edited.
+
+- **The main executable.** It is not in the seal, because its own signature already covers it. Re-sign it and the bundle stays consistent.
+- **A nested framework or dylib.** These are in the seal, by hash. Edit one and the seal goes stale, and the bundle will fail verification until Apple's `codesign` reseals it.
+
+### Printing a Mach-O File
+
+`MachOFile.Print` writes the header and every load command in a form close to `otool -h -l`:
+
+```csharp
+machO.Print(Console.Out);
+```
+
+### Links
+
+- [OS X ABI Mach-O File Format Reference](https://github.com/aidansteele/osx-abi-macho-file-format-reference)
diff --git a/readme.md b/readme.md
index ad098e5..5efcfd2 100644
--- a/readme.md
+++ b/readme.md
@@ -8,10 +8,11 @@ LibObjectFile is a .NET library to read, manipulate and write linker and executa
>
> - **PE** image file format (Portable Executable / DLL)
> - **ELF** object-file format
+> - **Mach-O** object-file format, including universal binaries
> - **DWARF** debugging format (version 4)
> - **Archive `ar`** file format (Common, GNU and BSD variants)
>
-> There is a longer term plan to support other file formats (e.g COFF, MACH-O, .lib) but as I don't
+> There is a longer term plan to support other file formats (e.g COFF, .lib) but as I don't
> have a need for them right now, it is left as an exercise for PR contributors! ;)
## Usage
@@ -54,6 +55,16 @@ elf.Write(outStream);
- Program headers with or without sections
- `ElfFile.AddNeededLibrary` injects a `DT_NEEDED` dependency into an existing image without moving any section (address-preserving, `patchelf`-style)
- Print with `readelf` similar output
+- Good support for the **Mach-O file format**:
+ - Support byte-to-byte roundtrip
+ - Read and write from/to a `System.IO.Stream`
+ - 32 and 64 bit, `i386`, `x86_64` and `arm64`
+ - Universal (fat) binaries via `MachOFatFile`
+ - The load commands a linked image is built from are decoded; the rest round-trip verbatim
+ - Symbol table, indirect symbol table and relocations
+ - `MachOFile.AddLoadDylib`, `AddRPath`, `RemoveRPath`, `ChangeDylibName` and `SetInstallName` edit an existing image without moving any section (address-preserving, `install_name_tool`-style)
+ - `MachOFile.AdHocSign` writes an ad-hoc code signature, which Apple Silicon requires in order to execute an image
+ - `MachOFile.Print` to print the content of a Mach-O file with `otool` similar output
- Support for **DWARF debugging format**:
- Partial support of Version 4 (currently still the default for GCC)
- Support for the sections: `.debug_info`, `.debug_line`, `.debug_aranges`, `.debug_abbrev` and `.debug_str`
diff --git a/src/LibObjectFile.Tests/LibObjectFile.Tests.csproj b/src/LibObjectFile.Tests/LibObjectFile.Tests.csproj
index 77332fe..02ef36c 100644
--- a/src/LibObjectFile.Tests/LibObjectFile.Tests.csproj
+++ b/src/LibObjectFile.Tests/LibObjectFile.Tests.csproj
@@ -25,6 +25,15 @@
+
+
+
+
+
+
+
+
+
@@ -84,6 +93,35 @@
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
diff --git a/src/LibObjectFile.Tests/MachO/MachOEditingTests.cs b/src/LibObjectFile.Tests/MachO/MachOEditingTests.cs
new file mode 100644
index 0000000..5764277
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/MachOEditingTests.cs
@@ -0,0 +1,177 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Linq;
+using LibObjectFile.MachO;
+
+namespace LibObjectFile.Tests.MachO;
+
+[TestClass]
+public class MachOEditingTests : MachOTestBase
+{
+ ///
+ /// The reference fixture is the same input run through Apple's install_name_tool, so
+ /// this pins the encoding against the tool the platform actually ships rather than against
+ /// this library's own idea of what a runpath command looks like.
+ ///
+ [TestMethod]
+ public void AddRPathMatchesInstallNameTool()
+ {
+ var file = LoadMachO("unixthread_i386");
+ file.AddRPath("@executable_path/../Frameworks");
+
+ var expected = File.ReadAllBytes(GetFile("unixthread_i386_rpath"));
+ ByteArrayAssert.AreEqual(expected, WriteToArray(file), "Adding a runpath does not match install_name_tool");
+ }
+
+ ///
+ /// The whole point of editing in place is that nothing moves. A section that shifted would
+ /// invalidate every address and relocation already baked into the image.
+ ///
+ [TestMethod]
+ public void AddingCommandsLeavesEverySectionWhereItWas()
+ {
+ var before = LoadMachO("unixthread_i386");
+ var originalLength = new FileInfo(GetFile("unixthread_i386")).Length;
+ var originalOffsets = before.Segments.SelectMany(s => s.Sections).Select(s => (s.Name, s.FileOffset)).ToArray();
+
+ var file = LoadMachO("unixthread_i386");
+ file.AddLoadDylib("@executable_path/../Frameworks/injected.dylib");
+ file.AddRPath("@executable_path/../Frameworks");
+ var written = WriteToArray(file);
+
+ Assert.AreEqual(originalLength, written.Length, "the file changed size");
+
+ var reread = MachOFile.Read(new MemoryStream(written));
+ var newOffsets = reread.Segments.SelectMany(s => s.Sections).Select(s => (s.Name, s.FileOffset)).ToArray();
+ CollectionAssert.AreEqual(originalOffsets, newOffsets, "a section moved");
+
+ foreach (var segment in reread.Segments)
+ {
+ var original = before.FindSegment(segment.Name);
+ Assert.IsNotNull(original);
+ Assert.AreEqual(original.FileOffset, segment.FileOffset, $"segment {segment.Name} moved");
+ Assert.AreEqual(original.VmAddress, segment.VmAddress, $"segment {segment.Name} was remapped");
+ }
+ }
+
+ ///
+ /// dyld refers to a library by its position in the command list, so a new dependency has to
+ /// go last or every existing binding would point at the wrong library.
+ ///
+ [TestMethod]
+ public void AddLoadDylibAppendsAfterTheExistingLibraries()
+ {
+ var file = LoadMachO("unixthread_i386");
+ var before = file.LinkedLibraries.Select(d => d.Name).ToArray();
+
+ file.AddLoadDylib("@rpath/injected.dylib");
+
+ var after = file.LinkedLibraries.Select(d => d.Name).ToArray();
+ CollectionAssert.AreEqual(before, after.Take(before.Length).ToArray(), "existing libraries were renumbered");
+ Assert.AreEqual("@rpath/injected.dylib", after[^1]);
+ Assert.AreSame(file.LoadCommands[^1], file.LinkedLibraries.Last());
+
+ // A weak dependency and a runpath added alongside have to survive being written out.
+ file.AddLoadDylib("@rpath/weak.dylib", MachOLoadCommandType.LoadWeakDylib);
+ file.AddRPath("@loader_path/../lib");
+ var reread = MachOFile.Read(new MemoryStream(WriteToArray(file)));
+ Assert.IsTrue(reread.LinkedLibraries.Single(d => d.Name == "@rpath/weak.dylib").IsWeak);
+ Assert.AreEqual("@loader_path/../lib", reread.RunPaths.Single().Path);
+ }
+
+ ///
+ /// Once the padding runs out the only way to continue would be to move content, so the edit
+ /// has to fail rather than silently invalidate the image.
+ ///
+ [TestMethod]
+ public void AddingBeyondTheAvailableSpaceThrows()
+ {
+ var file = LoadMachO("unixthread_i386");
+ var space = file.AvailableLoadCommandSpace;
+
+ // Each of these costs 12 bytes of header plus the padded path.
+ var path = new string('a', 100);
+ while (file.AvailableLoadCommandSpace >= 120)
+ {
+ file.AddRPath("/" + path + file.LoadCommands.Count);
+ }
+
+ Assert.IsTrue(file.AvailableLoadCommandSpace < space);
+
+ // Sized against the space actually left rather than the loop's threshold. A fixed path
+ // could still fit whatever padding the fill happened to stop on.
+ var tooLong = new string('a', (int)file.AvailableLoadCommandSpace + 16);
+ var exception = Assert.ThrowsExactly(() => file.AddRPath("/" + tooLong));
+ StringAssert.Contains(exception.Message, "free before the first section");
+ }
+
+ ///
+ /// An edit that does not fit has to leave the image exactly as it was. Assigning the new name
+ /// before checking there is room would leave the command renamed but not resized, describing
+ /// a string longer than it has space for.
+ ///
+ [TestMethod]
+ public void AnEditThatDoesNotFitChangesNothing()
+ {
+ var file = LoadMachO("unixthread_i386");
+
+ // Use up the room so that any growth fails.
+ while (file.AvailableLoadCommandSpace >= 64)
+ {
+ file.AddRPath("/" + new string('a', 40) + file.LoadCommands.Count);
+ }
+
+ var before = WriteToArray(file);
+ var namesBefore = file.LinkedLibraries.Select(d => d.Name).ToArray();
+ var sizesBefore = file.LinkedLibraries.Select(d => d.Size).ToArray();
+ var space = file.AvailableLoadCommandSpace;
+
+ var longName = "/" + new string('b', 200);
+ Assert.ThrowsExactly(() => file.ChangeDylibName("/usr/lib/libSystem.B.dylib", longName));
+
+ CollectionAssert.AreEqual(namesBefore, file.LinkedLibraries.Select(d => d.Name).ToArray(), "a name was changed by a failed edit");
+ CollectionAssert.AreEqual(sizesBefore, file.LinkedLibraries.Select(d => d.Size).ToArray(), "a size was changed by a failed edit");
+ Assert.AreEqual(space, file.AvailableLoadCommandSpace);
+ ByteArrayAssert.AreEqual(before, WriteToArray(file), "a failed edit changed the image");
+ }
+
+ [TestMethod]
+ public void ChangeDylibNameRepointsTheReference()
+ {
+ var file = LoadMachO("unixthread_i386");
+
+ Assert.AreEqual(1, file.ChangeDylibName("/usr/lib/libstdc++.6.dylib", "@rpath/libstdc++.6.dylib"));
+ Assert.AreEqual(0, file.ChangeDylibName("/nothing/here.dylib", "/other.dylib"));
+
+ var reread = MachOFile.Read(new MemoryStream(WriteToArray(file)));
+ CollectionAssert.Contains(reread.LinkedLibraries.Select(d => d.Name).ToArray(), "@rpath/libstdc++.6.dylib");
+ CollectionAssert.DoesNotContain(reread.LinkedLibraries.Select(d => d.Name).ToArray(), "/usr/lib/libstdc++.6.dylib");
+
+ // The same rewrite applied to a dylib's own name, which lives in a different command.
+ var dylib = LoadMachO("libhelloworld_x86_64.dylib");
+ Assert.AreEqual("/usr/local/lib/libhelloworld.dylib", dylib.IdDylib!.Name);
+ dylib.SetInstallName("@rpath/libhelloworld.dylib");
+ Assert.AreEqual("@rpath/libhelloworld.dylib", MachOFile.Read(new MemoryStream(WriteToArray(dylib))).IdDylib!.Name);
+
+ // An executable has no install name to rewrite.
+ Assert.ThrowsExactly(() => LoadMachO("unixthread_i386").SetInstallName("@rpath/whatever.dylib"));
+ }
+
+ [TestMethod]
+ public void RemoveRPathDropsOnlyTheMatchingRunPath()
+ {
+ var file = LoadMachO("unixthread_i386");
+ file.AddRPath("/one");
+ file.AddRPath("/two");
+
+ Assert.IsTrue(file.RemoveRPath("/one"));
+ Assert.IsFalse(file.RemoveRPath("/one"));
+
+ var reread = MachOFile.Read(new MemoryStream(WriteToArray(file)));
+ CollectionAssert.AreEqual(new[] { "/two" }, reread.RunPaths.Select(r => r.Path).ToArray());
+ }
+}
diff --git a/src/LibObjectFile.Tests/MachO/MachOSigningTests.cs b/src/LibObjectFile.Tests/MachO/MachOSigningTests.cs
new file mode 100644
index 0000000..9c80e05
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/MachOSigningTests.cs
@@ -0,0 +1,276 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Buffers.Binary;
+using System.IO;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+using LibObjectFile.MachO;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.CodeSign;
+
+namespace LibObjectFile.Tests.MachO;
+
+using static MachOCodeSignatureConstants;
+
+[TestClass]
+public class MachOSigningTests : MachOTestBase
+{
+ private static byte[] SignAndWrite(string fixture, string identifier)
+ {
+ var file = LoadMachO(fixture);
+ file.AdHocSign(identifier);
+ var stream = new MemoryStream();
+ file.Write(stream);
+ return stream.ToArray();
+ }
+
+ ///
+ /// Recomputes every page digest from the written image and compares it with what the code
+ /// directory claims. This is the property the kernel checks, so getting it wrong produces a
+ /// file that looks signed and is refused at execution.
+ ///
+ [TestMethod]
+ [DataRow("unixthread_i386")]
+ [DataRow("helloworld_x86_64")]
+ [DataRow("helloworld_arm64")]
+ [DataRow("libhelloworld_x86_64.dylib")]
+ public void EveryPageDigestMatchesTheImage(string fixture)
+ {
+ var image = SignAndWrite(fixture, fixture);
+ var file = MachOFile.Read(new MemoryStream(image));
+
+ var command = file.CodeSignature;
+ Assert.IsNotNull(command);
+
+ var codeDirectory = FindCodeDirectory(image, command.DataOffset);
+ var hashOffset = BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(16));
+ var codeSlots = BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(28));
+ var codeLimit = BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(32));
+
+ Assert.AreEqual(command.DataOffset, codeLimit, "the signature must cover exactly the bytes before it");
+ Assert.AreEqual((codeLimit + PageSize - 1) / PageSize, codeSlots);
+
+ for (var slot = 0; slot < codeSlots; slot++)
+ {
+ var start = slot * PageSize;
+ var length = Math.Min(PageSize, (int)codeLimit - start);
+ var expected = SHA256.HashData(image.AsSpan(start, length));
+ var stored = codeDirectory.Slice((int)hashOffset + slot * Sha256Size, Sha256Size);
+ CollectionAssert.AreEqual(expected, stored.ToArray(), $"page {slot} digest does not match");
+ }
+ }
+
+ [TestMethod]
+ public void SignatureUsesTheAdHocCodeDirectoryFormat()
+ {
+ var image = SignAndWrite("unixthread_i386", "swkotor");
+ var file = MachOFile.Read(new MemoryStream(image));
+ var command = file.CodeSignature!;
+
+ // Three blobs, which is what codesign produces for an ad-hoc signature: a code directory,
+ // an empty requirement set and an empty CMS wrapper. The wrapper is present and empty
+ // rather than omitted, matching a shipping ad-hoc signed dylib byte for byte in shape.
+ var superBlob = image.AsSpan((int)command.DataOffset);
+ Assert.AreEqual(EmbeddedSignatureMagic, BinaryPrimitives.ReadUInt32BigEndian(superBlob));
+ Assert.AreEqual(3u, BinaryPrimitives.ReadUInt32BigEndian(superBlob.Slice(8)), "code directory, requirements and CMS slots");
+
+ // The length field is the SuperBlob's own, so it ends at the last blob rather than
+ // covering the padding the signature is rounded up to. codesign reports it that way and
+ // so does Apple's own ad-hoc output; the slack belongs to the load command's datasize.
+ var declaredLength = BinaryPrimitives.ReadUInt32BigEndian(superBlob.Slice(4));
+ var lastBlobEnd = FindBlob(image, command.DataOffset, SignatureSlot).Length
+ + (int)BinaryPrimitives.ReadUInt32BigEndian(superBlob.Slice(12 + 2 * 8 + 4));
+ Assert.AreEqual((uint)lastBlobEnd, declaredLength, "the SuperBlob length excludes the alignment padding");
+ Assert.IsTrue(declaredLength <= command.DataSize, "the reserved region covers the blobs");
+
+ var emptyRequirements = FindBlob(image, command.DataOffset, RequirementsSlot);
+ Assert.AreEqual(12, emptyRequirements.Length, "an empty requirement set");
+ var cms = FindBlob(image, command.DataOffset, SignatureSlot);
+ Assert.AreEqual(BlobWrapperMagic, BinaryPrimitives.ReadUInt32BigEndian(cms));
+ Assert.AreEqual(8, cms.Length, "an empty CMS wrapper, as codesign writes for ad-hoc");
+
+ var codeDirectory = FindCodeDirectory(image, command.DataOffset);
+ Assert.AreEqual(CodeDirectoryMagic, BinaryPrimitives.ReadUInt32BigEndian(codeDirectory));
+ Assert.AreEqual(CodeDirectoryVersionExecSeg, BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(8)));
+ Assert.AreEqual(AdHocFlag, BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(12)));
+ Assert.AreEqual((uint)CodeDirectoryHeaderSize, BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(20)), "identifier follows the fixed header");
+ Assert.AreEqual(2u, BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(24)), "Info and requirements special slots");
+ Assert.AreEqual(Sha256Size, codeDirectory[36]);
+ Assert.AreEqual(HashTypeSha256, codeDirectory[37]);
+ Assert.AreEqual(PageSizeLog2, codeDirectory[39]);
+
+ var identifier = codeDirectory.Slice(CodeDirectoryHeaderSize);
+ Assert.AreEqual("swkotor", Encoding.UTF8.GetString(identifier.Slice(0, identifier.IndexOf((byte)0))));
+
+ // The executable range comes from __TEXT, and an executable is flagged as a main binary.
+ var text = file.FindSegment("__TEXT")!;
+ Assert.AreEqual(text.FileOffset, BinaryPrimitives.ReadUInt64BigEndian(codeDirectory.Slice(64)));
+ Assert.AreEqual(text.FileSize, BinaryPrimitives.ReadUInt64BigEndian(codeDirectory.Slice(72)));
+ Assert.AreEqual(ExecSegMainBinary, BinaryPrimitives.ReadUInt64BigEndian(codeDirectory.Slice(80)));
+
+ var hashOffset = (int)BinaryPrimitives.ReadUInt32BigEndian(codeDirectory.Slice(16));
+
+ var info = codeDirectory.Slice(hashOffset - Sha256Size, Sha256Size);
+ Assert.IsTrue(info.ToArray().All(b => b == 0), "the Info.plist slot should be zero");
+
+ var requirements = FindBlob(image, command.DataOffset, RequirementsSlot);
+ Assert.AreEqual(RequirementsMagic, BinaryPrimitives.ReadUInt32BigEndian(requirements));
+ Assert.AreEqual(0u, BinaryPrimitives.ReadUInt32BigEndian(requirements.Slice(8)), "an ad-hoc signature states no requirements");
+
+ var stored = codeDirectory.Slice(hashOffset - 2 * Sha256Size, Sha256Size);
+ CollectionAssert.AreEqual(SHA256.HashData(requirements.ToArray()), stored.ToArray());
+ }
+
+ ///
+ /// Re-signing has to replace the previous signature rather than stack a new one behind it,
+ /// otherwise repeated edits would grow the file without bound.
+ ///
+ [TestMethod]
+ public void ReSigningReplacesTheExistingSignature()
+ {
+ var file = LoadMachO("helloworld_arm64");
+ Assert.IsNotNull(file.CodeSignature);
+ var commands = file.LoadCommands.Count;
+
+ file.AdHocSign("helloworld");
+ var first = new MemoryStream();
+ file.Write(first);
+
+ var again = MachOFile.Read(new MemoryStream(first.ToArray()));
+ again.AdHocSign("helloworld");
+ var second = new MemoryStream();
+ again.Write(second);
+
+ Assert.AreEqual(commands, again.LoadCommands.Count, "re-signing added a load command");
+ Assert.AreEqual(first.Length, second.Length, "re-signing grew the file");
+ ByteArrayAssert.AreEqual(first.ToArray(), second.ToArray(), "re-signing is not idempotent");
+ }
+
+ [TestMethod]
+ public void SignedImagesRoundTrip()
+ {
+ var image = SignAndWrite("unixthread_i386", "swkotor");
+
+ var reread = MachOFile.Read(new MemoryStream(image));
+ var again = new MemoryStream();
+ reread.Write(again);
+
+ ByteArrayAssert.AreEqual(image, again.ToArray(), "a signed image does not round-trip");
+ }
+
+ ///
+ /// Signing appends to __LINKEDIT, so it must not disturb anything mapped before it.
+ ///
+ [TestMethod]
+ public void SigningLeavesEverySectionWhereItWas()
+ {
+ var before = LoadMachO("unixthread_i386");
+ var originalOffsets = before.Segments.SelectMany(s => s.Sections).Select(s => (s.Name, s.FileOffset)).ToArray();
+
+ var image = SignAndWrite("unixthread_i386", "swkotor");
+ var after = MachOFile.Read(new MemoryStream(image));
+
+ CollectionAssert.AreEqual(originalOffsets, after.Segments.SelectMany(s => s.Sections).Select(s => (s.Name, s.FileOffset)).ToArray());
+
+ var linkEdit = after.FindSegment("__LINKEDIT")!;
+ Assert.AreEqual(before.FindSegment("__LINKEDIT")!.FileOffset, linkEdit.FileOffset, "__LINKEDIT moved");
+ Assert.IsTrue(linkEdit.FileSize > before.FindSegment("__LINKEDIT")!.FileSize, "__LINKEDIT did not grow to cover the signature");
+ Assert.AreEqual(linkEdit.FileEndOffset, after.CodeSignature!.DataOffset + after.CodeSignature.DataSize);
+
+ // The image was unsigned, so signing had to add the command as well as the signature.
+ Assert.IsNull(before.CodeSignature);
+ Assert.AreEqual(before.LoadCommands.Count + 1, after.LoadCommands.Count);
+ Assert.AreEqual(0u, after.CodeSignature.DataOffset % SignatureAlignment, "the signature must be 16-byte aligned");
+
+ // A segment is sized in whole units of the architecture's segment alignment, and codesign
+ // rounds __LINKEDIT up when it grows. The fixtures leave enough slack that the signature
+ // fits inside the existing size, so the slack is removed to make the segment grow. A real
+ // image reaches this on its own: signing the 8MB i386 one adds about 65KB to a segment
+ // with 0x7CC to spare.
+ foreach (var name in new[] { "unixthread_i386", "helloworld_x86_64", "helloworld_arm64" })
+ {
+ var file = LoadMachO(name);
+ var segment = file.FindSegment("__LINKEDIT")!;
+ segment.VmSize = segment.FileSize;
+ file.AdHocSign("swkotor");
+
+ Assert.IsTrue(segment.VmSize >= segment.FileSize, $"{name} __LINKEDIT does not cover its content");
+ Assert.AreEqual(0ul, segment.VmSize % file.SegmentAlignment,
+ $"{name} __LINKEDIT ends mid-page at 0x{segment.VmSize:X} for a 0x{file.SegmentAlignment:X} alignment");
+ }
+ }
+
+ ///
+ /// Editing after signing leaves the signature covering bytes that are no longer there. The
+ /// result would look signed and be refused at execution, so writing it has to fail rather
+ /// than hand back something broken.
+ ///
+ [TestMethod]
+ public void EditingAfterSigningIsRejectedUntilSignedAgain()
+ {
+ var file = LoadMachO("unixthread_i386");
+ file.AdHocSign("swkotor");
+ Assert.IsFalse(file.IsCodeSignatureStale);
+
+ file.AddRPath("@executable_path/../Frameworks");
+ Assert.IsTrue(file.IsCodeSignatureStale);
+
+ var exception = Assert.ThrowsExactly(() => WriteToArray(file));
+ Assert.IsTrue(exception.Diagnostics.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_StaleCodeSignature));
+
+ // Signing again makes it whole, and the digests then cover the edit.
+ file.AdHocSign("swkotor");
+ Assert.IsFalse(file.IsCodeSignatureStale);
+ var image = WriteToArray(file);
+ Assert.AreEqual("@executable_path/../Frameworks", MachOFile.Read(new MemoryStream(image)).RunPaths.Single().Path);
+ }
+
+ [TestMethod]
+ public void RejectsWhatCannotBeSigned()
+ {
+ // An object file has no __LINKEDIT, so there is nowhere for a signature to live.
+ Assert.ThrowsExactly(() => LoadMachO("helloworld_x86_64.o").AdHocSign("helloworld"));
+ Assert.ThrowsExactly(() => LoadMachO("unixthread_i386").AdHocSign(string.Empty));
+
+ // Signing changes a good deal before it can fail, and a half-signed image is worse than
+ // an unsigned one, so a failure has to leave the image exactly as it was.
+ var file = LoadMachO("unixthread_i386");
+ var before = WriteToArray(file);
+ var contents = file.Content.Count;
+ var commands = file.LoadCommands.Count;
+
+ var section = file.FindSegment("__TEXT")!.Sections[0];
+ section.Address += 4;
+ Assert.ThrowsExactly(() => file.AdHocSign("swkotor"));
+ section.Address -= 4;
+
+ Assert.AreEqual(contents, file.Content.Count, "a failed signing left content behind");
+ Assert.AreEqual(commands, file.LoadCommands.Count, "a failed signing left its load command behind");
+ Assert.IsFalse(file.IsCodeSignatureStale, "a failed signing left the image marked stale");
+ ByteArrayAssert.AreEqual(before, WriteToArray(file), "a failed signing changed the image");
+ }
+
+ private static Span FindCodeDirectory(byte[] image, uint signatureOffset)
+ => FindBlob(image, signatureOffset, CodeDirectorySlot);
+
+ private static Span FindBlob(byte[] image, uint signatureOffset, uint slot)
+ {
+ var superBlob = image.AsSpan((int)signatureOffset);
+ var count = BinaryPrimitives.ReadUInt32BigEndian(superBlob.Slice(8));
+ for (var i = 0; i < count; i++)
+ {
+ var entry = superBlob.Slice(12 + i * 8);
+ if (BinaryPrimitives.ReadUInt32BigEndian(entry) != slot) continue;
+
+ var offset = (int)BinaryPrimitives.ReadUInt32BigEndian(entry.Slice(4));
+ var length = (int)BinaryPrimitives.ReadUInt32BigEndian(superBlob.Slice(offset + 4));
+ return superBlob.Slice(offset, length);
+ }
+
+ throw new InvalidOperationException($"The signature has no blob in slot {slot}");
+ }
+}
diff --git a/src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs b/src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs
new file mode 100644
index 0000000..198314b
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/MachOSimpleTests.cs
@@ -0,0 +1,650 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO;
+using LibObjectFile.MachO.Internal;
+using VerifyTests;
+
+namespace LibObjectFile.Tests.MachO;
+
+[TestClass]
+public class MachOSimpleTests : MachOTestBase
+{
+ ///
+ /// The raw structures are copied by value straight out of the file, so a wrong size or an
+ /// unexpected padding byte would silently shift every field after it.
+ ///
+ [TestMethod]
+ public unsafe void RawStructuresMatchTheOnDiskLayout()
+ {
+ Assert.AreEqual(28, sizeof(RawMachHeader32));
+ Assert.AreEqual(32, sizeof(RawMachHeader64));
+ Assert.AreEqual(8, sizeof(RawLoadCommand));
+ Assert.AreEqual(56, sizeof(RawSegmentCommand32));
+ Assert.AreEqual(72, sizeof(RawSegmentCommand64));
+ Assert.AreEqual(68, sizeof(RawSection32));
+ Assert.AreEqual(80, sizeof(RawSection64));
+ Assert.AreEqual(8, sizeof(RawFatHeader));
+ Assert.AreEqual(20, sizeof(RawFatArch));
+ Assert.AreEqual(32, sizeof(RawFatArch64));
+ }
+
+ [TestMethod]
+ [DataRow("helloworld_x86_64", true, MachOCpuType.X86_64, MachOFileType.Execute)]
+ [DataRow("helloworld_arm64", true, MachOCpuType.Arm64, MachOFileType.Execute)]
+ [DataRow("libhelloworld_x86_64.dylib", true, MachOCpuType.X86_64, MachOFileType.Dylib)]
+ [DataRow("helloworld_x86_64.o", true, MachOCpuType.X86_64, MachOFileType.Object)]
+ [DataRow("unixthread_i386", false, MachOCpuType.X86, MachOFileType.Execute)]
+ [DataRow("dyldinfo_i386", false, MachOCpuType.X86, MachOFileType.Execute)]
+ public void ReadsHeader(string name, bool is64Bit, MachOCpuType cpuType, MachOFileType fileType)
+ {
+ var file = LoadMachO(name);
+
+ Assert.AreEqual(is64Bit, file.Is64Bit);
+ Assert.AreEqual(cpuType, file.CpuType);
+ Assert.AreEqual(fileType, file.FileType);
+
+ // ncmds and sizeofcmds sit at the same offsets in both header layouts. Comparing the
+ // decoded table against them catches a walk that read a different number of commands
+ // than the file declares.
+ var raw = File.ReadAllBytes(GetFile(name));
+ Assert.AreEqual(BitConverter.ToUInt32(raw, 16), (uint)file.LoadCommands.Count, "ncmds");
+ Assert.AreEqual(BitConverter.ToUInt32(raw, 20), file.SizeOfCommands, "sizeofcmds");
+ }
+
+ ///
+ /// The padding after the load commands is what an in-place edit consumes, and a command
+ /// keeps the linker's own padding rather than being re-encoded to its smallest legal size.
+ /// Both are what let an untouched image round-trip byte for byte.
+ ///
+ [TestMethod]
+ public void ReportsSpaceLeftForNewLoadCommands()
+ {
+ var file = LoadMachO("unixthread_i386");
+
+ Assert.AreEqual(728u, file.LoadCommandsEndOffset);
+ Assert.AreEqual(2048ul, file.ContentStartOffset);
+ Assert.AreEqual(1320, file.AvailableLoadCommandSpace);
+ Assert.AreEqual(1320ul, file.LoadCommandPadding!.Size);
+ Assert.AreEqual(728ul, file.LoadCommandPadding.Position, "the padding follows the command table");
+
+ var dylinker = file.LoadCommands.OfType().Single(c => c.Type == MachOLoadCommandType.LoadDylinker);
+ Assert.AreEqual(28ul, dylinker.Size);
+ Assert.AreEqual(28u, dylinker.MinimumSize);
+ Assert.IsTrue(file.LoadCommands.OfType().All(d => d.Size >= d.MinimumSize));
+
+ // A table too large for sizeofcmds must not wrap into a small value, which would offer
+ // room that does not exist and let an edit write over the content after it.
+ dylinker.Size = 0x1_0000_0000;
+ Assert.AreEqual(uint.MaxValue, file.SizeOfCommands, "sizeofcmds saturates rather than wrapping");
+ Assert.IsTrue(file.AvailableLoadCommandSpace < 0, "a table that cannot be recorded leaves no room");
+ Assert.IsTrue(file.Verify().Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit));
+ }
+
+ ///
+ /// The snapshot shows the thread state's flavour and length, but not that the eleventh word
+ /// is the entry point, nor that the symbol table runs straight into the string table.
+ ///
+ [TestMethod]
+ public void ReadsThe32BitEntryPointAndTableLayout()
+ {
+ var file = LoadMachO("unixthread_i386");
+
+ var state = file.LoadCommands.OfType().Single().States.Single();
+ Assert.AreEqual(1u, state.Flavor, "x86_THREAD_STATE32");
+ Assert.AreEqual((uint)file.FindSegment("__TEXT")!.Sections.Single().Address, state.Registers[10]);
+
+ var symtab = file.LoadCommands.OfType().Single();
+ Assert.AreEqual(symtab.StringOffset, symtab.SymbolOffset + symtab.SymbolCount * MachOSymbolTableCommand.GetSymbolSize(file.Is64Bit));
+ }
+
+ ///
+ /// Packing is not visible in a printed image, since only the decoded version is shown.
+ ///
+ [TestMethod]
+ public void VersionPackingRoundTrips()
+ {
+ Assert.AreEqual(new Version(10, 13, 0), MachOVersion.Decode(0x000A0D00));
+ Assert.AreEqual(0x000A0D00u, MachOVersion.Encode(new Version(10, 13, 0)));
+ Assert.AreEqual(0x0001000Au, MachOVersion.Encode(new Version(1, 0, 10)));
+ Assert.ThrowsExactly(() => MachOVersion.Encode(new Version(1, 256, 0)));
+ }
+
+ [TestMethod]
+ [DataRow("helloworld_x86_64")]
+ [DataRow("helloworld_arm64")]
+ [DataRow("chainedfixups_arm64")]
+ [DataRow("libhelloworld_x86_64.dylib")]
+ [DataRow("helloworld_x86_64.o")]
+ [DataRow("unixthread_i386")]
+ [DataRow("dyldinfo_i386")]
+ public void ReadWriteIsByteExact(string name)
+ {
+ var original = File.ReadAllBytes(GetFile(name));
+
+ using var input = new MemoryStream(original);
+ var file = MachOFile.Read(input);
+
+ var output = new MemoryStream();
+ file.Write(output);
+
+ ByteArrayAssert.AreEqual(original, output.ToArray(), $"Invalid binary diff for {name} after read -> write");
+ }
+
+ ///
+ /// Commands this code does not model have to survive verbatim, which is what keeps the reader
+ /// usable against images from a newer linker. The type used here is a made-up one, so that
+ /// modelling more real commands later cannot quietly void the test.
+ ///
+ [TestMethod]
+ public void KeepsUnmodelledLoadCommandsVerbatim()
+ {
+ var payload = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 0x01, 0x02, 0x03, 0x04 };
+
+ var file = LoadMachO("unixthread_i386");
+ file.LoadCommands.Add(new MachOUnknownLoadCommand
+ {
+ Type = (MachOLoadCommandType)0x7F000001,
+ Size = 16,
+ Payload = payload,
+ });
+ file.LoadCommands.Add(new MachOTwoLevelHintsCommand
+ {
+ Type = MachOLoadCommandType.TwoLevelHints,
+ Size = MachOTwoLevelHintsCommand.CommandSize,
+ Offset = 0x2000,
+ HintCount = 7,
+ });
+
+ var stream = new MemoryStream();
+ file.Write(stream);
+ var reread = MachOFile.Read(new MemoryStream(stream.ToArray()));
+
+ var survivor = reread.LoadCommands.OfType().Single(c => c.Type == (MachOLoadCommandType)0x7F000001);
+ CollectionAssert.AreEqual(payload, survivor.Payload);
+
+ var hints = reread.LoadCommands.OfType().Single();
+ Assert.AreEqual(0x2000u, hints.Offset);
+ Assert.AreEqual(7u, hints.HintCount);
+ }
+ ///
+ /// Every offset pointing at relocatable data has to move when the map says so. The offsets
+ /// are read back through the typed properties rather than through the same walk being tested,
+ /// so a field the walk does not reach shows up here as one that failed to move.
+ ///
+ [TestMethod]
+ [DataRow("helloworld_x86_64")]
+ [DataRow("chainedfixups_arm64")]
+ [DataRow("unixthread_i386")]
+ [DataRow("dyldinfo_i386")]
+ [DataRow("helloworld_x86_64.o")]
+ public void EveryRecordedFileOffsetIsReachable(string name)
+ {
+ const uint delta = 0x1000;
+
+ var before = ReadOffsets(LoadMachO(name));
+ Assert.AreNotEqual(0, before.Count, "the fixture records no file offsets at all");
+
+ var file = LoadMachO(name);
+ var placementBefore = file.Segments.Select(s => (s.FileOffset, Sections: s.Sections.Select(x => x.FileOffset).ToArray())).ToArray();
+
+ file.UpdateFileOffsets(offset => offset + delta);
+
+ var after = ReadOffsets(file);
+ CollectionAssert.AreEqual(before.Select(o => o + delta).ToArray(), after.ToArray(), "an offset the walk should reach did not move");
+
+ // Placement is outside the contract: moving a segment or a section moves it in memory.
+ var placementAfter = file.Segments.Select(s => (s.FileOffset, Sections: s.Sections.Select(x => x.FileOffset).ToArray())).ToArray();
+ for (var i = 0; i < placementBefore.Length; i++)
+ {
+ Assert.AreEqual(placementBefore[i].FileOffset, placementAfter[i].FileOffset, "a segment was moved");
+ CollectionAssert.AreEqual(placementBefore[i].Sections, placementAfter[i].Sections, "a section was moved");
+ }
+
+ // An identity remap has to leave the image untouched.
+ var identity = LoadMachO(name);
+ identity.UpdateFileOffsets(offset => offset);
+ ByteArrayAssert.AreEqual(File.ReadAllBytes(GetFile(name)), WriteToArray(identity), "an identity remap changed the image");
+ }
+
+ ///
+ /// Reads the relocatable offsets straight off the commands, independently of the walk.
+ ///
+ private static List ReadOffsets(MachOFile file)
+ {
+ var offsets = new List();
+ void Add(uint value)
+ {
+ if (value != 0) offsets.Add(value);
+ }
+
+ foreach (var segment in file.Segments)
+ {
+ foreach (var section in segment.Sections) Add(section.RelocationOffset);
+ }
+
+ foreach (var command in file.LoadCommands)
+ {
+ switch (command)
+ {
+ case MachOSymbolTableCommand symtab:
+ Add(symtab.SymbolOffset); Add(symtab.StringOffset);
+ break;
+ case MachODynamicSymbolTableCommand dysymtab:
+ Add(dysymtab.TableOfContentsOffset); Add(dysymtab.ModuleTableOffset);
+ Add(dysymtab.ExternalReferenceOffset); Add(dysymtab.IndirectSymbolOffset);
+ Add(dysymtab.ExternalRelocationOffset); Add(dysymtab.LocalRelocationOffset);
+ break;
+ case MachODyldInfoCommand dyldInfo:
+ Add(dyldInfo.RebaseOffset); Add(dyldInfo.BindOffset); Add(dyldInfo.WeakBindOffset);
+ Add(dyldInfo.LazyBindOffset); Add(dyldInfo.ExportOffset);
+ break;
+ case MachOLinkEditDataCommand data:
+ Add(data.DataOffset);
+ break;
+ case MachOTwoLevelHintsCommand hints:
+ Add(hints.Offset);
+ break;
+ }
+ }
+
+ return offsets;
+ }
+
+
+ ///
+ /// Verification exists to catch the invariants nothing else does. The address check is the
+ /// one the format rests on: break it and the loader maps a section somewhere other than
+ /// where the code expects, which no round-trip test would notice.
+ ///
+ [TestMethod]
+ public void VerifyAcceptsRealImagesAndCatchesABrokenOne()
+ {
+ foreach (var name in new[] { "helloworld_x86_64", "helloworld_arm64", "unixthread_i386", "helloworld_x86_64.o" })
+ {
+ var diagnostics = new DiagnosticBag();
+ LoadMachO(name).Verify(diagnostics);
+ Assert.IsFalse(diagnostics.HasErrors, $"{name} should verify: {string.Join("; ", diagnostics.Messages)}");
+ }
+
+ var moved = LoadMachO("unixthread_i386");
+ moved.FindSegment("__TEXT")!.Sections[0].Address += 4;
+ var broken = new DiagnosticBag();
+ moved.Verify(broken);
+ Assert.IsTrue(broken.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_SectionAddressMismatch));
+
+ // An object file's sections carry addresses the linker has still to assign, so they do
+ // not track file offsets and the check above does not apply to them. Apple's own crt1.o
+ // breaks it. The committed object fixture happens to satisfy it, so the case is made
+ // rather than found.
+ var relocatable = LoadMachO("helloworld_x86_64.o");
+ relocatable.Segments.Single().Sections[1].Address += 0x1000;
+ Assert.IsFalse(relocatable.Verify().HasErrors, "an object file is not laid out at its final addresses");
+
+ var linked = LoadMachO("helloworld_x86_64");
+ Assert.AreEqual(MachOFileType.Execute, linked.FileType);
+ linked.FindSegment("__TEXT")!.Sections[1].Address += 0x1000;
+ Assert.IsTrue(linked.Verify().Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_SectionAddressMismatch),
+ "the same edit in a linked image is what the check is for");
+
+ var overlong = LoadMachO("unixthread_i386");
+ overlong.LoadCommands[0].Size += 1;
+ var misaligned = new DiagnosticBag();
+ overlong.Verify(misaligned);
+ Assert.IsTrue(misaligned.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidCommandAlignment));
+
+ // A header edited on its own describes a section that is not there, and the round-trip
+ // would still match because the header and the bytes are written from what each holds.
+ var resized = LoadMachO("unixthread_i386");
+ resized.FindSegment("__TEXT")!.Sections[0].Size += 8;
+ var mismatched = new DiagnosticBag();
+ resized.Verify(mismatched);
+ Assert.IsTrue(mismatched.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_SectionContentMismatch));
+ }
+
+ ///
+ /// The padding after the load command table is the only elastic thing in the file, so it has
+ /// to absorb exactly what the table gains and nothing else may shift. This is what an
+ /// install_name_tool-style edit depends on.
+ ///
+ [TestMethod]
+ public void GrowingTheCommandTableConsumesOnlyThePadding()
+ {
+ var file = LoadMachO("unixthread_i386");
+
+ var paddingBefore = file.LoadCommandPadding!.Size;
+ var tableEndBefore = file.LoadCommandsEndOffset;
+ var contentStart = file.ContentStartOffset;
+ var positionsBefore = file.Content.Select(c => (c.GetType().Name, c.Position)).ToArray();
+
+ var added = file.AddRPath("@executable_path/../Frameworks");
+ var image = WriteToArray(file);
+
+ Assert.AreEqual(tableEndBefore + added.Size, file.LoadCommandsEndOffset);
+ Assert.AreEqual(paddingBefore - added.Size, file.LoadCommandPadding!.Size, "the padding did not absorb the new command");
+ Assert.AreEqual(contentStart, file.ContentStartOffset, "content after the padding moved");
+
+ // Only the padding may have moved; everything after it stays exactly where it was.
+ var positionsAfter = file.Content.Select(c => (c.GetType().Name, c.Position)).ToArray();
+ for (var i = 0; i < positionsBefore.Length; i++)
+ {
+ if (positionsBefore[i].Name == nameof(MachOLoadCommandPadding)) continue;
+ Assert.AreEqual(positionsBefore[i], positionsAfter[i], $"content {i} moved");
+ }
+
+ Assert.AreEqual(new FileInfo(GetFile("unixthread_i386")).Length, image.Length, "the file changed size");
+ }
+
+ ///
+ /// Names and values cross-checked against llvm-nm. The object file is included because it
+ /// keeps its tables past the end of its only segment, so they belong to no segment and are
+ /// reached by a different path than a linked image's __LINKEDIT.
+ ///
+ [TestMethod]
+ public void ReadsTheSymbolTable()
+ {
+ var symbols = LoadMachO("helloworld_x86_64").ReadSymbolTable();
+
+ CollectionAssert.AreEqual(
+ new[] { "__mh_execute_header", "_helloworld_data", "_helloworld_twice", "_main", "_printf", "dyld_stub_binder" },
+ symbols.Select(s => s.Name).ToArray());
+
+ var main = symbols.Single(s => s.Name == "_main");
+ Assert.AreEqual(MachOSymbolKind.Section, main.Kind);
+ Assert.IsTrue(main.IsExternal);
+ Assert.IsFalse(main.IsDebug);
+ Assert.AreEqual(1, main.SectionIndex, "__text is the first section");
+ Assert.AreEqual(0x100000f50ul, main.Value);
+
+ var printf = symbols.Single(s => s.Name == "_printf");
+ Assert.AreEqual(MachOSymbolKind.Undefined, printf.Kind);
+ Assert.IsTrue(printf.IsExternal);
+ Assert.AreEqual(0, printf.SectionIndex);
+ Assert.AreEqual(0ul, printf.Value);
+ Assert.AreEqual(1, printf.LibraryOrdinal, "resolved from the first dylib, libSystem");
+
+ var fromObject = LoadMachO("helloworld_x86_64.o").ReadSymbolTable();
+ CollectionAssert.AreEqual(
+ new[] { "_helloworld_data", "_helloworld_twice", "_main", "_printf" },
+ fromObject.Select(s => s.Name).ToArray());
+ Assert.AreEqual(MachOSymbolKind.Undefined, fromObject.Single(s => s.Name == "_printf").Kind);
+
+ // The stub sections index into the indirect table through reserved1, and not every entry
+ // there is an index: the sentinels mark slots the loader has nothing to bind.
+ var file = LoadMachO("helloworld_x86_64");
+ var indirect = file.ReadIndirectSymbolTable();
+ Assert.AreEqual(4, indirect.Count);
+ CollectionAssert.Contains(indirect.ToArray(), MachOFile.IndirectSymbolAbsolute);
+
+ var stubs = file.Segments.SelectMany(s => s.Sections).Single(s => s.Name == "__stubs");
+ Assert.AreEqual(MachOSectionType.SymbolStubs, stubs.SectionType);
+ Assert.AreEqual("_printf", symbols[(int)indirect[(int)stubs.Reserved1]].Name);
+
+ // A 32-bit nlist packs into 12 bytes rather than 16, so it is a separate decode.
+ var i386 = LoadMachO("dyldinfo_i386").ReadSymbolTable();
+ CollectionAssert.AreEqual(new[] { "_main", "_dyldinfo_data" }, i386.Select(s => s.Name).ToArray());
+ Assert.AreEqual(0x800ul, i386[0].Value);
+ Assert.AreEqual(1, i386[0].SectionIndex);
+ Assert.AreEqual(0x2000ul, i386[1].Value);
+ Assert.AreEqual(2, i386[1].SectionIndex);
+
+ // arm64 shares the 64-bit decode with x86_64, so this covers the fixture rather than
+ // another path through the reader.
+ var arm64 = LoadMachO("helloworld_arm64").ReadSymbolTable();
+ Assert.AreEqual(0x100003F24ul, arm64.Single(s => s.Name == "_main").Value);
+ Assert.AreEqual(MachOSymbolKind.Undefined, arm64.Single(s => s.Name == "_printf").Kind);
+ }
+
+ ///
+ /// Values cross-checked against llvm-objdump. The two forms of entry pack their fields into
+ /// the same eight bytes in different orders, so encoding is checked to round-trip as well:
+ /// a field unpacked from the wrong bit still reads back consistently on its own.
+ ///
+ [TestMethod]
+ public void ReadsRelocations()
+ {
+ var file = LoadMachO("helloworld_x86_64.o");
+ var text = file.Segments.SelectMany(s => s.Sections).Single(s => s.Name == "__text");
+ var symbols = file.ReadSymbolTable();
+
+ var relocations = file.ReadRelocations(text);
+ Assert.AreEqual(4, relocations.Count);
+
+ var branch = relocations[0];
+ Assert.AreEqual(0x36, branch.Address);
+ Assert.AreEqual((byte)MachOX86_64RelocationType.Branch, branch.RawType);
+ Assert.IsTrue(branch.IsPcRelative);
+ Assert.IsTrue(branch.IsExternal);
+ Assert.AreEqual(4, branch.LengthInBytes);
+ Assert.IsFalse(branch.IsScattered);
+ Assert.AreEqual("_printf", symbols[(int)branch.SymbolOrSectionNumber].Name);
+
+ // A local entry numbers a section instead of a symbol.
+ var signed = relocations[1];
+ Assert.AreEqual((byte)MachOX86_64RelocationType.Signed, signed.RawType);
+ Assert.IsFalse(signed.IsExternal);
+ Assert.AreEqual(3u, signed.SymbolOrSectionNumber);
+
+ Assert.AreEqual("_helloworld_data", symbols[(int)relocations[3].SymbolOrSectionNumber].Name);
+
+ foreach (var relocation in relocations)
+ {
+ var (word0, word1) = relocation.Encode();
+ var again = MachORelocation.Decode(word0, word1);
+ Assert.AreEqual(relocation.ToString(), again.ToString());
+ Assert.AreEqual(relocation.SymbolOrSectionNumber, again.SymbolOrSectionNumber);
+ }
+
+ // The scattered form has no external flag and carries a target address instead.
+ var scattered = MachORelocation.Decode(MachORelocation.ScatteredMask | (2u << 28) | (1u << 24) | 0x123, 0x4567);
+ Assert.IsTrue(scattered.IsScattered);
+ Assert.AreEqual(0x123, scattered.Address);
+ Assert.AreEqual(1, scattered.RawType);
+ Assert.AreEqual(4, scattered.LengthInBytes);
+ Assert.AreEqual(0x4567, scattered.Value);
+ Assert.IsFalse(scattered.IsExternal);
+ Assert.AreEqual((MachORelocation.ScatteredMask | (2u << 28) | (1u << 24) | 0x123, 0x4567u), scattered.Encode());
+ }
+
+ ///
+ /// A universal binary stores its header big-endian whatever the architectures inside are,
+ /// which is the one place the format departs from the image's own byte order.
+ ///
+ [TestMethod]
+ public void ReadsAndWritesAUniversalBinary()
+ {
+ var original = File.ReadAllBytes(GetFile("helloworld_fat"));
+
+ using var input = new MemoryStream(original);
+ Assert.IsTrue(MachOFatFile.IsFat(input));
+ Assert.IsFalse(MachOFile.IsMachO(input), "a universal binary is not itself a Mach-O image");
+
+ var fat = MachOFatFile.Read(input);
+ Assert.IsFalse(fat.Is64BitOffsets);
+ CollectionAssert.AreEqual(new[] { MachOCpuType.X86_64, MachOCpuType.Arm64 }, fat.Slices.Select(s => s.CpuType).ToArray());
+
+ foreach (var slice in fat.Slices)
+ {
+ Assert.IsNotNull(slice.File);
+ Assert.AreEqual(slice.CpuType, slice.File.CpuType, "the slice table has to agree with the image it points at");
+ Assert.AreEqual(0ul, slice.FileOffset % slice.Alignment, "a slice is mapped directly, so it has to be page aligned");
+ }
+
+ Assert.IsFalse(fat.Verify().HasErrors, "a real universal binary has to verify");
+
+ var output = new MemoryStream();
+ fat.Write(output);
+ ByteArrayAssert.AreEqual(original, output.ToArray(), "Invalid binary diff for helloworld_fat after read -> write");
+
+ // Two slices claiming the same architecture leave the loader picking the first and the
+ // other unreachable, so the write reports it rather than producing a file lipo rejects.
+ var duplicated = MachOFatFile.Read(new MemoryStream(original));
+ duplicated.Slices[1].CpuType = duplicated.Slices[0].CpuType;
+ duplicated.Slices[1].CpuSubType = duplicated.Slices[0].CpuSubType;
+ Assert.IsFalse(duplicated.TryWrite(new MemoryStream(), out var duplicateDiagnostics));
+ Assert.IsTrue(duplicateDiagnostics.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_DuplicateFatSlice), string.Join("; ", duplicateDiagnostics.Messages));
+
+ // A slice that no longer fits the 32-bit slice table has to be reported, since casting it
+ // out would record a different slice than the one written.
+ var tooFar = MachOFatFile.Read(new MemoryStream(original));
+ tooFar.Slices[1].FileOffset = 0x1_0000_0000;
+ tooFar.Slices[1].Size = 0x10;
+ Assert.IsTrue(tooFar.Verify().Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit));
+ }
+
+ ///
+ /// Snapshots the decoded form of each fixture. This covers every field of every command at
+ /// once, so decoding a new command extends the snapshot rather than needing another test,
+ /// and a change to how anything is read shows up as a diff rather than passing unnoticed.
+ ///
+ [TestMethod]
+ [DataRow("helloworld_x86_64")]
+ [DataRow("helloworld_arm64")]
+ [DataRow("chainedfixups_arm64")]
+ [DataRow("libhelloworld_x86_64.dylib")]
+ [DataRow("helloworld_x86_64.o")]
+ [DataRow("unixthread_i386")]
+ [DataRow("dyldinfo_i386")]
+ public async Task Prints(string name)
+ {
+ await VerifyMachO(LoadMachO(name), name);
+ }
+
+
+ ///
+ /// A command is bounded by its own cmdsize and the table by the header's sizeofcmds. Without
+ /// that, a command declaring a size too small for its own fields still reads them, taking
+ /// bytes that belong to whatever follows and decoding something the file does not say.
+ ///
+ [TestMethod]
+ public void RejectsMalformedLoadCommands()
+ {
+ static byte[] Patch(Action corrupt)
+ {
+ var bytes = File.ReadAllBytes(GetFile("unixthread_i386"));
+ corrupt(bytes);
+ return bytes;
+ }
+
+ static uint Read(byte[] b, int offset) => BitConverter.ToUInt32(b, offset);
+ static void Write(byte[] b, int offset, uint value) => BitConverter.GetBytes(value).CopyTo(b, offset);
+
+ // Finds where a load command starts, so the patches below do not depend on offsets that
+ // would silently point at the wrong field if a fixture were regenerated.
+ static int FindCommand(byte[] b, uint type, int skip)
+ {
+ var is64 = Read(b, 0) == 0xfeedfacf;
+ var offset = is64 ? 32 : 28;
+ for (var i = 0; i < Read(b, 16); i++)
+ {
+ if (Read(b, offset) == type && skip-- == 0) return offset;
+ offset += (int)Read(b, offset + 4);
+ }
+
+ throw new InvalidOperationException($"No load command of type 0x{type:X} in the fixture");
+ }
+
+ // The first command is LC_SEGMENT at offset 28. Shrinking its cmdsize below its fixed
+ // part would have it read fields out of the command after it.
+ var tooSmall = Patch(b => Write(b, 32, 8));
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream(tooSmall), out _, out var d1));
+ Assert.IsTrue(d1.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidLoadCommandSize), string.Join("; ", d1.Messages));
+
+ // A table that does not end where the header says means the walk and the header disagree
+ // about which bytes are commands.
+ var shortTable = Patch(b => Write(b, 20, Read(b, 20) - 8));
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream(shortTable), out _, out var d2));
+ Assert.IsTrue(d2.Messages.Any(m => m.Id is DiagnosticId.MACHO_ERR_LoadCommandTableSizeMismatch
+ or DiagnosticId.MACHO_ERR_TruncatedLoadCommand), string.Join("; ", d2.Messages));
+
+ // A section count that does not fit the command would read section headers out of the
+ // commands after it.
+ var tooManySections = Patch(b => Write(b, 28 + 56 + 48, 40));
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream(tooManySections), out _, out var d3));
+ Assert.IsTrue(d3.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidLoadCommandSize), string.Join("; ", d3.Messages));
+
+ // A count chosen so that multiplying it out wraps: 0x40000001 sections at 68 bytes each
+ // comes back to 124, the size of the command being checked, so a check that multiplies
+ // would let it through and the section headers would be read out of the commands after it.
+ var wrappingSections = Patch(b => Write(b, FindCommand(b, 0x1, skip: 1) + 48, 0x40000001));
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream(wrappingSections), out _, out var d5));
+ Assert.IsTrue(d5.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidLoadCommandSize), string.Join("; ", d5.Messages));
+
+ // The same for a tool count: 0x20000000 tools at 8 bytes each wraps to nothing at all.
+ var arm64 = File.ReadAllBytes(GetFile("helloworld_arm64"));
+ Write(arm64, FindCommand(arm64, 0x32, skip: 0) + 20, 0x20000000);
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream(arm64), out _, out var d6));
+ Assert.IsTrue(d6.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidLoadCommandSize), string.Join("; ", d6.Messages));
+
+ // A count large enough that multiplying it by an entry size wraps a 32-bit product would
+ // otherwise pass a length check as a small number and then be read past.
+ var overflowing = LoadMachO("helloworld_x86_64");
+ overflowing.LoadCommands.OfType().Single().SymbolCount = 0x10000001;
+ Assert.ThrowsExactly(() => overflowing.ReadSymbolTable());
+
+ // A universal binary header claiming more slices than the file holds must not be trusted
+ // to size anything before that is checked.
+ var fat = File.ReadAllBytes(GetFile("helloworld_fat"));
+ BitConverter.GetBytes(0x10000000u).Reverse().ToArray().CopyTo(fat, 4);
+ Assert.IsFalse(MachOFatFile.TryRead(new MemoryStream(fat), out _, out var d4));
+ Assert.IsTrue(d4.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidFatHeader), string.Join("; ", d4.Messages));
+
+ // lipo refuses an alignment past 2^15, and a shift count is masked to the width of what
+ // it shifts, so an unchecked exponent would alias to a different alignment rather than
+ // being rejected. The align field of the first slice sits at the end of its 20-byte
+ // entry, and the slice table is big-endian.
+ var badAlign = File.ReadAllBytes(GetFile("helloworld_fat"));
+ BitConverter.GetBytes(MachOFatSlice.MaxAlignLog2 + 1).Reverse().ToArray().CopyTo(badAlign, MachOFatFile.HeaderSize + 16);
+ Assert.IsFalse(MachOFatFile.TryRead(new MemoryStream(badAlign), out _, out var d7));
+ Assert.IsTrue(d7.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidFatSliceAlignment), string.Join("; ", d7.Messages));
+ }
+
+ ///
+ /// A Try method owes the caller diagnostics rather than an exception, whatever it is handed.
+ /// The magic was read without a bound, so anything shorter than four bytes escaped as an
+ /// , as did a universal binary slice with no room for a
+ /// header.
+ ///
+ [TestMethod]
+ public void ReportsRatherThanThrowsOnUnreadableInput()
+ {
+ using var input = new MemoryStream("not a mach-o file at all"u8.ToArray());
+
+ Assert.IsFalse(MachOFile.IsMachO(input));
+ Assert.IsFalse(MachOFile.TryRead(input, out _, out var diagnostics));
+ Assert.IsTrue(diagnostics.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidMagic));
+
+ // A universal static library is a fat file of ar archives rather than images, which is
+ // a real thing to be handed and worth naming instead of reporting a stray magic.
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream("!\n"u8.ToArray()), out _, out var archive));
+ Assert.IsTrue(archive.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_UnexpectedArchive), string.Join("; ", archive.Messages));
+
+ foreach (var length in new[] { 0, 1, 3 })
+ {
+ Assert.IsFalse(MachOFile.TryRead(new MemoryStream(new byte[length]), out _, out var tooShort), $"a {length}-byte stream cannot be an image");
+ Assert.IsTrue(tooShort.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidMagic), string.Join("; ", tooShort.Messages));
+ }
+
+ // Every truncation of a real image, which is what walks each bounded read up to its
+ // limit. Anything that throws here fails the test by escaping.
+ var image = File.ReadAllBytes(GetFile("unixthread_i386"));
+ for (var length = 0; length < image.Length; length += 7)
+ {
+ MachOFile.TryRead(new MemoryStream(image.AsSpan(0, length).ToArray()), out _, out _);
+ }
+
+ // The size of the first slice sits 12 bytes into its entry, and the slice table is
+ // big-endian. A slice with no room for a header must be rejected, not descended into.
+ var fat = File.ReadAllBytes(GetFile("helloworld_fat"));
+ BitConverter.GetBytes(0u).Reverse().ToArray().CopyTo(fat, MachOFatFile.HeaderSize + 12);
+ Assert.IsFalse(MachOFatFile.TryRead(new MemoryStream(fat), out _, out var emptySlice));
+ Assert.IsTrue(emptySlice.Messages.Any(m => m.Id == DiagnosticId.MACHO_ERR_InvalidFatArchRange), string.Join("; ", emptySlice.Messages));
+ }
+}
diff --git a/src/LibObjectFile.Tests/MachO/MachOTestBase.cs b/src/LibObjectFile.Tests/MachO/MachOTestBase.cs
new file mode 100644
index 0000000..559a13c
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/MachOTestBase.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using LibObjectFile.MachO;
+using VerifyMSTest;
+using VerifyTests;
+
+namespace LibObjectFile.Tests.MachO;
+
+public abstract class MachOTestBase : VerifyBase
+{
+ protected static string GetFile(string name) => Path.Combine(AppContext.BaseDirectory, "MachO", name);
+
+ protected static MachOFile LoadMachO(string name) => MachOFile.Read(new MemoryStream(File.ReadAllBytes(GetFile(name))));
+
+ protected static byte[] WriteToArray(MachOFile file)
+ {
+ var stream = new MemoryStream();
+ file.Write(stream);
+ return stream.ToArray();
+ }
+
+ protected async Task VerifyMachO(MachOFile file, string name)
+ {
+ var writer = new StringWriter();
+ file.Print(writer);
+ await Verifier.Verify(writer.ToString()).UseParameters(name);
+ }
+}
diff --git a/src/LibObjectFile.Tests/MachO/chainedfixups_arm64 b/src/LibObjectFile.Tests/MachO/chainedfixups_arm64
new file mode 100755
index 0000000..1186d2f
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/chainedfixups_arm64 differ
diff --git a/src/LibObjectFile.Tests/MachO/dyldinfo_i386 b/src/LibObjectFile.Tests/MachO/dyldinfo_i386
new file mode 100644
index 0000000..296cc64
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/dyldinfo_i386 differ
diff --git a/src/LibObjectFile.Tests/MachO/dyldinfo_i386.yaml b/src/LibObjectFile.Tests/MachO/dyldinfo_i386.yaml
new file mode 100644
index 0000000..3ca795f
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/dyldinfo_i386.yaml
@@ -0,0 +1,178 @@
+# Models a 10.7-era i386 executable: 32-bit segments combined with the modern
+# LC_MAIN entry point and LC_DYLD_INFO_ONLY opcode streams. No current linker
+# emits that combination, since 32-bit Mach-O support was dropped while those
+# commands were still in use, so it has to be synthesized to be tested at all.
+--- !mach-o
+FileHeader:
+ magic: 0xFEEDFACE
+ cputype: 0x00000007
+ cpusubtype: 0x00000003
+ filetype: 0x00000002
+ ncmds: 16
+ sizeofcmds: 720
+ flags: 0x00218085
+LoadCommands:
+ - cmd: LC_SEGMENT
+ cmdsize: 56
+ segname: __PAGEZERO
+ vmaddr: 0
+ vmsize: 4096
+ fileoff: 0
+ filesize: 0
+ maxprot: 0
+ initprot: 0
+ nsects: 0
+ flags: 0
+ - cmd: LC_SEGMENT
+ cmdsize: 124
+ segname: __TEXT
+ vmaddr: 4096
+ vmsize: 4096
+ fileoff: 0
+ filesize: 4096
+ maxprot: 7
+ initprot: 5
+ nsects: 1
+ flags: 0
+ Sections:
+ - sectname: __text
+ segname: __TEXT
+ addr: 0x1800
+ size: 6
+ offset: 0x800
+ align: 2
+ reloff: 0
+ nreloc: 0
+ flags: 0x80000400
+ reserved1: 0
+ reserved2: 0
+ content: 'B82A000000C3'
+ - cmd: LC_SEGMENT
+ cmdsize: 124
+ segname: __DATA
+ vmaddr: 8192
+ vmsize: 4096
+ fileoff: 4096
+ filesize: 4096
+ maxprot: 7
+ initprot: 3
+ nsects: 1
+ flags: 0
+ Sections:
+ - sectname: __data
+ segname: __DATA
+ addr: 0x2000
+ size: 4
+ offset: 0x1000
+ align: 2
+ reloff: 0
+ nreloc: 0
+ flags: 0x00000000
+ reserved1: 0
+ reserved2: 0
+ content: '2A000000'
+ - cmd: LC_SEGMENT
+ cmdsize: 56
+ segname: __LINKEDIT
+ vmaddr: 12288
+ vmsize: 4096
+ fileoff: 8192
+ filesize: 64
+ maxprot: 7
+ initprot: 1
+ nsects: 0
+ flags: 0
+ - cmd: LC_DYLD_INFO_ONLY
+ cmdsize: 48
+ rebase_off: 0
+ rebase_size: 0
+ bind_off: 0
+ bind_size: 0
+ weak_bind_off: 0
+ weak_bind_size: 0
+ lazy_bind_off: 0
+ lazy_bind_size: 0
+ export_off: 8192
+ export_size: 2
+ - cmd: LC_SYMTAB
+ cmdsize: 24
+ symoff: 8194
+ nsyms: 2
+ stroff: 8218
+ strsize: 24
+ - cmd: LC_DYSYMTAB
+ cmdsize: 80
+ ilocalsym: 0
+ nlocalsym: 0
+ iextdefsym: 0
+ nextdefsym: 2
+ iundefsym: 2
+ nundefsym: 0
+ tocoff: 0
+ ntoc: 0
+ modtaboff: 0
+ nmodtab: 0
+ extrefsymoff: 0
+ nextrefsyms: 0
+ indirectsymoff: 0
+ nindirectsyms: 0
+ extreloff: 0
+ nextrel: 0
+ locreloff: 0
+ nlocrel: 0
+ - cmd: LC_LOAD_DYLINKER
+ cmdsize: 28
+ name: 12
+ Content: '/usr/lib/dyld'
+ ZeroPadBytes: 1
+ - cmd: LC_UUID
+ cmdsize: 24
+ uuid: 4D4F4445-524E-3332-4249-540000000002
+ - cmd: LC_VERSION_MIN_MACOSX
+ cmdsize: 16
+ version: 0x000A0700
+ sdk: 0x000A0700
+ - cmd: LC_SOURCE_VERSION
+ cmdsize: 16
+ version: 0
+ - cmd: LC_MAIN
+ cmdsize: 24
+ entryoff: 2048
+ stacksize: 0
+ - cmd: LC_FUNCTION_STARTS
+ cmdsize: 16
+ dataoff: 8248
+ datasize: 8
+ - cmd: LC_DATA_IN_CODE
+ cmdsize: 16
+ dataoff: 8256
+ datasize: 0
+ - cmd: LC_DYLIB_CODE_SIGN_DRS
+ cmdsize: 16
+ dataoff: 8256
+ datasize: 0
+ - cmd: LC_LOAD_DYLIB
+ cmdsize: 52
+ dylib:
+ name: 24
+ timestamp: 2
+ current_version: 0x004F0901
+ compatibility_version: 0x00010000
+ Content: '/usr/lib/libSystem.B.dylib'
+ ZeroPadBytes: 2
+LinkEditData:
+ NameList:
+ - n_strx: 2
+ n_type: 0x0F
+ n_sect: 1
+ n_desc: 0x0010
+ n_value: 2048
+ - n_strx: 8
+ n_type: 0x0F
+ n_sect: 2
+ n_desc: 0
+ n_value: 8192
+ StringTable:
+ - ' '
+ - _main
+ - _dyldinfo_data
diff --git a/src/LibObjectFile.Tests/MachO/generate_files.sh b/src/LibObjectFile.Tests/MachO/generate_files.sh
new file mode 100755
index 0000000..9396bcd
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/generate_files.sh
@@ -0,0 +1,61 @@
+#!/bin/sh
+# Regenerates the Mach-O test fixtures. Needs an OSXCross toolchain on PATH
+# (o64-clang for x86_64, oa64-clang for arm64) so the fixtures come out of the
+# real cctools ld64 rather than an approximation, plus LLVM for yaml2obj and
+# ld64.lld. The toolchain is only needed to regenerate, not to run the tests,
+# since the fixtures are committed.
+#
+# OSXCross: https://github.com/tpoechtrager/osxcross
+# It builds the cctools/ld64 port and needs a macOS SDK packaged per its README;
+# Apple does not permit redistributing the SDK, which is why the fixtures are
+# committed rather than built during the test run.
+#
+# LLVM and current cctools both dropped 32-bit Mach-O linking, so the two i386
+# fixtures are synthesized with yaml2obj rather than linked. unixthread_i386
+# models a pre-10.8 executable, which uses LC_UNIXTHREAD for the entry point;
+# dyldinfo_i386 models a 10.7-era one, which pairs 32-bit segments with LC_MAIN
+# and LC_DYLD_INFO_ONLY. No linker still emits either combination.
+#
+# The committed fixtures were produced with LLVM 22.1.8 and the OSXCross
+# MacOSX14 SDK. Another version will not necessarily lay them out the same way,
+# so regenerating with one is expected to move offsets and will show up as a
+# diff in the byte-exact and snapshot tests. Check such a diff rather than
+# accepting it: the fixtures exist to pin what a real toolchain emits.
+set -e
+
+# Locate the SDK rather than assuming a version, so a toolchain built against a
+# different one still works.
+SDK="$(dirname "$(command -v oa64-clang)")/../SDK"
+SDK="$(ls -d "$SDK"/MacOSX*.sdk 2>/dev/null | sort -V | tail -1)"
+if [ -z "$SDK" ]; then
+ echo "No macOS SDK found next to oa64-clang; see the OSXCross README." >&2
+ exit 1
+fi
+
+o64-clang helloworld.c -o helloworld_x86_64
+oa64-clang helloworld.c -o helloworld_arm64
+
+# arm64 gets an ad-hoc signature from the linker, x86_64 stays unsigned. The
+# signing tests need both a "sign from scratch" and a "replace existing" input.
+o64-clang -dynamiclib -install_name /usr/local/lib/libhelloworld.dylib \
+ libhelloworld.c -o libhelloworld_x86_64.dylib
+
+o64-clang -c helloworld.c -o helloworld_x86_64.o
+
+lipo -create helloworld_x86_64 helloworld_arm64 -output helloworld_fat
+
+# cctools ld64 predates chained fixups, so this one fixture comes from lld,
+# which emits LC_DYLD_CHAINED_FIXUPS when the deployment target is macOS 12+.
+# That is a separate read path from the LC_DYLD_INFO_ONLY opcode streams above.
+oa64-clang -mmacosx-version-min=12.0 -c helloworld.c -o chained.o
+ld64.lld -arch arm64 -platform_version macos 12.0 12.0 -syslibroot "$SDK" \
+ -lSystem -e _main -fixup_chains -o chainedfixups_arm64 chained.o
+rm -f chained.o
+
+yaml2obj unixthread_i386.yaml -o unixthread_i386
+yaml2obj dyldinfo_i386.yaml -o dyldinfo_i386
+
+# A reference for the editing tests: what Apple's own tool produces for the same
+# edit. Comparing against it catches an encoding that is merely self-consistent.
+cp unixthread_i386 unixthread_i386_rpath
+install_name_tool -add_rpath '@executable_path/../Frameworks' unixthread_i386_rpath
diff --git a/src/LibObjectFile.Tests/MachO/helloworld.c b/src/LibObjectFile.Tests/MachO/helloworld.c
new file mode 100644
index 0000000..fe64797
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/helloworld.c
@@ -0,0 +1,14 @@
+// Fixture source for the Mach-O tests. It calls into libc on purpose: the
+// resulting lazy-binding stubs, indirect symbol table and bind opcodes are
+// structures the reader has to handle, and a freestanding binary has none.
+#include
+
+int helloworld_twice(int x) { return x * 2; }
+
+int helloworld_data = 21;
+
+int main(void)
+{
+ printf("hello world %d\n", helloworld_twice(helloworld_data));
+ return 0;
+}
diff --git a/src/LibObjectFile.Tests/MachO/helloworld_arm64 b/src/LibObjectFile.Tests/MachO/helloworld_arm64
new file mode 100755
index 0000000..b30ce94
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/helloworld_arm64 differ
diff --git a/src/LibObjectFile.Tests/MachO/helloworld_fat b/src/LibObjectFile.Tests/MachO/helloworld_fat
new file mode 100755
index 0000000..bcc3fb2
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/helloworld_fat differ
diff --git a/src/LibObjectFile.Tests/MachO/helloworld_x86_64 b/src/LibObjectFile.Tests/MachO/helloworld_x86_64
new file mode 100755
index 0000000..d6dfdc8
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/helloworld_x86_64 differ
diff --git a/src/LibObjectFile.Tests/MachO/helloworld_x86_64.o b/src/LibObjectFile.Tests/MachO/helloworld_x86_64.o
new file mode 100644
index 0000000..d1ab558
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/helloworld_x86_64.o differ
diff --git a/src/LibObjectFile.Tests/MachO/libhelloworld.c b/src/LibObjectFile.Tests/MachO/libhelloworld.c
new file mode 100644
index 0000000..0449b16
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/libhelloworld.c
@@ -0,0 +1,9 @@
+// Fixture source for the Mach-O dylib tests. See helloworld.c for why this
+// calls into libc rather than standing alone.
+#include
+
+int helloworld_shared(int x)
+{
+ printf("shared %d\n", x);
+ return x + 1;
+}
diff --git a/src/LibObjectFile.Tests/MachO/libhelloworld_x86_64.dylib b/src/LibObjectFile.Tests/MachO/libhelloworld_x86_64.dylib
new file mode 100755
index 0000000..b28d941
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/libhelloworld_x86_64.dylib differ
diff --git a/src/LibObjectFile.Tests/MachO/unixthread_i386 b/src/LibObjectFile.Tests/MachO/unixthread_i386
new file mode 100644
index 0000000..cce73c4
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/unixthread_i386 differ
diff --git a/src/LibObjectFile.Tests/MachO/unixthread_i386.yaml b/src/LibObjectFile.Tests/MachO/unixthread_i386.yaml
new file mode 100644
index 0000000..64d42da
--- /dev/null
+++ b/src/LibObjectFile.Tests/MachO/unixthread_i386.yaml
@@ -0,0 +1,147 @@
+# Models a pre-10.8 i386 executable: LC_UNIXTHREAD instead of LC_MAIN, and
+# padding between the end of the load commands and the first section so that
+# in-place load-command injection has somewhere to go. The first section sits
+# at 0x800 while the load commands end at 728.
+--- !mach-o
+FileHeader:
+ magic: 0xFEEDFACE
+ cputype: 0x00000007
+ cpusubtype: 0x00000003
+ filetype: 0x00000002
+ ncmds: 11
+ sizeofcmds: 700
+ flags: 0x00012085
+LoadCommands:
+ - cmd: LC_SEGMENT
+ cmdsize: 56
+ segname: __PAGEZERO
+ vmaddr: 0
+ vmsize: 4096
+ fileoff: 0
+ filesize: 0
+ maxprot: 0
+ initprot: 0
+ nsects: 0
+ flags: 4
+ - cmd: LC_SEGMENT
+ cmdsize: 124
+ segname: __TEXT
+ vmaddr: 4096
+ vmsize: 4096
+ fileoff: 0
+ filesize: 4096
+ maxprot: 7
+ initprot: 5
+ nsects: 1
+ flags: 0
+ Sections:
+ - sectname: __text
+ segname: __TEXT
+ addr: 0x1800
+ size: 16
+ offset: 0x800
+ align: 2
+ reloff: 0
+ nreloc: 0
+ flags: 0x80000400
+ reserved1: 0
+ reserved2: 0
+ content: 'B800000000C3'
+ - cmd: LC_SEGMENT
+ cmdsize: 124
+ segname: __DATA
+ vmaddr: 8192
+ vmsize: 4096
+ fileoff: 4096
+ filesize: 4096
+ maxprot: 7
+ initprot: 3
+ nsects: 1
+ flags: 0
+ Sections:
+ - sectname: __data
+ segname: __DATA
+ addr: 0x2000
+ size: 4
+ offset: 0x1000
+ align: 2
+ reloff: 0
+ nreloc: 0
+ flags: 0x00000000
+ reserved1: 0
+ reserved2: 0
+ content: '2A000000'
+ - cmd: LC_SEGMENT
+ cmdsize: 56
+ segname: __LINKEDIT
+ vmaddr: 12288
+ vmsize: 4096
+ fileoff: 8192
+ filesize: 56
+ maxprot: 7
+ initprot: 1
+ nsects: 0
+ flags: 0
+ - cmd: LC_SYMTAB
+ cmdsize: 24
+ symoff: 8192
+ nsyms: 2
+ stroff: 8216
+ strsize: 32
+ - cmd: LC_DYSYMTAB
+ cmdsize: 80
+ ilocalsym: 0
+ nlocalsym: 0
+ iextdefsym: 0
+ nextdefsym: 2
+ iundefsym: 2
+ nundefsym: 0
+ tocoff: 0
+ ntoc: 0
+ modtaboff: 0
+ nmodtab: 0
+ extrefsymoff: 0
+ nextrefsyms: 0
+ indirectsymoff: 0
+ nindirectsyms: 0
+ extreloff: 0
+ nextrel: 0
+ locreloff: 0
+ nlocrel: 0
+ - cmd: LC_LOAD_DYLINKER
+ cmdsize: 28
+ name: 12
+ Content: '/usr/lib/dyld'
+ ZeroPadBytes: 1
+ - cmd: LC_UUID
+ cmdsize: 24
+ uuid: 4B4F544F-5231-4D41-4300-000000000001
+ - cmd: LC_UNIXTHREAD
+ cmdsize: 80
+ PayloadBytes: [ 0x01, 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 ]
+ - cmd: LC_LOAD_DYLIB
+ cmdsize: 52
+ dylib:
+ name: 24
+ timestamp: 2
+ current_version: 0x0007D001
+ compatibility_version: 0x00010000
+ Content: '/usr/lib/libSystem.B.dylib'
+ ZeroPadBytes: 2
+ - cmd: LC_LOAD_DYLIB
+ cmdsize: 52
+ dylib:
+ name: 24
+ timestamp: 2
+ current_version: 0x00070000
+ compatibility_version: 0x00070000
+ Content: '/usr/lib/libstdc++.6.dylib'
+ ZeroPadBytes: 2
diff --git a/src/LibObjectFile.Tests/MachO/unixthread_i386_rpath b/src/LibObjectFile.Tests/MachO/unixthread_i386_rpath
new file mode 100644
index 0000000..586dbc9
Binary files /dev/null and b/src/LibObjectFile.Tests/MachO/unixthread_i386_rpath differ
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=chainedfixups_arm64.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=chainedfixups_arm64.verified.txt
new file mode 100644
index 0000000..c0b6a15
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=chainedfixups_arm64.verified.txt
@@ -0,0 +1,216 @@
+Mach header:
+ magic MH_MAGIC_64
+ cputype Arm64
+ cpusubtype 0x00000000
+ filetype Execute
+ ncmds 17
+ sizeofcmds 1192
+ flags 0x200085 NoUndefs, DyldLink, TwoLevel, PositionIndependent
+
+Load command 0
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __PAGEZERO
+ vmaddr 0x00000000
+ vmsize 0x100000000
+ fileoff 0
+ filesize 0
+ maxprot ---
+ initprot ---
+ nsects 0
+ flags 0x0
+Load command 1
+ cmd LC_SEGMENT_64
+ cmdsize 392
+ segname __TEXT
+ vmaddr 0x100000000
+ vmsize 0x00004000
+ fileoff 0
+ filesize 16384
+ maxprot r-x
+ initprot r-x
+ nsects 4
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x1000004e8
+ size 0x00000064
+ offset 1256
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __stubs
+ segname __TEXT
+ addr 0x10000054c
+ size 0x0000000c
+ offset 1356
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type SymbolStubs
+ attributes SomeInstructions, PureInstructions
+ reserved1 1
+ reserved2 12
+Section
+ sectname __cstring
+ segname __TEXT
+ addr 0x100000558
+ size 0x00000010
+ offset 1368
+ align 2^0 (1)
+ reloff 0
+ nreloc 0
+ type CStringLiterals
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Section
+ sectname __unwind_info
+ segname __TEXT
+ addr 0x100000568
+ size 0x0000103c
+ offset 1384
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 2
+ cmd LC_SEGMENT_64
+ cmdsize 152
+ segname __DATA_CONST
+ vmaddr 0x100004000
+ vmsize 0x00004000
+ fileoff 16384
+ filesize 16384
+ maxprot rw-
+ initprot rw-
+ nsects 1
+ flags 0x10
+Section
+ sectname __got
+ segname __DATA_CONST
+ addr 0x100004000
+ size 0x00000008
+ offset 16384
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type NonLazySymbolPointers
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 3
+ cmd LC_SEGMENT_64
+ cmdsize 152
+ segname __DATA
+ vmaddr 0x100008000
+ vmsize 0x00004000
+ fileoff 32768
+ filesize 16384
+ maxprot rw-
+ initprot rw-
+ nsects 1
+ flags 0x0
+Section
+ sectname __data
+ segname __DATA
+ addr 0x100008000
+ size 0x00000004
+ offset 32768
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 4
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __LINKEDIT
+ vmaddr 0x10000c000
+ vmsize 0x000003b0
+ fileoff 49152
+ filesize 944
+ maxprot r--
+ initprot r--
+ nsects 0
+ flags 0x0
+Load command 5
+ cmd LC_DYLD_CHAINED_FIXUPS
+ cmdsize 16
+ dataoff 49152
+ datasize 96
+Load command 6
+ cmd LC_DYLD_EXPORTS_TRIE
+ cmdsize 16
+ dataoff 49248
+ datasize 88
+Load command 7
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 49344
+ nsyms 6
+ stroff 49448
+ strsize 88
+Load command 8
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 0
+ nextdefsym 4
+ nundefsym 2
+ indirectsymoff 49440
+ nindirectsyms 2
+Load command 9
+ cmd LC_LOAD_DYLINKER
+ cmdsize 32
+ path /usr/lib/dyld
+Load command 10
+ cmd LC_UUID
+ cmdsize 24
+ uuid 4C4C4468-5555-3144-A159-D3B12CF35C25
+Load command 11
+ cmd LC_BUILD_VERSION
+ cmdsize 32
+ platform MacOS
+ minos 12.0.0
+ sdk 12.0.0
+ ntools 1
+ tool Lld
+ version 22.1.8
+Load command 12
+ cmd LC_MAIN
+ cmdsize 24
+ entryoff 1280
+ stacksize 0
+Load command 13
+ cmd LC_LOAD_DYLIB
+ cmdsize 56
+ name /usr/lib/libSystem.B.dylib
+ timestamp 0
+ current version 1336.61.1
+ compatibility version 1.0.0
+Load command 14
+ cmd LC_FUNCTION_STARTS
+ cmdsize 16
+ dataoff 49336
+ datasize 8
+Load command 15
+ cmd LC_DATA_IN_CODE
+ cmdsize 16
+ dataoff 49344
+ datasize 0
+Load command 16
+ cmd LC_CODE_SIGNATURE
+ cmdsize 16
+ dataoff 49536
+ datasize 560
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=dyldinfo_i386.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=dyldinfo_i386.verified.txt
new file mode 100644
index 0000000..4de3fc9
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=dyldinfo_i386.verified.txt
@@ -0,0 +1,153 @@
+Mach header:
+ magic MH_MAGIC
+ cputype X86
+ cpusubtype 0x00000003
+ filetype Execute
+ ncmds 16
+ sizeofcmds 720
+ flags 0x218085 NoUndefs, DyldLink, TwoLevel, WeakDefines, BindsToWeak, PositionIndependent
+
+Load command 0
+ cmd LC_SEGMENT
+ cmdsize 56
+ segname __PAGEZERO
+ vmaddr 0x00000000
+ vmsize 0x00001000
+ fileoff 0
+ filesize 0
+ maxprot ---
+ initprot ---
+ nsects 0
+ flags 0x0
+Load command 1
+ cmd LC_SEGMENT
+ cmdsize 124
+ segname __TEXT
+ vmaddr 0x00001000
+ vmsize 0x00001000
+ fileoff 0
+ filesize 4096
+ maxprot rwx
+ initprot r-x
+ nsects 1
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x00001800
+ size 0x00000006
+ offset 2048
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Load command 2
+ cmd LC_SEGMENT
+ cmdsize 124
+ segname __DATA
+ vmaddr 0x00002000
+ vmsize 0x00001000
+ fileoff 4096
+ filesize 4096
+ maxprot rwx
+ initprot rw-
+ nsects 1
+ flags 0x0
+Section
+ sectname __data
+ segname __DATA
+ addr 0x00002000
+ size 0x00000004
+ offset 4096
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 3
+ cmd LC_SEGMENT
+ cmdsize 56
+ segname __LINKEDIT
+ vmaddr 0x00003000
+ vmsize 0x00001000
+ fileoff 8192
+ filesize 64
+ maxprot rwx
+ initprot r--
+ nsects 0
+ flags 0x0
+Load command 4
+ cmd LC_DYLD_INFO_ONLY
+ cmdsize 48
+ rebase_off 0
+ rebase_size 0
+ bind_off 0
+ bind_size 0
+ lazy_bind_off 0
+ lazy_bind_size 0
+ export_off 8192
+ export_size 2
+Load command 5
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 8194
+ nsyms 2
+ stroff 8218
+ strsize 24
+Load command 6
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 0
+ nextdefsym 2
+ nundefsym 0
+ indirectsymoff 0
+ nindirectsyms 0
+Load command 7
+ cmd LC_LOAD_DYLINKER
+ cmdsize 28
+ path /usr/lib/dyld
+Load command 8
+ cmd LC_UUID
+ cmdsize 24
+ uuid 4D4F4445-524E-3332-4249-540000000002
+Load command 9
+ cmd LC_VERSION_MIN_MACOSX
+ cmdsize 16
+ version 10.7.0
+ sdk 10.7.0
+Load command 10
+ cmd LC_SOURCE_VERSION
+ cmdsize 16
+ version 0.0.0.0
+Load command 11
+ cmd LC_MAIN
+ cmdsize 24
+ entryoff 2048
+ stacksize 0
+Load command 12
+ cmd LC_FUNCTION_STARTS
+ cmdsize 16
+ dataoff 8248
+ datasize 8
+Load command 13
+ cmd LC_DATA_IN_CODE
+ cmdsize 16
+ dataoff 8256
+ datasize 0
+Load command 14
+ cmd LC_DYLIB_CODE_SIGN_DRS
+ cmdsize 16
+ dataoff 8256
+ datasize 0
+Load command 15
+ cmd LC_LOAD_DYLIB
+ cmdsize 52
+ name /usr/lib/libSystem.B.dylib
+ timestamp 2
+ current version 79.9.1
+ compatibility version 1.0.0
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_arm64.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_arm64.verified.txt
new file mode 100644
index 0000000..37bcde3
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_arm64.verified.txt
@@ -0,0 +1,247 @@
+Mach header:
+ magic MH_MAGIC_64
+ cputype Arm64
+ cpusubtype 0x00000000
+ filetype Execute
+ ncmds 17
+ sizeofcmds 1384
+ flags 0x200085 NoUndefs, DyldLink, TwoLevel, PositionIndependent
+
+Load command 0
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __PAGEZERO
+ vmaddr 0x00000000
+ vmsize 0x100000000
+ fileoff 0
+ filesize 0
+ maxprot ---
+ initprot ---
+ nsects 0
+ flags 0x0
+Load command 1
+ cmd LC_SEGMENT_64
+ cmdsize 472
+ segname __TEXT
+ vmaddr 0x100000000
+ vmsize 0x00004000
+ fileoff 0
+ filesize 16384
+ maxprot r-x
+ initprot r-x
+ nsects 5
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x100003f0c
+ size 0x00000064
+ offset 16140
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __stubs
+ segname __TEXT
+ addr 0x100003f70
+ size 0x0000000c
+ offset 16240
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type SymbolStubs
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 12
+Section
+ sectname __stub_helper
+ segname __TEXT
+ addr 0x100003f7c
+ size 0x00000024
+ offset 16252
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __cstring
+ segname __TEXT
+ addr 0x100003fa0
+ size 0x00000010
+ offset 16288
+ align 2^0 (1)
+ reloff 0
+ nreloc 0
+ type CStringLiterals
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Section
+ sectname __unwind_info
+ segname __TEXT
+ addr 0x100003fb0
+ size 0x00000050
+ offset 16304
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 2
+ cmd LC_SEGMENT_64
+ cmdsize 152
+ segname __DATA_CONST
+ vmaddr 0x100004000
+ vmsize 0x00004000
+ fileoff 16384
+ filesize 16384
+ maxprot rw-
+ initprot rw-
+ nsects 1
+ flags 0x10
+Section
+ sectname __got
+ segname __DATA_CONST
+ addr 0x100004000
+ size 0x00000008
+ offset 16384
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type NonLazySymbolPointers
+ attributes (none)
+ reserved1 1
+ reserved2 0
+Load command 3
+ cmd LC_SEGMENT_64
+ cmdsize 232
+ segname __DATA
+ vmaddr 0x100008000
+ vmsize 0x00004000
+ fileoff 32768
+ filesize 16384
+ maxprot rw-
+ initprot rw-
+ nsects 2
+ flags 0x0
+Section
+ sectname __la_symbol_ptr
+ segname __DATA
+ addr 0x100008000
+ size 0x00000008
+ offset 32768
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type LazySymbolPointers
+ attributes (none)
+ reserved1 2
+ reserved2 0
+Section
+ sectname __data
+ segname __DATA
+ addr 0x100008008
+ size 0x0000000c
+ offset 32776
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 4
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __LINKEDIT
+ vmaddr 0x10000c000
+ vmsize 0x00004000
+ fileoff 49152
+ filesize 925
+ maxprot r--
+ initprot r--
+ nsects 0
+ flags 0x0
+Load command 5
+ cmd LC_DYLD_INFO_ONLY
+ cmdsize 48
+ rebase_off 49152
+ rebase_size 8
+ bind_off 49160
+ bind_size 24
+ lazy_bind_off 49184
+ lazy_bind_size 16
+ export_off 49200
+ export_size 88
+Load command 6
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 49296
+ nsyms 7
+ stroff 49424
+ strsize 104
+Load command 7
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 1
+ nextdefsym 4
+ nundefsym 2
+ indirectsymoff 49408
+ nindirectsyms 3
+Load command 8
+ cmd LC_LOAD_DYLINKER
+ cmdsize 32
+ path /usr/lib/dyld
+Load command 9
+ cmd LC_UUID
+ cmdsize 24
+ uuid CCF0B44A-CF74-3987-962B-B2EB1FD51B9A
+Load command 10
+ cmd LC_BUILD_VERSION
+ cmdsize 32
+ platform MacOS
+ minos 11.0.0
+ sdk 14.2.0
+ ntools 1
+ tool Ld
+ version 711.0.0
+Load command 11
+ cmd LC_SOURCE_VERSION
+ cmdsize 16
+ version 0.0.0.0
+Load command 12
+ cmd LC_MAIN
+ cmdsize 24
+ entryoff 16164
+ stacksize 0
+Load command 13
+ cmd LC_LOAD_DYLIB
+ cmdsize 56
+ name /usr/lib/libSystem.B.dylib
+ timestamp 2
+ current version 1336.61.1
+ compatibility version 1.0.0
+Load command 14
+ cmd LC_FUNCTION_STARTS
+ cmdsize 16
+ dataoff 49288
+ datasize 8
+Load command 15
+ cmd LC_DATA_IN_CODE
+ cmdsize 16
+ dataoff 49296
+ datasize 0
+Load command 16
+ cmd LC_CODE_SIGNATURE
+ cmdsize 16
+ dataoff 49536
+ datasize 541
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_x86_64.o.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_x86_64.o.verified.txt
new file mode 100644
index 0000000..57766ec
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_x86_64.o.verified.txt
@@ -0,0 +1,106 @@
+Mach header:
+ magic MH_MAGIC_64
+ cputype X86_64
+ cpusubtype 0x00000003
+ filetype Object
+ ncmds 4
+ sizeofcmds 592
+ flags 0x2000 SubsectionsViaSymbols
+
+Load command 0
+ cmd LC_SEGMENT_64
+ cmdsize 472
+ segname
+ vmaddr 0x00000000
+ vmsize 0x00000100
+ fileoff 624
+ filesize 256
+ maxprot rwx
+ initprot rwx
+ nsects 5
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x00000000
+ size 0x00000042
+ offset 624
+ align 2^4 (16)
+ reloff 880
+ nreloc 4
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __data
+ segname __DATA
+ addr 0x00000044
+ size 0x00000004
+ offset 692
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Section
+ sectname __cstring
+ segname __TEXT
+ addr 0x00000048
+ size 0x00000010
+ offset 696
+ align 2^0 (1)
+ reloff 0
+ nreloc 0
+ type CStringLiterals
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Section
+ sectname __compact_unwind
+ segname __LD
+ addr 0x00000058
+ size 0x00000040
+ offset 712
+ align 2^3 (8)
+ reloff 912
+ nreloc 2
+ type Regular
+ attributes Debug
+ reserved1 0
+ reserved2 0
+Section
+ sectname __eh_frame
+ segname __TEXT
+ addr 0x00000098
+ size 0x00000068
+ offset 776
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type Coalesced
+ attributes LiveSupport, StripStaticSyms, NoToc
+ reserved1 0
+ reserved2 0
+Load command 1
+ cmd LC_VERSION_MIN_MACOSX
+ cmdsize 16
+ version 10.13.0
+ sdk 14.2.0
+Load command 2
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 928
+ nsyms 4
+ stroff 992
+ strsize 56
+Load command 3
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 0
+ nextdefsym 3
+ nundefsym 1
+ indirectsymoff 0
+ nindirectsyms 0
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_x86_64.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_x86_64.verified.txt
new file mode 100644
index 0000000..2e8b939
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=helloworld_x86_64.verified.txt
@@ -0,0 +1,239 @@
+Mach header:
+ magic MH_MAGIC_64
+ cputype X86_64
+ cpusubtype 0x00000003
+ filetype Execute
+ ncmds 15
+ sizeofcmds 1360
+ flags 0x200085 NoUndefs, DyldLink, TwoLevel, PositionIndependent
+
+Load command 0
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __PAGEZERO
+ vmaddr 0x00000000
+ vmsize 0x100000000
+ fileoff 0
+ filesize 0
+ maxprot ---
+ initprot ---
+ nsects 0
+ flags 0x0
+Load command 1
+ cmd LC_SEGMENT_64
+ cmdsize 472
+ segname __TEXT
+ vmaddr 0x100000000
+ vmsize 0x00001000
+ fileoff 0
+ filesize 4096
+ maxprot r-x
+ initprot r-x
+ nsects 5
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x100000f40
+ size 0x00000042
+ offset 3904
+ align 2^4 (16)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __stubs
+ segname __TEXT
+ addr 0x100000f82
+ size 0x00000006
+ offset 3970
+ align 2^1 (2)
+ reloff 0
+ nreloc 0
+ type SymbolStubs
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 6
+Section
+ sectname __stub_helper
+ segname __TEXT
+ addr 0x100000f88
+ size 0x0000001a
+ offset 3976
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __cstring
+ segname __TEXT
+ addr 0x100000fa2
+ size 0x00000010
+ offset 4002
+ align 2^0 (1)
+ reloff 0
+ nreloc 0
+ type CStringLiterals
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Section
+ sectname __unwind_info
+ segname __TEXT
+ addr 0x100000fb4
+ size 0x00000048
+ offset 4020
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 2
+ cmd LC_SEGMENT_64
+ cmdsize 392
+ segname __DATA
+ vmaddr 0x100001000
+ vmsize 0x00001000
+ fileoff 4096
+ filesize 4096
+ maxprot rw-
+ initprot rw-
+ nsects 4
+ flags 0x0
+Section
+ sectname __nl_symbol_ptr
+ segname __DATA
+ addr 0x100001000
+ size 0x00000008
+ offset 4096
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type NonLazySymbolPointers
+ attributes (none)
+ reserved1 1
+ reserved2 0
+Section
+ sectname __got
+ segname __DATA
+ addr 0x100001008
+ size 0x00000008
+ offset 4104
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type NonLazySymbolPointers
+ attributes (none)
+ reserved1 2
+ reserved2 0
+Section
+ sectname __la_symbol_ptr
+ segname __DATA
+ addr 0x100001010
+ size 0x00000008
+ offset 4112
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type LazySymbolPointers
+ attributes (none)
+ reserved1 3
+ reserved2 0
+Section
+ sectname __data
+ segname __DATA
+ addr 0x100001018
+ size 0x00000004
+ offset 4120
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 3
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __LINKEDIT
+ vmaddr 0x100002000
+ vmsize 0x00001000
+ fileoff 8192
+ filesize 336
+ maxprot r--
+ initprot r--
+ nsects 0
+ flags 0x0
+Load command 4
+ cmd LC_DYLD_INFO_ONLY
+ cmdsize 48
+ rebase_off 8192
+ rebase_size 8
+ bind_off 8200
+ bind_size 24
+ lazy_bind_off 8224
+ lazy_bind_size 16
+ export_off 8240
+ export_size 80
+Load command 5
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 8328
+ nsyms 6
+ stroff 8440
+ strsize 88
+Load command 6
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 0
+ nextdefsym 4
+ nundefsym 2
+ indirectsymoff 8424
+ nindirectsyms 4
+Load command 7
+ cmd LC_LOAD_DYLINKER
+ cmdsize 32
+ path /usr/lib/dyld
+Load command 8
+ cmd LC_UUID
+ cmdsize 24
+ uuid 0DDA3E66-624D-3432-9527-F96F79E505F5
+Load command 9
+ cmd LC_VERSION_MIN_MACOSX
+ cmdsize 16
+ version 10.13.0
+ sdk 14.2.0
+Load command 10
+ cmd LC_SOURCE_VERSION
+ cmdsize 16
+ version 0.0.0.0
+Load command 11
+ cmd LC_MAIN
+ cmdsize 24
+ entryoff 3920
+ stacksize 0
+Load command 12
+ cmd LC_LOAD_DYLIB
+ cmdsize 56
+ name /usr/lib/libSystem.B.dylib
+ timestamp 2
+ current version 1336.61.1
+ compatibility version 1.0.0
+Load command 13
+ cmd LC_FUNCTION_STARTS
+ cmdsize 16
+ dataoff 8320
+ datasize 8
+Load command 14
+ cmd LC_DATA_IN_CODE
+ cmdsize 16
+ dataoff 8328
+ datasize 0
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=libhelloworld_x86_64.dylib.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=libhelloworld_x86_64.dylib.verified.txt
new file mode 100644
index 0000000..42a0382
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=libhelloworld_x86_64.dylib.verified.txt
@@ -0,0 +1,212 @@
+Mach header:
+ magic MH_MAGIC_64
+ cputype X86_64
+ cpusubtype 0x00000003
+ filetype Dylib
+ ncmds 13
+ sizeofcmds 1216
+ flags 0x100085 NoUndefs, DyldLink, TwoLevel, NoReexportedDylibs
+
+Load command 0
+ cmd LC_SEGMENT_64
+ cmdsize 472
+ segname __TEXT
+ vmaddr 0x00000000
+ vmsize 0x00001000
+ fileoff 0
+ filesize 4096
+ maxprot r-x
+ initprot r-x
+ nsects 5
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x00000f60
+ size 0x00000028
+ offset 3936
+ align 2^4 (16)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __stubs
+ segname __TEXT
+ addr 0x00000f88
+ size 0x00000006
+ offset 3976
+ align 2^1 (2)
+ reloff 0
+ nreloc 0
+ type SymbolStubs
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 6
+Section
+ sectname __stub_helper
+ segname __TEXT
+ addr 0x00000f90
+ size 0x0000001a
+ offset 3984
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Section
+ sectname __cstring
+ segname __TEXT
+ addr 0x00000faa
+ size 0x0000000b
+ offset 4010
+ align 2^0 (1)
+ reloff 0
+ nreloc 0
+ type CStringLiterals
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Section
+ sectname __unwind_info
+ segname __TEXT
+ addr 0x00000fb8
+ size 0x00000048
+ offset 4024
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 1
+ cmd LC_SEGMENT_64
+ cmdsize 312
+ segname __DATA
+ vmaddr 0x00001000
+ vmsize 0x00001000
+ fileoff 4096
+ filesize 4096
+ maxprot rw-
+ initprot rw-
+ nsects 3
+ flags 0x0
+Section
+ sectname __nl_symbol_ptr
+ segname __DATA
+ addr 0x00001000
+ size 0x00000008
+ offset 4096
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type NonLazySymbolPointers
+ attributes (none)
+ reserved1 1
+ reserved2 0
+Section
+ sectname __got
+ segname __DATA
+ addr 0x00001008
+ size 0x00000008
+ offset 4104
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type NonLazySymbolPointers
+ attributes (none)
+ reserved1 2
+ reserved2 0
+Section
+ sectname __la_symbol_ptr
+ segname __DATA
+ addr 0x00001010
+ size 0x00000008
+ offset 4112
+ align 2^3 (8)
+ reloff 0
+ nreloc 0
+ type LazySymbolPointers
+ attributes (none)
+ reserved1 3
+ reserved2 0
+Load command 2
+ cmd LC_SEGMENT_64
+ cmdsize 72
+ segname __LINKEDIT
+ vmaddr 0x00002000
+ vmsize 0x00001000
+ fileoff 8192
+ filesize 200
+ maxprot r--
+ initprot r--
+ nsects 0
+ flags 0x0
+Load command 3
+ cmd LC_ID_DYLIB
+ cmdsize 64
+ name /usr/local/lib/libhelloworld.dylib
+ timestamp 1
+ current version 0.0.0
+ compatibility version 0.0.0
+Load command 4
+ cmd LC_DYLD_INFO_ONLY
+ cmdsize 48
+ rebase_off 8192
+ rebase_size 8
+ bind_off 8200
+ bind_size 24
+ lazy_bind_off 8224
+ lazy_bind_size 16
+ export_off 8240
+ export_size 32
+Load command 5
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 8280
+ nsyms 3
+ stroff 8344
+ strsize 48
+Load command 6
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 0
+ nextdefsym 1
+ nundefsym 2
+ indirectsymoff 8328
+ nindirectsyms 4
+Load command 7
+ cmd LC_UUID
+ cmdsize 24
+ uuid 81A0F075-659E-3944-9A81-0B05568D0635
+Load command 8
+ cmd LC_VERSION_MIN_MACOSX
+ cmdsize 16
+ version 10.13.0
+ sdk 14.2.0
+Load command 9
+ cmd LC_SOURCE_VERSION
+ cmdsize 16
+ version 0.0.0.0
+Load command 10
+ cmd LC_LOAD_DYLIB
+ cmdsize 56
+ name /usr/lib/libSystem.B.dylib
+ timestamp 2
+ current version 1336.61.1
+ compatibility version 1.0.0
+Load command 11
+ cmd LC_FUNCTION_STARTS
+ cmdsize 16
+ dataoff 8272
+ datasize 8
+Load command 12
+ cmd LC_DATA_IN_CODE
+ cmdsize 16
+ dataoff 8280
+ datasize 0
diff --git a/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=unixthread_i386.verified.txt b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=unixthread_i386.verified.txt
new file mode 100644
index 0000000..bfbb83a
--- /dev/null
+++ b/src/LibObjectFile.Tests/Verified/MachOSimpleTests.Prints_name=unixthread_i386.verified.txt
@@ -0,0 +1,125 @@
+Mach header:
+ magic MH_MAGIC
+ cputype X86
+ cpusubtype 0x00000003
+ filetype Execute
+ ncmds 11
+ sizeofcmds 700
+ flags 0x12085 NoUndefs, DyldLink, TwoLevel, SubsectionsViaSymbols, BindsToWeak
+
+Load command 0
+ cmd LC_SEGMENT
+ cmdsize 56
+ segname __PAGEZERO
+ vmaddr 0x00000000
+ vmsize 0x00001000
+ fileoff 0
+ filesize 0
+ maxprot ---
+ initprot ---
+ nsects 0
+ flags 0x4
+Load command 1
+ cmd LC_SEGMENT
+ cmdsize 124
+ segname __TEXT
+ vmaddr 0x00001000
+ vmsize 0x00001000
+ fileoff 0
+ filesize 4096
+ maxprot rwx
+ initprot r-x
+ nsects 1
+ flags 0x0
+Section
+ sectname __text
+ segname __TEXT
+ addr 0x00001800
+ size 0x00000010
+ offset 2048
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes SomeInstructions, PureInstructions
+ reserved1 0
+ reserved2 0
+Load command 2
+ cmd LC_SEGMENT
+ cmdsize 124
+ segname __DATA
+ vmaddr 0x00002000
+ vmsize 0x00001000
+ fileoff 4096
+ filesize 4096
+ maxprot rwx
+ initprot rw-
+ nsects 1
+ flags 0x0
+Section
+ sectname __data
+ segname __DATA
+ addr 0x00002000
+ size 0x00000004
+ offset 4096
+ align 2^2 (4)
+ reloff 0
+ nreloc 0
+ type Regular
+ attributes (none)
+ reserved1 0
+ reserved2 0
+Load command 3
+ cmd LC_SEGMENT
+ cmdsize 56
+ segname __LINKEDIT
+ vmaddr 0x00003000
+ vmsize 0x00001000
+ fileoff 8192
+ filesize 56
+ maxprot rwx
+ initprot r--
+ nsects 0
+ flags 0x0
+Load command 4
+ cmd LC_SYMTAB
+ cmdsize 24
+ symoff 8192
+ nsyms 2
+ stroff 8216
+ strsize 32
+Load command 5
+ cmd LC_DYSYMTAB
+ cmdsize 80
+ nlocalsym 0
+ nextdefsym 2
+ nundefsym 0
+ indirectsymoff 0
+ nindirectsyms 0
+Load command 6
+ cmd LC_LOAD_DYLINKER
+ cmdsize 28
+ path /usr/lib/dyld
+Load command 7
+ cmd LC_UUID
+ cmdsize 24
+ uuid 4B4F544F-5231-4D41-4300-000000000001
+Load command 8
+ cmd LC_UNIXTHREAD
+ cmdsize 80
+ flavor 1
+ count 16
+Load command 9
+ cmd LC_LOAD_DYLIB
+ cmdsize 52
+ name /usr/lib/libSystem.B.dylib
+ timestamp 2
+ current version 7.208.1
+ compatibility version 1.0.0
+Load command 10
+ cmd LC_LOAD_DYLIB
+ cmdsize 52
+ name /usr/lib/libstdc++.6.dylib
+ timestamp 2
+ current version 7.0.0
+ compatibility version 7.0.0
diff --git a/src/LibObjectFile/Diagnostics/DiagnosticId.cs b/src/LibObjectFile/Diagnostics/DiagnosticId.cs
index 352a183..8563691 100644
--- a/src/LibObjectFile/Diagnostics/DiagnosticId.cs
+++ b/src/LibObjectFile/Diagnostics/DiagnosticId.cs
@@ -213,4 +213,32 @@ public enum DiagnosticId
PE_ERR_InvalidResourceDirectoryEntry = 4001,
PE_ERR_InvalidResourceDirectoryEntryRVAOffsetToData = 4002,
PE_ERR_InvalidResourceString = 4003,
+
+ // Mach-O
+ MACHO_ERR_InvalidMagic = 5000,
+ MACHO_ERR_UnsupportedByteOrder = 5001,
+ MACHO_ERR_InvalidLoadCommandSize = 5002,
+ MACHO_ERR_TruncatedLoadCommand = 5003,
+ MACHO_ERR_InvalidImageBitness = 5004,
+ MACHO_ERR_InvalidContentFileRange = 5005,
+ MACHO_ERR_InvalidFatHeader = 5006,
+ MACHO_ERR_InvalidFatArchRange = 5007,
+ MACHO_ERR_NoRoomForLoadCommands = 5008,
+ MACHO_ERR_ValueTooLargeFor32Bit = 5009,
+ MACHO_ERR_UnexpectedFatFile = 5010,
+ MACHO_ERR_DataOutsideImage = 5011,
+ MACHO_ERR_ContentNotContiguous = 5012,
+ MACHO_ERR_SectionAddressMismatch = 5013,
+ MACHO_ERR_InvalidCommandAlignment = 5014,
+ MACHO_ERR_SectionOutsideSegment = 5015,
+ MACHO_ERR_StaleCodeSignature = 5016,
+ MACHO_ERR_SectionContentMismatch = 5017,
+ MACHO_ERR_LoadCommandTableSizeMismatch = 5018,
+ MACHO_ERR_LoadCommandOverread = 5019,
+ MACHO_ERR_InvalidFatSliceAlignment = 5020,
+ MACHO_ERR_OverlappingFatSlices = 5021,
+ MACHO_ERR_DuplicateFatSlice = 5022,
+ MACHO_ERR_MissingFatSliceImage = 5023,
+ MACHO_ERR_UnexpectedEndOfStream = 5024,
+ MACHO_ERR_UnexpectedArchive = 5025,
}
\ No newline at end of file
diff --git a/src/LibObjectFile/MachO/CodeSign/MachOAdHocSignatureBuilder.cs b/src/LibObjectFile/MachO/CodeSign/MachOAdHocSignatureBuilder.cs
new file mode 100644
index 0000000..d35dac1
--- /dev/null
+++ b/src/LibObjectFile/MachO/CodeSign/MachOAdHocSignatureBuilder.cs
@@ -0,0 +1,225 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Buffers.Binary;
+using System.Security.Cryptography;
+using System.Text;
+using LibObjectFile.Utils;
+
+namespace LibObjectFile.MachO.CodeSign;
+
+using static MachOCodeSignatureConstants;
+
+///
+/// Builds an ad-hoc embedded code signature.
+///
+///
+/// An ad-hoc signature carries no certificate. It states only that the image hashes to what the
+/// code directory says, which is what lets the kernel give the process a stable identity. Apple
+/// Silicon refuses to execute an unsigned image at all, so this is a requirement there rather
+/// than a hardening measure.
+///
+public sealed class MachOAdHocSignatureBuilder
+{
+ private readonly byte[] _identifier;
+
+ ///
+ /// Initializes a builder for an image identified by .
+ ///
+ ///
+ /// The signing identity recorded in the code directory. Apple's tooling uses the file name
+ /// of the binary, and any process asking for this image's identity gets this string back.
+ ///
+ /// is null.
+ /// is empty.
+ public MachOAdHocSignatureBuilder(string identifier)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(identifier);
+
+ Identifier = identifier;
+ var bytes = Encoding.UTF8.GetBytes(identifier);
+ _identifier = new byte[bytes.Length + 1];
+ bytes.CopyTo(_identifier, 0);
+ }
+
+ ///
+ /// Gets the signing identity recorded in the code directory.
+ ///
+ public string Identifier { get; }
+
+ ///
+ /// Gets or sets the number of bytes of the image covered by the signature, which is the file
+ /// offset the signature itself starts at.
+ ///
+ public uint CodeLimit { get; set; }
+
+ ///
+ /// Gets or sets the file offset of the executable segment, normally __TEXT.
+ ///
+ public ulong ExecSegmentBase { get; set; }
+
+ ///
+ /// Gets or sets the file size of the executable segment.
+ ///
+ public ulong ExecSegmentLimit { get; set; }
+
+ ///
+ /// Gets or sets the executable segment flags, which mark whether the image is a main binary.
+ ///
+ public ulong ExecSegmentFlags { get; set; }
+
+ ///
+ /// Gets the number of page hashes the code directory will hold for the current .
+ ///
+ public uint CodeSlotCount => (CodeLimit + PageSize - 1) / PageSize;
+
+ ///
+ /// Gets the total size of the signature for the current and identifier.
+ ///
+ ///
+ /// The size depends only on those two, so a caller can reserve room for the signature and
+ /// lay out the image before any hash has been computed.
+ ///
+ public uint ComputeSize()
+ => AlignHelper.AlignUp((uint)ContentSize, (uint)SignatureAlignment);
+
+ ///
+ /// The size of the blobs themselves, which is what the SuperBlob header records. The room
+ /// reserved for the signature is rounded up from this, and the padding is not part of the
+ /// SuperBlob: codesign reports the length field as the blob's own, and Apple's ad-hoc output
+ /// leaves the slack to the load command's datasize.
+ ///
+ private int ContentSize => SuperBlobSize + CodeDirectorySize + RequirementsSize + BlobWrapperSize;
+
+ // Header plus one index entry for each of the code directory, requirements and CMS slots.
+ private const int SuperBlobSize = 12 + 3 * 8;
+ private const int RequirementsSize = 12;
+ private const int BlobWrapperSize = 8;
+
+ // Two special slots are described: the Info.plist hash and the requirements hash.
+ private const int SpecialSlotCount = 2;
+
+ private int HashOffset => CodeDirectoryHeaderSize + _identifier.Length + SpecialSlotCount * Sha256Size;
+
+ private int CodeDirectorySize => HashOffset + (int)CodeSlotCount * Sha256Size;
+
+ ///
+ /// Produces the signature for an image whose first bytes are
+ /// .
+ ///
+ ///
+ /// The image content up to . The signature is not part of what it
+ /// covers, so this excludes the signature itself.
+ ///
+ /// The signature, of exactly bytes.
+ /// is shorter than .
+ public byte[] Build(ReadOnlySpan image)
+ {
+ if (image.Length < CodeLimit)
+ {
+ throw new ArgumentException($"The image is {image.Length} bytes but the signature covers {CodeLimit}", nameof(image));
+ }
+
+ var result = new byte[ComputeSize()];
+ var span = result.AsSpan();
+
+ var codeDirectoryOffset = SuperBlobSize;
+ var requirementsOffset = codeDirectoryOffset + CodeDirectorySize;
+ var blobWrapperOffset = requirementsOffset + RequirementsSize;
+
+ WriteRequirements(span.Slice(requirementsOffset, RequirementsSize));
+ WriteBlobWrapper(span.Slice(blobWrapperOffset, BlobWrapperSize));
+ WriteCodeDirectory(span.Slice(codeDirectoryOffset, CodeDirectorySize), image, span.Slice(requirementsOffset, RequirementsSize));
+ WriteSuperBlob(span, codeDirectoryOffset, requirementsOffset, blobWrapperOffset, ContentSize);
+
+ return result;
+ }
+
+ private static void WriteSuperBlob(Span span, int codeDirectoryOffset, int requirementsOffset, int blobWrapperOffset, int totalLength)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(span, EmbeddedSignatureMagic);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(4), (uint)totalLength);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(8), 3);
+
+ WriteIndexEntry(span.Slice(12), CodeDirectorySlot, codeDirectoryOffset);
+ WriteIndexEntry(span.Slice(20), RequirementsSlot, requirementsOffset);
+ WriteIndexEntry(span.Slice(28), SignatureSlot, blobWrapperOffset);
+ }
+
+ private static void WriteIndexEntry(Span span, uint slot, int offset)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(span, slot);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(4), (uint)offset);
+ }
+
+ ///
+ /// Writes an empty requirement set. Ad-hoc signing states no requirements, but the slot is
+ /// still present and hashed, because its absence is not the same as it being empty.
+ ///
+ private static void WriteRequirements(Span span)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(span, RequirementsMagic);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(4), RequirementsSize);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(8), 0);
+ }
+
+ ///
+ /// Writes the empty wrapper that would hold a CMS signature.
+ ///
+ ///
+ /// The slot is present and empty rather than omitted, which is what codesign produces
+ /// when it signs ad-hoc: a shipping ad-hoc signed dylib has exactly these three blobs, a code
+ /// directory, a twelve byte empty requirement set and this eight byte wrapper. A linker
+ /// writes something smaller still, a lone code directory flagged LINKER_SIGNED, but
+ /// that is a different thing from what signing a finished image produces.
+ ///
+ private static void WriteBlobWrapper(Span span)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(span, BlobWrapperMagic);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(4), BlobWrapperSize);
+ }
+
+ private void WriteCodeDirectory(Span span, ReadOnlySpan image, ReadOnlySpan requirements)
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(span, CodeDirectoryMagic);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(4), (uint)CodeDirectorySize);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(8), CodeDirectoryVersionExecSeg);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(12), AdHocFlag);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(16), (uint)HashOffset);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(20), CodeDirectoryHeaderSize);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(24), SpecialSlotCount);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(28), CodeSlotCount);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(32), CodeLimit);
+ span[36] = Sha256Size;
+ span[37] = HashTypeSha256;
+ span[38] = 0;
+ span[39] = PageSizeLog2;
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(40), 0);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(44), 0);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(48), 0);
+ BinaryPrimitives.WriteUInt32BigEndian(span.Slice(52), 0);
+ BinaryPrimitives.WriteUInt64BigEndian(span.Slice(56), 0);
+ BinaryPrimitives.WriteUInt64BigEndian(span.Slice(64), ExecSegmentBase);
+ BinaryPrimitives.WriteUInt64BigEndian(span.Slice(72), ExecSegmentLimit);
+ BinaryPrimitives.WriteUInt64BigEndian(span.Slice(80), ExecSegmentFlags);
+
+ _identifier.CopyTo(span.Slice(CodeDirectoryHeaderSize));
+
+ // Special slots are indexed backwards from the code hashes, so slot n sits n hashes
+ // before the start of the code hashes. Slot 1 is the Info.plist, which a bare executable
+ // does not have, and a slot with nothing in it is recorded as zero rather than omitted.
+ var hashOffset = HashOffset;
+ span.Slice(hashOffset - SpecialSlotCount * Sha256Size, SpecialSlotCount * Sha256Size).Clear();
+ SHA256.HashData(requirements, span.Slice(hashOffset - (int)RequirementsSlot * Sha256Size, Sha256Size));
+
+ for (var slot = 0; slot < CodeSlotCount; slot++)
+ {
+ var start = slot * PageSize;
+ var length = Math.Min(PageSize, (int)CodeLimit - start);
+ SHA256.HashData(image.Slice(start, length), span.Slice(hashOffset + slot * Sha256Size, Sha256Size));
+ }
+ }
+
+}
diff --git a/src/LibObjectFile/MachO/CodeSign/MachOCodeSignatureConstants.cs b/src/LibObjectFile/MachO/CodeSign/MachOCodeSignatureConstants.cs
new file mode 100644
index 0000000..86c8f8a
--- /dev/null
+++ b/src/LibObjectFile/MachO/CodeSign/MachOCodeSignatureConstants.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO.CodeSign;
+
+///
+/// Constants of the embedded code signature format.
+///
+///
+/// Every structure in a code signature is stored big-endian, unlike the rest of a Mach-O image,
+/// because the format predates Apple's move to little-endian hardware and was never changed.
+///
+public static class MachOCodeSignatureConstants
+{
+ /// Magic of the outer blob holding all the others (CSMAGIC_EMBEDDED_SIGNATURE).
+ public const uint EmbeddedSignatureMagic = 0xfade0cc0;
+
+ /// Magic of a code directory (CSMAGIC_CODEDIRECTORY).
+ public const uint CodeDirectoryMagic = 0xfade0c02;
+
+ /// Magic of a requirement set (CSMAGIC_REQUIREMENTS).
+ public const uint RequirementsMagic = 0xfade0c01;
+
+ /// Magic of the blob wrapping a CMS signature (CSMAGIC_BLOBWRAPPER).
+ public const uint BlobWrapperMagic = 0xfade0b01;
+
+ /// Slot of the code directory (CSSLOT_CODEDIRECTORY).
+ public const uint CodeDirectorySlot = 0;
+
+ /// Slot of the bundle's Info.plist hash (CSSLOT_INFOSLOT).
+ public const uint InfoSlot = 1;
+
+ /// Slot of the requirement set (CSSLOT_REQUIREMENTS).
+ public const uint RequirementsSlot = 2;
+
+ /// Slot of the CMS signature (CSSLOT_SIGNATURESLOT).
+ public const uint SignatureSlot = 0x10000;
+
+ /// Code directory version understanding the executable segment fields (CS_SUPPORTSEXECSEG).
+ public const uint CodeDirectoryVersionExecSeg = 0x20400;
+
+ /// Size of the fixed part of a code directory.
+ public const int CodeDirectoryHeaderSize = 88;
+
+ /// The signature carries no CMS signer and is trusted only by its own hashes (CS_ADHOC).
+ public const uint AdHocFlag = 0x0002;
+
+ /// SHA-256 code hashes (CS_HASHTYPE_SHA256).
+ public const byte HashTypeSha256 = 2;
+
+ /// Size in bytes of a SHA-256 hash.
+ public const int Sha256Size = 32;
+
+ /// The signed pages are 4 KB, stored as the log2 of the size.
+ public const byte PageSizeLog2 = 12;
+
+ /// Size of a signed page, derived from .
+ public const int PageSize = 1 << PageSizeLog2;
+
+ /// The executable segment belongs to a main binary rather than a library (CS_EXECSEG_MAIN_BINARY).
+ public const ulong ExecSegMainBinary = 0x1;
+
+ /// Alignment the signature blob starts at inside __LINKEDIT.
+ public const int SignatureAlignment = 16;
+}
diff --git a/src/LibObjectFile/MachO/Content/MachOContent.cs b/src/LibObjectFile/MachO/Content/MachOContent.cs
new file mode 100644
index 0000000..c3c266d
--- /dev/null
+++ b/src/LibObjectFile/MachO/Content/MachOContent.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A run of bytes in a Mach-O image, holding its place in the file.
+///
+///
+/// Everything in an image is one of these, in one ordered list: the header, the load command
+/// table, the padding after it, each section's bytes, each table in __LINKEDIT and any
+/// alignment padding between them. Nothing is left implicit, so writing the list back out
+/// reproduces the file and a layout is a single walk over it.
+///
+/// is where Mach-O parts company with ELF. A section's address is
+/// its segment's address plus its distance from the segment's file offset, so moving a section
+/// in the file moves it in memory and invalidates everything referring to it. Content carrying
+/// an address is therefore pinned and a layout leaves it alone; only what nothing addresses,
+/// which in practice is __LINKEDIT and the file tail, is free to move.
+///
+///
+public abstract class MachOContent : MachOObject
+{
+ ///
+ /// Initializes a new instance.
+ ///
+ protected MachOContent()
+ {
+ FileAlignment = 1;
+ }
+
+ ///
+ /// Gets or sets the alignment this content requires in the file.
+ ///
+ public uint FileAlignment { get; set; }
+
+ ///
+ /// Gets whether a layout has to leave this content where it is, because something records
+ /// its address and moving it in the file would move it in memory.
+ ///
+ public virtual bool IsPositionPinned => false;
+
+ ///
+ /// Writes the bytes of this content. The writer is positioned at .
+ ///
+ /// The writer to write to.
+ public abstract void WriteContent(MachOWriter writer);
+}
diff --git a/src/LibObjectFile/MachO/Content/MachOHeaderContent.cs b/src/LibObjectFile/MachO/Content/MachOHeaderContent.cs
new file mode 100644
index 0000000..9b6e80d
--- /dev/null
+++ b/src/LibObjectFile/MachO/Content/MachOHeaderContent.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The Mach-O header, which is always the first content of an image.
+///
+///
+/// The header is generated from the file it belongs to rather than held as bytes, so the counts
+/// it records cannot drift from the load commands actually present.
+///
+public sealed class MachOHeaderContent : MachOContent
+{
+ ///
+ public override bool IsPositionPinned => true;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = context.File.HeaderSize;
+
+ ///
+ public override unsafe void WriteContent(MachOWriter writer)
+ {
+ var file = writer.File;
+
+ if (file.Is64Bit)
+ {
+ writer.Write(new RawMachHeader64
+ {
+ Magic = MachOMagic.Magic64,
+ CpuType = (uint)file.CpuType,
+ CpuSubType = file.CpuSubType,
+ FileType = (uint)file.FileType,
+ NumberOfCommands = (uint)file.LoadCommands.Count,
+ SizeOfCommands = file.SizeOfCommands,
+ Flags = (uint)file.Flags,
+ Reserved = file.Reserved,
+ });
+ }
+ else
+ {
+ writer.Write(new RawMachHeader32
+ {
+ Magic = MachOMagic.Magic32,
+ CpuType = (uint)file.CpuType,
+ CpuSubType = file.CpuSubType,
+ FileType = (uint)file.FileType,
+ NumberOfCommands = (uint)file.LoadCommands.Count,
+ SizeOfCommands = file.SizeOfCommands,
+ Flags = (uint)file.Flags,
+ });
+ }
+ }
+}
diff --git a/src/LibObjectFile/MachO/Content/MachOLoadCommandPadding.cs b/src/LibObjectFile/MachO/Content/MachOLoadCommandPadding.cs
new file mode 100644
index 0000000..86f4a1c
--- /dev/null
+++ b/src/LibObjectFile/MachO/Content/MachOLoadCommandPadding.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The padding a linker leaves between the load command table and whatever follows it.
+///
+///
+/// Adding a load command consumes this from the front, since the table grows into it, so what
+/// survives is its tail. That is what bounds an install_name_tool-style edit: once the
+/// padding is used up there is nowhere for another command to go without moving content, and
+/// moving content in a Mach-O moves addresses with it.
+///
+public sealed class MachOLoadCommandPadding : MachOStreamContent
+{
+ ///
+ /// Initializes a new instance holding the padding bytes.
+ ///
+ /// The padding bytes as the linker left them.
+ /// is null.
+ public MachOLoadCommandPadding(Stream content) : base(content)
+ {
+ }
+
+ ///
+ public override bool IsPositionPinned => true;
+
+ ///
+ /// Keeps the size the layout assigned rather than taking it from the bytes held, because the
+ /// load command table may have grown into them since they were read.
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ {
+ }
+
+ ///
+ public override void WriteContent(MachOWriter writer)
+ {
+ // The commands grew into the front of this, so the part that survives is the tail.
+ var available = (long)Content.Length;
+ var keep = (long)Size;
+
+ if (keep > available)
+ {
+ writer.WriteZero((int)(keep - available));
+ keep = available;
+ }
+
+ Content.Position = available - keep;
+ writer.Write(Content, (ulong)keep);
+ }
+}
diff --git a/src/LibObjectFile/MachO/Content/MachOLoadCommandTable.cs b/src/LibObjectFile/MachO/Content/MachOLoadCommandTable.cs
new file mode 100644
index 0000000..4bc1712
--- /dev/null
+++ b/src/LibObjectFile/MachO/Content/MachOLoadCommandTable.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The load command table, which follows the header and describes everything else.
+///
+///
+/// The table is pinned because dyld expects it directly after the header, and because the
+/// padding the linker leaves after it is what bounds how far it can grow.
+///
+public sealed class MachOLoadCommandTable : MachOContent
+{
+ ///
+ public override bool IsPositionPinned => true;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ {
+ foreach (var command in context.File.LoadCommands)
+ {
+ command.UpdateLayout(context);
+ }
+
+ Size = context.File.SizeOfCommands;
+ }
+
+ ///
+ public override void WriteContent(MachOWriter writer)
+ {
+ foreach (var command in writer.File.LoadCommands)
+ {
+ command.Write(writer);
+ }
+ }
+}
diff --git a/src/LibObjectFile/MachO/Content/MachOSectionData.cs b/src/LibObjectFile/MachO/Content/MachOSectionData.cs
new file mode 100644
index 0000000..3fe0087
--- /dev/null
+++ b/src/LibObjectFile/MachO/Content/MachOSectionData.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The bytes of a .
+///
+///
+/// This is pinned. A section's address is its segment's address plus its distance from the
+/// segment's file offset, so moving it in the file moves it in memory, and every instruction and
+/// relocation referring to it would then be wrong. Only relinking can move a section.
+///
+public sealed class MachOSectionData : MachOStreamContent
+{
+ ///
+ /// Initializes a new instance holding the bytes of a section.
+ ///
+ /// The section these bytes belong to.
+ /// The bytes of the section.
+ /// or is null.
+ public MachOSectionData(MachOSection section, Stream content) : base(content)
+ {
+ ArgumentNullException.ThrowIfNull(section);
+ Section = section;
+ }
+
+ ///
+ /// Gets the section these bytes belong to.
+ ///
+ public MachOSection Section { get; }
+
+ ///
+ public override bool IsPositionPinned => true;
+
+ ///
+ protected override bool PrintMembers(System.Text.StringBuilder builder)
+ {
+ builder.Append($"{Section.SegmentName},{Section.Name} Position = 0x{Position:X}, Size = 0x{Size:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/Content/MachOStreamContent.cs b/src/LibObjectFile/MachO/Content/MachOStreamContent.cs
new file mode 100644
index 0000000..1d7b01c
--- /dev/null
+++ b/src/LibObjectFile/MachO/Content/MachOStreamContent.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Content held as a stream of bytes this library does not interpret.
+///
+///
+/// This covers alignment padding as well as anything else the file contains. Padding is kept as
+/// the bytes that were there rather than regenerated: a linker pads executable sections with
+/// nop rather than zeros, so filling a gap with zeros would put a different instruction
+/// in a place that can be reached.
+///
+public class MachOStreamContent : MachOContent
+{
+ private Stream _content;
+
+ ///
+ /// Initializes a new instance holding the given bytes.
+ ///
+ /// The bytes of this content.
+ /// is null.
+ public MachOStreamContent(Stream content)
+ {
+ ArgumentNullException.ThrowIfNull(content);
+ _content = content;
+ Size = (ulong)content.Length;
+ }
+
+ ///
+ /// Gets or sets the bytes of this content.
+ ///
+ /// The value is null.
+ public Stream Content
+ {
+ get => _content;
+ set
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ _content = value;
+ Size = (ulong)value.Length;
+ }
+ }
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = (ulong)_content.Length;
+
+ ///
+ public override void WriteContent(MachOWriter writer)
+ {
+ _content.Position = 0;
+ writer.Write(_content, (ulong)_content.Length);
+ }
+
+ ///
+ protected override bool PrintMembers(System.Text.StringBuilder builder)
+ {
+ builder.Append($"Position = 0x{Position:X}, Size = 0x{Size:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/Internal/MachOName.cs b/src/LibObjectFile/MachO/Internal/MachOName.cs
new file mode 100644
index 0000000..0e73ba2
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/MachOName.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+
+namespace LibObjectFile.MachO.Internal;
+
+///
+/// Helpers for the fixed 16-byte name fields used by segments and sections.
+///
+internal static class MachOName
+{
+ ///
+ /// The width of a segment or section name field.
+ ///
+ public const int Length = 16;
+
+ ///
+ /// Decodes a fixed-width name. A name that fills the field has no terminator, so the
+ /// terminator is optional rather than required.
+ ///
+ public static string Read(ReadOnlySpan span)
+ {
+ int end = span.IndexOf((byte)0);
+ if (end < 0) end = span.Length;
+ return Encoding.UTF8.GetString(span.Slice(0, end));
+ }
+
+ ///
+ /// Encodes a name into a fixed-width field, zero-filling the remainder. Names longer than
+ /// the field are truncated, which matches what the linker does.
+ ///
+ public static void Write(Span span, string name)
+ {
+ span.Clear();
+ Encoding.UTF8.TryGetBytes(name, span, out _);
+ }
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawBuildToolVersion.cs b/src/LibObjectFile/MachO/Internal/RawBuildToolVersion.cs
new file mode 100644
index 0000000..cdea4a9
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawBuildToolVersion.cs
@@ -0,0 +1,19 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// One tool entry of a build version command (build_tool_version).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawBuildToolVersion
+{
+ public uint Tool;
+ public uint Version;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawBuildVersionCommand.cs b/src/LibObjectFile/MachO/Internal/RawBuildVersionCommand.cs
new file mode 100644
index 0000000..3b0bf25
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawBuildVersionCommand.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The build version load command (build_version_command), followed by
+/// ToolCount tool entries.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawBuildVersionCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint Platform;
+ public uint MinOS;
+ public uint Sdk;
+ public uint ToolCount;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawDyldInfoCommand.cs b/src/LibObjectFile/MachO/Internal/RawDyldInfoCommand.cs
new file mode 100644
index 0000000..9470ebf
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawDyldInfoCommand.cs
@@ -0,0 +1,34 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The compressed dyld information load command (dyld_info_command).
+///
+///
+/// Each pair locates an opcode stream in __LINKEDIT. The streams address their targets by
+/// segment index and offset within that segment, never by file offset, so relocating them only
+/// requires updating the offsets recorded here.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawDyldInfoCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint RebaseOffset;
+ public uint RebaseSize;
+ public uint BindOffset;
+ public uint BindSize;
+ public uint WeakBindOffset;
+ public uint WeakBindSize;
+ public uint LazyBindOffset;
+ public uint LazyBindSize;
+ public uint ExportOffset;
+ public uint ExportSize;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawDylibCommand.cs b/src/LibObjectFile/MachO/Internal/RawDylibCommand.cs
new file mode 100644
index 0000000..f7156f7
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawDylibCommand.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A dylib load command (dylib_command), followed by the library path.
+///
+///
+/// NameOffset is measured from the start of the command, not from the end of this
+/// structure, so a command whose path is preceded by padding is still valid.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawDylibCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint NameOffset;
+ public uint Timestamp;
+ public uint CurrentVersion;
+ public uint CompatibilityVersion;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawDysymtabCommand.cs b/src/LibObjectFile/MachO/Internal/RawDysymtabCommand.cs
new file mode 100644
index 0000000..7befc63
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawDysymtabCommand.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The dynamic symbol table load command (dysymtab_command).
+///
+///
+/// The first three pairs are index and count into the symbol table proper, which the linker
+/// sorts into local, external and undefined runs. The rest are file offsets to tables of their
+/// own, each paired with an entry count rather than a byte size.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawDysymtabCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint LocalSymbolIndex;
+ public uint LocalSymbolCount;
+ public uint ExternalSymbolIndex;
+ public uint ExternalSymbolCount;
+ public uint UndefinedSymbolIndex;
+ public uint UndefinedSymbolCount;
+ public uint TableOfContentsOffset;
+ public uint TableOfContentsCount;
+ public uint ModuleTableOffset;
+ public uint ModuleTableCount;
+ public uint ExternalReferenceOffset;
+ public uint ExternalReferenceCount;
+ public uint IndirectSymbolOffset;
+ public uint IndirectSymbolCount;
+ public uint ExternalRelocationOffset;
+ public uint ExternalRelocationCount;
+ public uint LocalRelocationOffset;
+ public uint LocalRelocationCount;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawEntryPointCommand.cs b/src/LibObjectFile/MachO/Internal/RawEntryPointCommand.cs
new file mode 100644
index 0000000..7b131f9
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawEntryPointCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The entry point load command (entry_point_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawEntryPointCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public ulong EntryOffset;
+ public ulong StackSize;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawFatArch.cs b/src/LibObjectFile/MachO/Internal/RawFatArch.cs
new file mode 100644
index 0000000..c4a0ff1
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawFatArch.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// One slice of a universal binary (fat_arch). Stored big-endian; see .
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawFatArch
+{
+ public uint CpuType;
+ public uint CpuSubType;
+ public uint Offset;
+ public uint Size;
+ public uint Align;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawFatArch64.cs b/src/LibObjectFile/MachO/Internal/RawFatArch64.cs
new file mode 100644
index 0000000..7589894
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawFatArch64.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// One slice of a universal binary using 64-bit offsets (fat_arch_64), used when a slice
+/// starts beyond 4GB. Stored big-endian; see .
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawFatArch64
+{
+ public uint CpuType;
+ public uint CpuSubType;
+ public ulong Offset;
+ public ulong Size;
+ public uint Align;
+ public uint Reserved;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawFatHeader.cs b/src/LibObjectFile/MachO/Internal/RawFatHeader.cs
new file mode 100644
index 0000000..f05e250
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawFatHeader.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The header of a universal binary (fat_header), followed by NumberOfArchitectures
+/// architecture entries.
+///
+///
+/// Unlike every other structure in the format, the fat header and its architecture entries are
+/// always stored big-endian regardless of the architectures they contain, so they cannot be read
+/// by a straight copy on a little-endian host.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawFatHeader
+{
+ public uint Magic;
+ public uint NumberOfArchitectures;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawLinkEditDataCommand.cs b/src/LibObjectFile/MachO/Internal/RawLinkEditDataCommand.cs
new file mode 100644
index 0000000..b783c0d
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawLinkEditDataCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A load command pointing at a blob in __LINKEDIT (linkedit_data_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawLinkEditDataCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint DataOffset;
+ public uint DataSize;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawLoadCommand.cs b/src/LibObjectFile/MachO/Internal/RawLoadCommand.cs
new file mode 100644
index 0000000..bc48396
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawLoadCommand.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The header every load command starts with (load_command). CmdSize covers the
+/// header itself, so walking the command list means advancing by that value.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawLoadCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawMachHeader32.cs b/src/LibObjectFile/MachO/Internal/RawMachHeader32.cs
new file mode 100644
index 0000000..ac0edad
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawMachHeader32.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The 32-bit Mach-O file header (mach_header).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawMachHeader32
+{
+ public uint Magic;
+ public uint CpuType;
+ public uint CpuSubType;
+ public uint FileType;
+ public uint NumberOfCommands;
+ public uint SizeOfCommands;
+ public uint Flags;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawMachHeader64.cs b/src/LibObjectFile/MachO/Internal/RawMachHeader64.cs
new file mode 100644
index 0000000..952679e
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawMachHeader64.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The 64-bit Mach-O file header (mach_header_64). Identical to the 32-bit header
+/// except for the trailing reserved field, which pads it to 32 bytes.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawMachHeader64
+{
+ public uint Magic;
+ public uint CpuType;
+ public uint CpuSubType;
+ public uint FileType;
+ public uint NumberOfCommands;
+ public uint SizeOfCommands;
+ public uint Flags;
+ public uint Reserved;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawNList32.cs b/src/LibObjectFile/MachO/Internal/RawNList32.cs
new file mode 100644
index 0000000..c8f4ea0
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawNList32.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A 32-bit symbol table entry (nlist).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawNList32
+{
+ public uint StringIndex;
+ public byte Type;
+ public byte SectionIndex;
+ ///
+ /// The description field. The reference declares this signed for nlist and unsigned
+ /// for nlist_64; it is read unsigned in both cases because it is a set of bit fields
+ /// rather than a number, and its high byte is the ordinal of the library defining the symbol.
+ ///
+ public ushort Description;
+ public uint Value;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawNList64.cs b/src/LibObjectFile/MachO/Internal/RawNList64.cs
new file mode 100644
index 0000000..2dacf03
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawNList64.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A 64-bit symbol table entry (nlist_64).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawNList64
+{
+ public uint StringIndex;
+ public byte Type;
+ public byte SectionIndex;
+ ///
+ /// The description field. The reference declares this signed for nlist and unsigned
+ /// for nlist_64; it is read unsigned in both cases because it is a set of bit fields
+ /// rather than a number, and its high byte is the ordinal of the library defining the symbol.
+ ///
+ public ushort Description;
+ public ulong Value;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawPathCommand.cs b/src/LibObjectFile/MachO/Internal/RawPathCommand.cs
new file mode 100644
index 0000000..2bc62ab
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawPathCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The shared shape of the load commands that carry a single path, covering
+/// rpath_command, dylinker_command and sub_*_command.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawPathCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint PathOffset;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawRelocationInfo.cs b/src/LibObjectFile/MachO/Internal/RawRelocationInfo.cs
new file mode 100644
index 0000000..f946135
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawRelocationInfo.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A relocation entry (relocation_info or scattered_relocation_info).
+///
+///
+/// The two forms share a size and are told apart by the top bit of the first word. C bitfields
+/// have no equivalent here, so both words are kept whole and unpacked by hand, which also avoids
+/// depending on how a compiler happens to lay the fields out.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawRelocationInfo
+{
+ public uint Word0;
+ public uint Word1;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawSection32.cs b/src/LibObjectFile/MachO/Internal/RawSection32.cs
new file mode 100644
index 0000000..70fb779
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawSection32.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A 32-bit section header (section). It carries its own segment name, so a section is
+/// self-describing even though it is stored inside a segment command.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal unsafe struct RawSection32
+{
+ public fixed byte SectionName[16];
+ public fixed byte SegmentName[16];
+ public uint Address;
+ public uint Size;
+ public uint Offset;
+ public uint Align;
+ public uint RelocationOffset;
+ public uint NumberOfRelocations;
+ public uint Flags;
+ public uint Reserved1;
+ public uint Reserved2;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawSection64.cs b/src/LibObjectFile/MachO/Internal/RawSection64.cs
new file mode 100644
index 0000000..0c4adbd
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawSection64.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A 64-bit section header (section_64).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal unsafe struct RawSection64
+{
+ public fixed byte SectionName[16];
+ public fixed byte SegmentName[16];
+ public ulong Address;
+ public ulong Size;
+ public uint Offset;
+ public uint Align;
+ public uint RelocationOffset;
+ public uint NumberOfRelocations;
+ public uint Flags;
+ public uint Reserved1;
+ public uint Reserved2;
+ public uint Reserved3;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawSegmentCommand32.cs b/src/LibObjectFile/MachO/Internal/RawSegmentCommand32.cs
new file mode 100644
index 0000000..c5a1bc8
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawSegmentCommand32.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A 32-bit segment load command (segment_command), followed in the file by
+/// NumberOfSections entries.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal unsafe struct RawSegmentCommand32
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public fixed byte SegmentName[16];
+ public uint VmAddress;
+ public uint VmSize;
+ public uint FileOffset;
+ public uint FileSize;
+ public uint MaxProtection;
+ public uint InitProtection;
+ public uint NumberOfSections;
+ public uint Flags;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawSegmentCommand64.cs b/src/LibObjectFile/MachO/Internal/RawSegmentCommand64.cs
new file mode 100644
index 0000000..945c85d
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawSegmentCommand64.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A 64-bit segment load command (segment_command_64), followed in the file by
+/// NumberOfSections entries.
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal unsafe struct RawSegmentCommand64
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public fixed byte SegmentName[16];
+ public ulong VmAddress;
+ public ulong VmSize;
+ public ulong FileOffset;
+ public ulong FileSize;
+ public uint MaxProtection;
+ public uint InitProtection;
+ public uint NumberOfSections;
+ public uint Flags;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawSourceVersionCommand.cs b/src/LibObjectFile/MachO/Internal/RawSourceVersionCommand.cs
new file mode 100644
index 0000000..62fc527
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawSourceVersionCommand.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The source version load command (source_version_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawSourceVersionCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public ulong Version;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawSymtabCommand.cs b/src/LibObjectFile/MachO/Internal/RawSymtabCommand.cs
new file mode 100644
index 0000000..271ac6f
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawSymtabCommand.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The symbol table load command (symtab_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawSymtabCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint SymbolOffset;
+ public uint SymbolCount;
+ public uint StringOffset;
+ public uint StringSize;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawTwoLevelHintsCommand.cs b/src/LibObjectFile/MachO/Internal/RawTwoLevelHintsCommand.cs
new file mode 100644
index 0000000..1fe7038
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawTwoLevelHintsCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The two-level namespace hint table load command (twolevel_hints_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawTwoLevelHintsCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint Offset;
+ public uint HintCount;
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawUuidCommand.cs b/src/LibObjectFile/MachO/Internal/RawUuidCommand.cs
new file mode 100644
index 0000000..6cf334e
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawUuidCommand.cs
@@ -0,0 +1,20 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// The image identifier load command (uuid_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal unsafe struct RawUuidCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public fixed byte Uuid[16];
+}
diff --git a/src/LibObjectFile/MachO/Internal/RawVersionMinCommand.cs b/src/LibObjectFile/MachO/Internal/RawVersionMinCommand.cs
new file mode 100644
index 0000000..7563c37
--- /dev/null
+++ b/src/LibObjectFile/MachO/Internal/RawVersionMinCommand.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace LibObjectFile.MachO.Internal;
+
+#pragma warning disable CS0649
+
+///
+/// A minimum OS version load command (version_min_command).
+///
+[StructLayout(LayoutKind.Sequential, Pack = 4)]
+internal struct RawVersionMinCommand
+{
+ public uint Cmd;
+ public uint CmdSize;
+ public uint Version;
+ public uint Sdk;
+}
diff --git a/src/LibObjectFile/MachO/MachOBuildToolVersion.cs b/src/LibObjectFile/MachO/MachOBuildToolVersion.cs
new file mode 100644
index 0000000..acca812
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOBuildToolVersion.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A tool that took part in producing an image, as recorded by LC_BUILD_VERSION.
+///
+public enum MachOBuildTool : uint
+{
+ /// The tool is not one of the known values.
+ Unknown = 0,
+ /// The Clang compiler (TOOL_CLANG).
+ Clang = 1,
+ /// The Swift compiler (TOOL_SWIFT).
+ Swift = 2,
+ /// The static linker (TOOL_LD).
+ Ld = 3,
+ /// An alternative linker (TOOL_LLD).
+ Lld = 4,
+}
+
+///
+/// One entry of the tool list in .
+///
+/// The tool.
+/// The tool's version, packed as 16.8.8 bits.
+public readonly record struct MachOBuildToolVersion(MachOBuildTool Tool, uint PackedVersion)
+{
+ ///
+ /// Gets the tool's version.
+ ///
+ public Version Version => MachOVersion.Decode(PackedVersion);
+
+ ///
+ public override string ToString() => $"{Tool} {Version}";
+}
diff --git a/src/LibObjectFile/MachO/MachOBuildVersionCommand.cs b/src/LibObjectFile/MachO/MachOBuildVersionCommand.cs
new file mode 100644
index 0000000..5da89b4
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOBuildVersionCommand.cs
@@ -0,0 +1,131 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The build version load command (LC_BUILD_VERSION).
+///
+///
+/// This replaced the LC_VERSION_MIN_* family, carrying the platform as a field rather
+/// than encoding it in the command type, and adding the list of tools that produced the image.
+///
+public sealed class MachOBuildVersionCommand : MachOLoadCommand
+{
+ ///
+ /// The size of the command excluding its tool entries.
+ ///
+ public const uint HeaderSize = 24;
+
+ ///
+ /// The size of one tool entry.
+ ///
+ public const uint ToolSize = 8;
+
+ ///
+ /// Gets or sets the platform the image targets.
+ ///
+ public MachOPlatform Platform { get; set; }
+
+ ///
+ /// Gets or sets the packed minimum OS version.
+ ///
+ public uint MinOSVersion { get; set; }
+
+ ///
+ /// Gets or sets the packed SDK version the image was built against.
+ ///
+ public uint SdkVersion { get; set; }
+
+ ///
+ /// Gets the tools that produced this image.
+ ///
+ public List Tools { get; } = [];
+
+ ///
+ /// Gets the minimum OS version.
+ ///
+ public Version MinOS => MachOVersion.Decode(MinOSVersion);
+
+ ///
+ /// Gets the SDK version.
+ ///
+ public Version Sdk => MachOVersion.Decode(SdkVersion);
+
+ ///
+ public override uint MinimumCommandSize => HeaderSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ => Size = HeaderSize + (uint)Tools.Count * ToolSize;
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawBuildVersionCommand), out RawBuildVersionCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_BUILD_VERSION at 0x{Position:X}");
+ return;
+ }
+
+ Platform = (MachOPlatform)raw.Platform;
+ MinOSVersion = raw.MinOS;
+ SdkVersion = raw.Sdk;
+
+ Tools.Clear();
+
+ // Derived by division for the same reason as the section count: multiplying the declared
+ // count out could wrap past the 32-bit field and pass a check it should not.
+ if (Size < HeaderSize || raw.ToolCount > (Size - HeaderSize) / ToolSize)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidLoadCommandSize,
+ $"LC_BUILD_VERSION declares {raw.ToolCount} tools, which do not fit in its cmdsize of {Size}");
+ return;
+ }
+
+ for (uint i = 0; i < raw.ToolCount; i++)
+ {
+ if (!reader.TryReadData(sizeof(RawBuildToolVersion), out RawBuildToolVersion tool))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated tool entry in LC_BUILD_VERSION at 0x{Position:X}");
+ return;
+ }
+
+ Tools.Add(new MachOBuildToolVersion((MachOBuildTool)tool.Tool, tool.Version));
+ }
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.Write(new RawBuildVersionCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ Platform = (uint)Platform,
+ MinOS = MinOSVersion,
+ Sdk = SdkVersion,
+ ToolCount = (uint)Tools.Count,
+ });
+
+ foreach (var tool in Tools)
+ {
+ writer.Write(new RawBuildToolVersion { Tool = (uint)tool.Tool, Version = tool.PackedVersion });
+ }
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Platform = {Platform}, MinOS = {MinOS}, Sdk = {Sdk}, Tools = {Tools.Count}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOCpuType.cs b/src/LibObjectFile/MachO/MachOCpuType.cs
new file mode 100644
index 0000000..5b65b08
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOCpuType.cs
@@ -0,0 +1,33 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// CPU architecture of a Mach-O image, as stored in the cputype header field.
+///
+///
+/// The 64-bit variants are the 32-bit value with set, which is how a
+/// reader tells x86_64 from i386 without consulting the magic.
+///
+public enum MachOCpuType : uint
+{
+ /// Marks a type as using the 64-bit ABI (CPU_ARCH_ABI64).
+ Abi64 = 0x01000000,
+
+ /// Motorola 68000 (CPU_TYPE_MC680x0).
+ MC680x0 = 6,
+ /// 32-bit Intel, also known as CPU_TYPE_I386 (CPU_TYPE_X86).
+ X86 = 7,
+ /// 64-bit Intel (CPU_TYPE_X86_64).
+ X86_64 = X86 | Abi64,
+ /// 32-bit ARM (CPU_TYPE_ARM).
+ Arm = 12,
+ /// 64-bit ARM (CPU_TYPE_ARM64).
+ Arm64 = Arm | Abi64,
+ /// 32-bit PowerPC (CPU_TYPE_POWERPC).
+ PowerPC = 18,
+ /// 64-bit PowerPC (CPU_TYPE_POWERPC64).
+ PowerPC64 = PowerPC | Abi64,
+}
diff --git a/src/LibObjectFile/MachO/MachODyldInfoCommand.cs b/src/LibObjectFile/MachO/MachODyldInfoCommand.cs
new file mode 100644
index 0000000..06d70d7
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachODyldInfoCommand.cs
@@ -0,0 +1,132 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The compressed dyld information load command (LC_DYLD_INFO or LC_DYLD_INFO_ONLY).
+///
+///
+/// Each offset and size pair locates an opcode stream in __LINKEDIT telling dyld how to
+/// rebase and bind the image. The streams are not decoded here; they address their targets by
+/// segment index and offset within that segment rather than by file offset, so moving them only
+/// means updating the offsets recorded in this command.
+///
+/// The ONLY spelling means the image carries no classic relocations as a fallback, so a
+/// loader that does not understand these streams cannot load it at all.
+///
+///
+public sealed class MachODyldInfoCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 48;
+
+ /// Gets or sets the file offset of the rebase opcodes.
+ public uint RebaseOffset { get; set; }
+
+ /// Gets or sets the size in bytes of the rebase opcodes.
+ public uint RebaseSize { get; set; }
+
+ /// Gets or sets the file offset of the binding opcodes.
+ public uint BindOffset { get; set; }
+
+ /// Gets or sets the size in bytes of the binding opcodes.
+ public uint BindSize { get; set; }
+
+ /// Gets or sets the file offset of the weak binding opcodes.
+ public uint WeakBindOffset { get; set; }
+
+ /// Gets or sets the size in bytes of the weak binding opcodes.
+ public uint WeakBindSize { get; set; }
+
+ /// Gets or sets the file offset of the lazy binding opcodes.
+ public uint LazyBindOffset { get; set; }
+
+ /// Gets or sets the size in bytes of the lazy binding opcodes.
+ public uint LazyBindSize { get; set; }
+
+ /// Gets or sets the file offset of the export trie.
+ public uint ExportOffset { get; set; }
+
+ /// Gets or sets the size in bytes of the export trie.
+ public uint ExportSize { get; set; }
+
+ ///
+ /// Gets a value indicating whether the image relies on these streams alone, with no classic
+ /// relocations to fall back on.
+ ///
+ public bool IsOnly => Type == MachOLoadCommandType.DyldInfoOnly;
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+ if (RebaseOffset != 0) RebaseOffset = mapper(RebaseOffset);
+ if (BindOffset != 0) BindOffset = mapper(BindOffset);
+ if (WeakBindOffset != 0) WeakBindOffset = mapper(WeakBindOffset);
+ if (LazyBindOffset != 0) LazyBindOffset = mapper(LazyBindOffset);
+ if (ExportOffset != 0) ExportOffset = mapper(ExportOffset);
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawDyldInfoCommand), out RawDyldInfoCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated {Type} at 0x{Position:X}");
+ return;
+ }
+
+ RebaseOffset = raw.RebaseOffset;
+ RebaseSize = raw.RebaseSize;
+ BindOffset = raw.BindOffset;
+ BindSize = raw.BindSize;
+ WeakBindOffset = raw.WeakBindOffset;
+ WeakBindSize = raw.WeakBindSize;
+ LazyBindOffset = raw.LazyBindOffset;
+ LazyBindSize = raw.LazyBindSize;
+ ExportOffset = raw.ExportOffset;
+ ExportSize = raw.ExportSize;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.Write(new RawDyldInfoCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ RebaseOffset = RebaseOffset,
+ RebaseSize = RebaseSize,
+ BindOffset = BindOffset,
+ BindSize = BindSize,
+ WeakBindOffset = WeakBindOffset,
+ WeakBindSize = WeakBindSize,
+ LazyBindOffset = LazyBindOffset,
+ LazyBindSize = LazyBindSize,
+ ExportOffset = ExportOffset,
+ ExportSize = ExportSize,
+ });
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Type = {Type}, Rebase = 0x{RebaseSize:X}, Bind = 0x{BindSize:X}, LazyBind = 0x{LazyBindSize:X}, Export = 0x{ExportSize:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachODylibCommand.cs b/src/LibObjectFile/MachO/MachODylibCommand.cs
new file mode 100644
index 0000000..f1c40cc
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachODylibCommand.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A load command naming a dynamic library, covering LC_LOAD_DYLIB, LC_ID_DYLIB,
+/// LC_LOAD_WEAK_DYLIB, LC_REEXPORT_DYLIB, LC_LOAD_UPWARD_DYLIB and
+/// LC_LAZY_LOAD_DYLIB.
+///
+///
+/// The order of these commands is what dyld reports as the library ordinal, so inserting one
+/// anywhere other than after the existing ones renumbers the bindings of every command following
+/// it. appends rather than inserts for that reason.
+///
+public sealed class MachODylibCommand : MachOPathLoadCommand
+{
+ ///
+ /// Gets or sets the path of the library, which is its install name rather than a file path
+ /// and may start with @rpath, @executable_path or @loader_path.
+ ///
+ public string Name
+ {
+ get => Value;
+ set => Value = value;
+ }
+
+ ///
+ /// Gets or sets the build timestamp of the library.
+ ///
+ public uint Timestamp { get; set; }
+
+ ///
+ /// Gets or sets the current version of the library, packed as 16.8.8 bits.
+ ///
+ public uint CurrentVersion { get; set; }
+
+ ///
+ /// Gets or sets the oldest version of the library this image is compatible with, packed as 16.8.8 bits.
+ ///
+ public uint CompatibilityVersion { get; set; }
+
+ ///
+ /// Gets a value indicating whether a missing library is tolerated at load time.
+ ///
+ public bool IsWeak => Type == MachOLoadCommandType.LoadWeakDylib;
+
+ ///
+ protected override unsafe uint FixedSize => (uint)sizeof(RawDylibCommand);
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ var commandPosition = reader.Position;
+ if (!reader.TryReadData(sizeof(RawDylibCommand), out RawDylibCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated dylib command at 0x{commandPosition:X}");
+ return;
+ }
+
+ Timestamp = raw.Timestamp;
+ CurrentVersion = raw.CurrentVersion;
+ CompatibilityVersion = raw.CompatibilityVersion;
+ ReadValue(reader, commandPosition, raw.NameOffset);
+ }
+
+ ///
+ public override unsafe void Write(MachOWriter writer)
+ {
+ writer.Write(new RawDylibCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ NameOffset = (uint)sizeof(RawDylibCommand),
+ Timestamp = Timestamp,
+ CurrentVersion = CurrentVersion,
+ CompatibilityVersion = CompatibilityVersion,
+ });
+ WriteValue(writer);
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachODynamicSymbolTableCommand.cs b/src/LibObjectFile/MachO/MachODynamicSymbolTableCommand.cs
new file mode 100644
index 0000000..8219526
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachODynamicSymbolTableCommand.cs
@@ -0,0 +1,181 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The dynamic symbol table load command (LC_DYSYMTAB).
+///
+///
+/// The index and count pairs describe runs within the table located by
+/// , which the linker sorts into local, then external, then
+/// undefined symbols. The remaining pairs locate tables of their own in __LINKEDIT. The
+/// indirect symbol table is the one that matters at runtime: the stub and symbol pointer sections
+/// index into it through their reserved1 field.
+///
+public sealed class MachODynamicSymbolTableCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 80;
+
+ /// The size of one table of contents entry (dylib_table_of_contents).
+ public const uint TableOfContentsEntrySize = 8;
+
+ /// The size of one external reference entry (dylib_reference).
+ public const uint ExternalReferenceEntrySize = 4;
+
+ /// The size of one indirect symbol entry, which is an index into the symbol table.
+ public const uint IndirectSymbolEntrySize = 4;
+
+ ///
+ /// Gets the size of one module table entry for the given image width
+ /// (dylib_module or dylib_module_64).
+ ///
+ /// Whether the containing image is 64-bit.
+ /// 56 for a 64-bit image, 52 for a 32-bit one, the difference being one 64-bit field.
+ public static uint GetModuleTableEntrySize(bool is64Bit) => is64Bit ? 56u : 52u;
+
+ /// Gets or sets the index of the first local symbol.
+ public uint LocalSymbolIndex { get; set; }
+
+ /// Gets or sets the number of local symbols.
+ public uint LocalSymbolCount { get; set; }
+
+ /// Gets or sets the index of the first externally defined symbol.
+ public uint ExternalSymbolIndex { get; set; }
+
+ /// Gets or sets the number of externally defined symbols.
+ public uint ExternalSymbolCount { get; set; }
+
+ /// Gets or sets the index of the first undefined symbol.
+ public uint UndefinedSymbolIndex { get; set; }
+
+ /// Gets or sets the number of undefined symbols.
+ public uint UndefinedSymbolCount { get; set; }
+
+ /// Gets or sets the file offset of the table of contents, used only by static libraries.
+ public uint TableOfContentsOffset { get; set; }
+
+ /// Gets or sets the number of table of contents entries.
+ public uint TableOfContentsCount { get; set; }
+
+ /// Gets or sets the file offset of the module table.
+ public uint ModuleTableOffset { get; set; }
+
+ /// Gets or sets the number of module table entries.
+ public uint ModuleTableCount { get; set; }
+
+ /// Gets or sets the file offset of the external reference table.
+ public uint ExternalReferenceOffset { get; set; }
+
+ /// Gets or sets the number of external reference entries.
+ public uint ExternalReferenceCount { get; set; }
+
+ /// Gets or sets the file offset of the indirect symbol table.
+ public uint IndirectSymbolOffset { get; set; }
+
+ /// Gets or sets the number of indirect symbol entries, each a 4-byte symbol index.
+ public uint IndirectSymbolCount { get; set; }
+
+ /// Gets or sets the file offset of the external relocation entries.
+ public uint ExternalRelocationOffset { get; set; }
+
+ /// Gets or sets the number of external relocation entries.
+ public uint ExternalRelocationCount { get; set; }
+
+ /// Gets or sets the file offset of the local relocation entries.
+ public uint LocalRelocationOffset { get; set; }
+
+ /// Gets or sets the number of local relocation entries.
+ public uint LocalRelocationCount { get; set; }
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+ if (TableOfContentsOffset != 0) TableOfContentsOffset = mapper(TableOfContentsOffset);
+ if (ModuleTableOffset != 0) ModuleTableOffset = mapper(ModuleTableOffset);
+ if (ExternalReferenceOffset != 0) ExternalReferenceOffset = mapper(ExternalReferenceOffset);
+ if (IndirectSymbolOffset != 0) IndirectSymbolOffset = mapper(IndirectSymbolOffset);
+ if (ExternalRelocationOffset != 0) ExternalRelocationOffset = mapper(ExternalRelocationOffset);
+ if (LocalRelocationOffset != 0) LocalRelocationOffset = mapper(LocalRelocationOffset);
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawDysymtabCommand), out RawDysymtabCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_DYSYMTAB at 0x{Position:X}");
+ return;
+ }
+
+ LocalSymbolIndex = raw.LocalSymbolIndex;
+ LocalSymbolCount = raw.LocalSymbolCount;
+ ExternalSymbolIndex = raw.ExternalSymbolIndex;
+ ExternalSymbolCount = raw.ExternalSymbolCount;
+ UndefinedSymbolIndex = raw.UndefinedSymbolIndex;
+ UndefinedSymbolCount = raw.UndefinedSymbolCount;
+ TableOfContentsOffset = raw.TableOfContentsOffset;
+ TableOfContentsCount = raw.TableOfContentsCount;
+ ModuleTableOffset = raw.ModuleTableOffset;
+ ModuleTableCount = raw.ModuleTableCount;
+ ExternalReferenceOffset = raw.ExternalReferenceOffset;
+ ExternalReferenceCount = raw.ExternalReferenceCount;
+ IndirectSymbolOffset = raw.IndirectSymbolOffset;
+ IndirectSymbolCount = raw.IndirectSymbolCount;
+ ExternalRelocationOffset = raw.ExternalRelocationOffset;
+ ExternalRelocationCount = raw.ExternalRelocationCount;
+ LocalRelocationOffset = raw.LocalRelocationOffset;
+ LocalRelocationCount = raw.LocalRelocationCount;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.Write(new RawDysymtabCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ LocalSymbolIndex = LocalSymbolIndex,
+ LocalSymbolCount = LocalSymbolCount,
+ ExternalSymbolIndex = ExternalSymbolIndex,
+ ExternalSymbolCount = ExternalSymbolCount,
+ UndefinedSymbolIndex = UndefinedSymbolIndex,
+ UndefinedSymbolCount = UndefinedSymbolCount,
+ TableOfContentsOffset = TableOfContentsOffset,
+ TableOfContentsCount = TableOfContentsCount,
+ ModuleTableOffset = ModuleTableOffset,
+ ModuleTableCount = ModuleTableCount,
+ ExternalReferenceOffset = ExternalReferenceOffset,
+ ExternalReferenceCount = ExternalReferenceCount,
+ IndirectSymbolOffset = IndirectSymbolOffset,
+ IndirectSymbolCount = IndirectSymbolCount,
+ ExternalRelocationOffset = ExternalRelocationOffset,
+ ExternalRelocationCount = ExternalRelocationCount,
+ LocalRelocationOffset = LocalRelocationOffset,
+ LocalRelocationCount = LocalRelocationCount,
+ });
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Local = {LocalSymbolCount}, External = {ExternalSymbolCount}, Undefined = {UndefinedSymbolCount}, Indirect = {IndirectSymbolCount}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFatFile.Verify.cs b/src/LibObjectFile/MachO/MachOFatFile.Verify.cs
new file mode 100644
index 0000000..1eb1c1a
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFatFile.Verify.cs
@@ -0,0 +1,107 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFatFile
+{
+ ///
+ /// Checks this universal binary for inconsistencies.
+ ///
+ /// What was found. Empty if nothing was.
+ public DiagnosticBag Verify()
+ {
+ var diagnostics = new DiagnosticBag();
+ Verify(diagnostics);
+ return diagnostics;
+ }
+
+ ///
+ /// Checks this universal binary for inconsistencies, including each slice's own image.
+ ///
+ /// Receives what was found.
+ /// is null.
+ public void Verify(DiagnosticBag diagnostics)
+ {
+ ArgumentNullException.ThrowIfNull(diagnostics);
+
+ var entrySize = (ulong)GetSliceEntrySize(Is64BitOffsets);
+ var tableEnd = (ulong)HeaderSize + (ulong)Slices.Count * entrySize;
+
+ var architectures = new HashSet<(MachOCpuType, uint)>();
+ var ranges = new List<(ulong Start, ulong End, int Index)>();
+
+ for (var i = 0; i < Slices.Count; i++)
+ {
+ var slice = Slices[i];
+
+ if (slice.File is null)
+ {
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_MissingFatSliceImage,
+ $"Slice {i} ({slice.CpuType}) has no image to write");
+ continue;
+ }
+
+ if (slice.AlignLog2 > MachOFatSlice.MaxAlignLog2)
+ {
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidFatSliceAlignment,
+ $"Slice {i} ({slice.CpuType}) has an alignment exponent of {slice.AlignLog2}, past the {MachOFatSlice.MaxAlignLog2} a universal binary allows");
+ }
+ else if ((slice.FileOffset & (slice.Alignment - 1)) != 0)
+ {
+ // A slice is mapped straight out of the containing file, so an offset off its own
+ // page boundary is one the loader cannot map.
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidFatSliceAlignment,
+ $"Slice {i} ({slice.CpuType}) starts at 0x{slice.FileOffset:X}, which is not a multiple of its 0x{slice.Alignment:X} alignment");
+ }
+
+ if (slice.FileOffset < tableEnd)
+ {
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_OverlappingFatSlices,
+ $"Slice {i} ({slice.CpuType}) starts at 0x{slice.FileOffset:X}, inside the slice table that ends at 0x{tableEnd:X}");
+ }
+
+ // The 32-bit table cannot express these, and casting on the way out would record a
+ // different slice rather than fail.
+ if (!Is64BitOffsets && (slice.FileOffset > uint.MaxValue || slice.Size > uint.MaxValue))
+ {
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit,
+ $"Slice {i} ({slice.CpuType}) spans [0x{slice.FileOffset:X}, 0x{slice.FileOffset + slice.Size:X}) which a 32-bit slice table cannot record. Set {nameof(Is64BitOffsets)}.");
+ }
+
+ if (!architectures.Add((slice.CpuType, slice.CpuSubType)))
+ {
+ // The loader takes the first slice matching the host, so a duplicate is either
+ // dead weight or a different image than the one that will be run.
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_DuplicateFatSlice,
+ $"Slice {i} repeats the architecture {slice.CpuType} (subtype 0x{slice.CpuSubType:X}) of an earlier slice");
+ }
+
+ ranges.Add((slice.FileOffset, slice.FileOffset + slice.Size, i));
+ slice.File.Verify(diagnostics);
+ }
+
+ // The header may list slices in any order, so overlap is checked in file order.
+ ranges.Sort((a, b) => a.Start.CompareTo(b.Start));
+ for (var i = 1; i < ranges.Count; i++)
+ {
+ if (ranges[i].Start < ranges[i - 1].End)
+ {
+ diagnostics.Error(
+ DiagnosticId.MACHO_ERR_OverlappingFatSlices,
+ $"Slice {ranges[i].Index} starts at 0x{ranges[i].Start:X}, inside slice {ranges[i - 1].Index} which ends at 0x{ranges[i - 1].End:X}");
+ }
+ }
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFatFile.cs b/src/LibObjectFile/MachO/MachOFatFile.cs
new file mode 100644
index 0000000..e039c0d
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFatFile.cs
@@ -0,0 +1,360 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Buffers.Binary;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.IO;
+using LibObjectFile.MachO.Internal;
+using LibObjectFile.Utils;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A universal binary, holding one per architecture.
+///
+///
+/// The header and its slice table are stored big-endian whatever the architectures inside are,
+/// which is the one place the format does not follow the image's own byte order.
+///
+/// Slices are aligned to the page size of their architecture so the loader can map one straight
+/// out of the containing file, which is why a universal binary is larger than its slices put
+/// together.
+///
+///
+public sealed partial class MachOFatFile
+{
+ ///
+ /// The size of the header preceding the slice table (fat_header).
+ ///
+ public static unsafe int HeaderSize => sizeof(RawFatHeader);
+
+ ///
+ /// Gets the size of one slice table entry for the given offset width.
+ ///
+ /// Whether slice offsets are stored as 64-bit values.
+ /// The size of a fat_arch_64 or a fat_arch.
+ public static unsafe int GetSliceEntrySize(bool is64BitOffsets)
+ => is64BitOffsets ? sizeof(RawFatArch64) : sizeof(RawFatArch);
+
+ ///
+ /// Gets the slices of this universal binary, in the order the header lists them.
+ ///
+ public List Slices { get; } = [];
+
+ ///
+ /// Gets or sets whether slice offsets are stored as 64-bit values, which is needed once a
+ /// slice starts beyond 4GB.
+ ///
+ public bool Is64BitOffsets { get; set; }
+
+ ///
+ /// Checks whether a stream starts with a universal binary magic, without consuming it.
+ ///
+ /// The stream to inspect.
+ /// true if the stream starts with a universal binary magic.
+ /// is null.
+ public static bool IsFat(Stream stream)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ var position = stream.Position;
+ try
+ {
+ Span magic = stackalloc byte[4];
+ if (stream.Read(magic) != 4) return false;
+ var value = BinaryPrimitives.ReadUInt32BigEndian(magic);
+ return value is MachOMagic.FatMagic or MachOMagic.FatMagic64;
+ }
+ finally
+ {
+ stream.Position = position;
+ }
+ }
+
+ ///
+ /// Reads a universal binary from a stream.
+ ///
+ /// The stream positioned at the start of the file.
+ /// Options controlling how each slice is read, or null for the defaults.
+ /// The universal binary read.
+ /// is null.
+ /// The stream does not contain a readable universal binary.
+ public static MachOFatFile Read(Stream stream, MachOReaderOptions? options = null)
+ {
+ if (!TryRead(stream, out var file, out var diagnostics, options))
+ {
+ throw new ObjectFileException($"Unexpected error while reading the Mach-O universal binary", diagnostics);
+ }
+ return file;
+ }
+
+ ///
+ /// Tries to read a universal binary from a stream.
+ ///
+ /// The stream positioned at the start of the file.
+ /// The universal binary read, if reading succeeded.
+ /// The diagnostics collected, if reading failed.
+ /// Options controlling how each slice is read, or null for the defaults.
+ /// true if the file was read; otherwise false.
+ /// is null.
+ public static bool TryRead(Stream stream, [NotNullWhen(true)] out MachOFatFile? file, [NotNullWhen(false)] out DiagnosticBag? diagnostics, MachOReaderOptions? options = null)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ var bag = new DiagnosticBag();
+ file = new MachOFatFile();
+ diagnostics = bag;
+
+ var basePosition = stream.Position;
+ Span header = stackalloc byte[HeaderSize];
+ stream.Position = basePosition;
+ if (stream.Read(header) != HeaderSize)
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatHeader, "The stream is too short to hold a universal binary header");
+ file = null;
+ return false;
+ }
+
+ var magic = BinaryPrimitives.ReadUInt32BigEndian(header);
+ if (magic is not (MachOMagic.FatMagic or MachOMagic.FatMagic64))
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatHeader, $"Invalid universal binary magic 0x{magic:X8}");
+ file = null;
+ return false;
+ }
+
+ file.Is64BitOffsets = magic == MachOMagic.FatMagic64;
+ var count = BinaryPrimitives.ReadUInt32BigEndian(header.Slice(4));
+
+ var entrySize = GetSliceEntrySize(file.Is64BitOffsets);
+ if ((ulong)count * (ulong)entrySize > (ulong)(stream.Length - stream.Position))
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatHeader, $"The universal binary header claims {count} architectures, which do not fit in the file");
+ file = null;
+ return false;
+ }
+
+ var entries = new byte[count * entrySize];
+ try
+ {
+ stream.ReadExactly(entries);
+ }
+ catch (EndOfStreamException)
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatHeader, $"The universal binary header claims {count} architectures, which do not fit in the file");
+ file = null;
+ return false;
+ }
+
+ for (var i = 0; i < count; i++)
+ {
+ var entry = entries.AsSpan(i * entrySize);
+ var slice = new MachOFatSlice
+ {
+ CpuType = (MachOCpuType)BinaryPrimitives.ReadUInt32BigEndian(entry),
+ CpuSubType = BinaryPrimitives.ReadUInt32BigEndian(entry.Slice(4)),
+ };
+
+ if (file.Is64BitOffsets)
+ {
+ slice.FileOffset = BinaryPrimitives.ReadUInt64BigEndian(entry.Slice(8));
+ slice.Size = BinaryPrimitives.ReadUInt64BigEndian(entry.Slice(16));
+ slice.AlignLog2 = BinaryPrimitives.ReadUInt32BigEndian(entry.Slice(24));
+ }
+ else
+ {
+ slice.FileOffset = BinaryPrimitives.ReadUInt32BigEndian(entry.Slice(8));
+ slice.Size = BinaryPrimitives.ReadUInt32BigEndian(entry.Slice(12));
+ slice.AlignLog2 = BinaryPrimitives.ReadUInt32BigEndian(entry.Slice(16));
+ }
+
+ if (slice.AlignLog2 > MachOFatSlice.MaxAlignLog2)
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatSliceAlignment, $"Slice {i} has an alignment exponent of {slice.AlignLog2}, past the {MachOFatSlice.MaxAlignLog2} a universal binary allows");
+ file = null;
+ return false;
+ }
+
+ if (slice.Size < MachOFile.MinHeaderSize)
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatArchRange, $"Slice {i} is {slice.Size} bytes, too short to hold a Mach-O header");
+ file = null;
+ return false;
+ }
+
+ if (slice.FileOffset + slice.Size > (ulong)stream.Length)
+ {
+ bag.Error(DiagnosticId.MACHO_ERR_InvalidFatArchRange, $"Slice {i} spans [0x{slice.FileOffset:X}, 0x{slice.FileOffset + slice.Size:X}) which extends past the end of the file");
+ file = null;
+ return false;
+ }
+
+ // Each slice is a complete image, so it is read through a view bounded to the slice.
+ // Handing it the whole stream would let a malformed slice read its neighbours.
+ var sliceStream = new SubStream(stream, basePosition + (long)slice.FileOffset, (long)slice.Size);
+ if (!MachOFile.TryRead(sliceStream, out var sliceFile, out var sliceDiagnostics, options))
+ {
+ foreach (var message in sliceDiagnostics.Messages)
+ {
+ bag.Log(message);
+ }
+ file = null;
+ return false;
+ }
+
+ slice.File = sliceFile;
+ file.Slices.Add(slice);
+ }
+
+ diagnostics = null;
+ return true;
+ }
+
+ ///
+ /// Writes this universal binary to a stream.
+ ///
+ /// The stream to write to.
+ /// is null.
+ /// The universal binary or one of its slices is not writable.
+ ///
+ /// The stream is grown to hold the last slice if it is shorter, and is flushed before this
+ /// method returns but is not disposed.
+ ///
+ public void Write(Stream stream)
+ {
+ if (!TryWrite(stream, out var diagnostics))
+ {
+ throw new ObjectFileException($"Unexpected error while writing the Mach-O universal binary", diagnostics);
+ }
+ }
+
+ ///
+ /// Tries to write this universal binary to a stream.
+ ///
+ /// The stream to write to.
+ /// The diagnostics collected while writing.
+ /// true if the file was written without errors; otherwise false.
+ /// is null.
+ ///
+ /// Each slice is laid out before the slices are placed, so a slice that changed size since it
+ /// was read is recorded at the size it actually writes rather than overrunning the one after
+ /// it. The space between slices is left zeroed: every universal binary examined pads with
+ /// zeros, and slices are aligned rather than packed, so the gaps hold nothing worth
+ /// preserving.
+ ///
+ /// The stream is grown to hold the last slice if it is shorter, and is flushed before this
+ /// method returns but is not disposed.
+ ///
+ ///
+ public bool TryWrite(Stream stream, out DiagnosticBag diagnostics)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ diagnostics = new DiagnosticBag();
+
+ // Each slice is verified and laid out before the containing file is, because the size
+ // recorded for a slice has to be the size that slice goes on to write.
+ foreach (var slice in Slices)
+ {
+ if (slice.File is null) continue;
+
+ var context = new MachOVisitorContext(slice.File, diagnostics);
+ slice.File.Verify(context);
+ if (diagnostics.HasErrors) return false;
+
+ slice.File.UpdateLayout(context);
+ if (diagnostics.HasErrors) return false;
+ }
+
+ UpdateLayout();
+
+ Verify(diagnostics);
+ if (diagnostics.HasErrors) return false;
+
+ var basePosition = stream.Position;
+ var entrySize = GetSliceEntrySize(Is64BitOffsets);
+
+ var header = new byte[HeaderSize + Slices.Count * entrySize];
+ BinaryPrimitives.WriteUInt32BigEndian(header, Is64BitOffsets ? MachOMagic.FatMagic64 : MachOMagic.FatMagic);
+ BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(4), (uint)Slices.Count);
+
+ for (var i = 0; i < Slices.Count; i++)
+ {
+ var slice = Slices[i];
+ var entry = header.AsSpan(HeaderSize + i * entrySize);
+ BinaryPrimitives.WriteUInt32BigEndian(entry, (uint)slice.CpuType);
+ BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(4), slice.CpuSubType);
+
+ if (Is64BitOffsets)
+ {
+ BinaryPrimitives.WriteUInt64BigEndian(entry.Slice(8), slice.FileOffset);
+ BinaryPrimitives.WriteUInt64BigEndian(entry.Slice(16), slice.Size);
+ BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(24), slice.AlignLog2);
+ }
+ else
+ {
+ BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(8), (uint)slice.FileOffset);
+ BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(12), (uint)slice.Size);
+ BinaryPrimitives.WriteUInt32BigEndian(entry.Slice(16), slice.AlignLog2);
+ }
+ }
+
+ stream.Write(header);
+
+ foreach (var slice in Slices)
+ {
+ if (slice.File is null) continue;
+ stream.Position = basePosition + (long)slice.FileOffset;
+ var writer = new MachOWriter(slice.File, stream, diagnostics);
+ slice.File.Write(writer);
+ if (diagnostics.HasErrors) return false;
+ }
+
+ var end = Slices.Count == 0 ? 0 : Slices.Max(s => (long)(s.FileOffset + s.Size));
+ if (stream.Length < basePosition + end)
+ {
+ stream.SetLength(basePosition + end);
+ }
+
+ stream.Flush();
+
+ return !diagnostics.HasErrors;
+ }
+
+ ///
+ /// Recomputes where each slice sits and how large it is.
+ ///
+ ///
+ /// A slice is mapped straight out of the containing file, so it has to start on a page
+ /// boundary for its architecture. Sizes are computed from each slice rather than taken from
+ /// what it was read as, since an edited slice no longer matches that, and the header would
+ /// otherwise point the loader at a slice that overruns the one after it.
+ ///
+ public void UpdateLayout()
+ {
+ var cursor = (ulong)(HeaderSize + Slices.Count * GetSliceEntrySize(Is64BitOffsets));
+
+ foreach (var slice in Slices)
+ {
+ if (slice.File is null)
+ {
+ slice.Size = 0;
+ continue;
+ }
+
+ var alignment = slice.Alignment;
+ slice.FileOffset = AlignHelper.AlignUp(cursor, alignment);
+ slice.Size = slice.File.ComputeFileSize();
+ cursor = slice.FileOffset + slice.Size;
+ }
+ }
+
+ ///
+ public override string ToString() => $"{nameof(MachOFatFile)} {{ Slices = {Slices.Count} }}";
+}
diff --git a/src/LibObjectFile/MachO/MachOFatSlice.cs b/src/LibObjectFile/MachO/MachOFatSlice.cs
new file mode 100644
index 0000000..0e53ebe
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFatSlice.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+///
+/// One architecture's image inside a .
+///
+public sealed class MachOFatSlice
+{
+ ///
+ /// Gets or sets the CPU architecture of this slice. It repeats what the slice's own header
+ /// says, so that a loader can pick a slice without parsing it.
+ ///
+ public MachOCpuType CpuType { get; set; }
+
+ ///
+ /// Gets or sets the CPU subtype of this slice.
+ ///
+ public uint CpuSubType { get; set; }
+
+ ///
+ /// The largest alignment exponent a slice can carry.
+ ///
+ ///
+ /// This is the limit lipo enforces, which reports that -segalign "must be equal to or
+ /// less than 8000 (hex)". Real images use 12 to 14, the page size of the architecture.
+ ///
+ /// The bound matters beyond matching the tooling: a shift count is masked to the width of
+ /// the value it shifts, so an unchecked exponent would quietly alias to a different
+ /// alignment rather than being rejected.
+ ///
+ ///
+ public const uint MaxAlignLog2 = 15;
+
+ ///
+ /// Gets or sets the alignment of this slice as a power of two. It is the page size of the
+ /// architecture, because the slice is mapped directly out of the containing file.
+ ///
+ public uint AlignLog2 { get; set; }
+
+ ///
+ /// Gets or sets the offset of this slice in the containing file.
+ ///
+ public ulong FileOffset { get; set; }
+
+ ///
+ /// Gets or sets the size of this slice in the containing file.
+ ///
+ public ulong Size { get; set; }
+
+ ///
+ /// Gets or sets the image this slice holds.
+ ///
+ public MachOFile? File { get; set; }
+
+ ///
+ /// Gets the alignment of this slice in bytes.
+ ///
+ ///
+ /// Only meaningful while is within , which
+ /// the reader enforces and checks.
+ ///
+ public ulong Alignment => 1ul << (int)AlignLog2;
+
+ ///
+ public override string ToString()
+ => $"{nameof(MachOFatSlice)} {{ {CpuType}, FileOffset = 0x{FileOffset:X}, Size = 0x{Size:X}, Align = 2^{AlignLog2} }}";
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Edit.cs b/src/LibObjectFile/MachO/MachOFile.Edit.cs
new file mode 100644
index 0000000..8ac6353
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Edit.cs
@@ -0,0 +1,213 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Adds a dependency on , so dyld loads that library at startup.
+ ///
+ ///
+ /// The install name of the library. It is resolved by dyld rather than opened as a path, so
+ /// it may start with @rpath, @executable_path or @loader_path.
+ ///
+ /// The command that was added.
+ ///
+ /// The command is appended, never inserted, because dyld numbers libraries by their command
+ /// order and every binding in the symbol table refers to a library by that number. Appending
+ /// leaves the existing numbering alone.
+ ///
+ /// is null.
+ /// is empty, or the type is not a dylib command.
+ ///
+ /// The image has no room left before its first section. The message states the shortfall.
+ ///
+ public MachODylibCommand AddLoadDylib(string name)
+ => AddLoadDylib(name, MachOLoadCommandType.LoadDylib);
+
+ ///
+ /// The install name to record, as it will appear to dyld.
+ ///
+ /// Which dylib command to add. makes the
+ /// library optional at runtime.
+ ///
+ public MachODylibCommand AddLoadDylib(string name, MachOLoadCommandType type)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ if (type is not (MachOLoadCommandType.LoadDylib
+ or MachOLoadCommandType.LoadWeakDylib
+ or MachOLoadCommandType.ReexportDylib
+ or MachOLoadCommandType.LoadUpwardDylib
+ or MachOLoadCommandType.LazyLoadDylib))
+ {
+ throw new ArgumentException($"{type} is not a dylib load command", nameof(type));
+ }
+
+ var command = new MachODylibCommand
+ {
+ Type = type,
+ Is64Bit = Is64Bit,
+ Name = name,
+ Timestamp = 0,
+ // A dependency the loader has no version expectations of. dyld only compares these
+ // against the target library's own values, and 1.0.0 always satisfies the comparison.
+ CurrentVersion = 0x10000,
+ CompatibilityVersion = 0x10000,
+ };
+
+ AppendCommand(command);
+ return command;
+ }
+
+ ///
+ /// Adds a runpath that @rpath expands to when resolving dependencies.
+ ///
+ /// The directory to add, commonly relative to @executable_path.
+ /// The command that was added.
+ /// is null.
+ /// is empty.
+ ///
+ /// The image has no room left before its first section. The message states the shortfall.
+ ///
+ public MachOPathCommand AddRPath(string path)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(path);
+
+ var command = new MachOPathCommand
+ {
+ Type = MachOLoadCommandType.RPath,
+ Is64Bit = Is64Bit,
+ Path = path,
+ };
+
+ AppendCommand(command);
+ return command;
+ }
+
+ ///
+ /// Removes a runpath.
+ ///
+ /// The runpath to remove.
+ /// true if a matching runpath was removed; otherwise false.
+ ///
+ /// Unlike the dylib commands, runpaths are unnumbered, so dropping one changes nothing but
+ /// the search order.
+ ///
+ /// is null.
+ public bool RemoveRPath(string path)
+ {
+ ArgumentNullException.ThrowIfNull(path);
+
+ var command = RunPaths.FirstOrDefault(c => c.Path == path);
+ if (command is null) return false;
+
+ LoadCommands.Remove(command);
+ MarkCodeSignatureStale();
+ return true;
+ }
+
+ ///
+ /// Repoints every reference to at ,
+ /// which is what install_name_tool -change does.
+ ///
+ /// The install name currently referenced.
+ /// The install name to reference instead.
+ /// The number of commands that were repointed.
+ /// or is null.
+ /// is empty.
+ ///
+ /// The longer name does not fit in the room left before the first section.
+ ///
+ public int ChangeDylibName(string oldName, string newName)
+ {
+ ArgumentNullException.ThrowIfNull(oldName);
+ ArgumentException.ThrowIfNullOrEmpty(newName);
+
+ var matches = LoadCommands.OfType().Where(c => c.Name == oldName).ToArray();
+ if (matches.Length == 0) return 0;
+
+ // Work out what the new name costs before assigning it, so an edit that does not fit
+ // leaves the image exactly as it was rather than half renamed.
+ long extra = 0;
+ foreach (var command in matches)
+ {
+ extra += Math.Max(0, (long)command.ComputeMinimumSize(newName) - (long)command.Size);
+ }
+
+ EnsureLoadCommandSpace(extra);
+
+ foreach (var command in matches)
+ {
+ command.Name = newName;
+ command.Size = Math.Max(command.Size, command.MinimumSize);
+ }
+
+ MarkCodeSignatureStale();
+ return matches.Length;
+ }
+
+ ///
+ /// Sets this image's own install name, which is what install_name_tool -id does.
+ ///
+ /// The install name to record.
+ /// is null.
+ /// is empty.
+ ///
+ /// This image is not a dylib and so has no install name, or the longer name does not fit.
+ ///
+ public void SetInstallName(string name)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(name);
+
+ var command = IdDylib
+ ?? throw new InvalidOperationException("This image has no LC_ID_DYLIB command, so it is not a dylib and has no install name.");
+
+ EnsureLoadCommandSpace(Math.Max(0, (long)command.ComputeMinimumSize(name) - (long)command.Size));
+
+ command.Name = name;
+ command.Size = Math.Max(command.Size, command.MinimumSize);
+ MarkCodeSignatureStale();
+ }
+
+ private void AppendCommand(MachOLoadCommand command)
+ {
+ command.Size = command is MachOPathLoadCommand path ? path.MinimumSize : command.Size;
+ EnsureLoadCommandSpace((long)command.Size);
+
+ // Only once the command is known to fit, since a signature is not stale if nothing changed.
+ LoadCommands.Add(command);
+ MarkCodeSignatureStale();
+ }
+
+ ///
+ /// Records that the image no longer matches its signature, if it has one.
+ ///
+ private void MarkCodeSignatureStale()
+ {
+ if (CodeSignature is not null)
+ {
+ IsCodeSignatureStale = true;
+ }
+ }
+
+ ///
+ /// Checks that the command table can grow by without
+ /// pushing into the first section.
+ ///
+ private void EnsureLoadCommandSpace(long additionalBytes)
+ {
+ if (additionalBytes <= AvailableLoadCommandSpace) return;
+
+ throw new InvalidOperationException(
+ $"The load commands need {additionalBytes} more bytes but only {AvailableLoadCommandSpace} are free before the first section at 0x{ContentStartOffset:X}. " +
+ "Making room would mean moving content, which would invalidate the addresses in this image.");
+ }
+
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Read.cs b/src/LibObjectFile/MachO/MachOFile.Read.cs
new file mode 100644
index 0000000..bb95885
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Read.cs
@@ -0,0 +1,445 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Reads a Mach-O image from a stream.
+ ///
+ /// The stream positioned at the start of the image.
+ /// Options controlling how content is read, or null for the defaults.
+ /// The image read.
+ /// is null.
+ /// The stream does not contain a readable Mach-O image.
+ public static MachOFile Read(Stream stream, MachOReaderOptions? options = null)
+ {
+ if (!TryRead(stream, out var file, out var diagnostics, options))
+ {
+ throw new ObjectFileException($"Unexpected error while reading the Mach-O file", diagnostics);
+ }
+ return file;
+ }
+
+ ///
+ /// Tries to read a Mach-O image from a stream.
+ ///
+ /// The stream positioned at the start of the image.
+ /// The image read, if reading succeeded.
+ /// The diagnostics collected, if reading failed.
+ /// Options controlling how content is read, or null for the defaults.
+ /// true if the image was read; otherwise false.
+ /// is null.
+ public static bool TryRead(Stream stream, [NotNullWhen(true)] out MachOFile? file, [NotNullWhen(false)] out DiagnosticBag? diagnostics, MachOReaderOptions? options = null)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ file = new MachOFile();
+ var reader = new MachOReader(file, stream, options ?? new MachOReaderOptions());
+ diagnostics = reader.Diagnostics;
+
+ try
+ {
+ file.Read(reader);
+ }
+ catch (EndOfStreamException)
+ {
+ // Every read in the walk is bounded before it is taken, so arriving here means one of
+ // those bounds was missed rather than that the caller passed a short stream. Either
+ // way a Try method owes the caller a diagnostic rather than an exception.
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_UnexpectedEndOfStream,
+ "The image ends inside a structure the file declares, so it is truncated or malformed");
+ }
+
+ if (reader.Diagnostics.HasErrors)
+ {
+ file = null;
+ return false;
+ }
+
+ diagnostics = null;
+ return true;
+ }
+
+ ///
+ /// Checks whether a stream starts with a thin Mach-O magic, without consuming it.
+ ///
+ /// The stream to inspect.
+ /// true if the stream starts with a Mach-O magic.
+ /// is null.
+ public static bool IsMachO(Stream stream)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+ var position = stream.Position;
+ try
+ {
+ Span magic = stackalloc byte[4];
+ if (stream.Read(magic) != 4) return false;
+ var value = BitConverter.ToUInt32(magic);
+ return value is MachOMagic.Magic32 or MachOMagic.Magic64 or MachOMagic.Cigam32 or MachOMagic.Cigam64;
+ }
+ finally
+ {
+ stream.Position = position;
+ }
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ Position = reader.Position;
+
+ if (!reader.TryReadData(sizeof(uint), out uint magic))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_InvalidMagic, "The stream is too short to hold a Mach-O magic");
+ return;
+ }
+
+ reader.Position -= 4;
+
+ switch (magic)
+ {
+ case MachOMagic.Magic32:
+ Is64Bit = false;
+ break;
+ case MachOMagic.Magic64:
+ Is64Bit = true;
+ break;
+ case MachOMagic.Cigam32:
+ case MachOMagic.Cigam64:
+ // Reading these means byte-swapping every field. The only architectures that
+ // shipped big-endian Mach-O are PowerPC and 68k, so nothing is done here rather
+ // than carrying a swapping reader that nothing exercises.
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_UnsupportedByteOrder, $"Big-endian Mach-O images are not supported (magic 0x{magic:X8})");
+ return;
+ case MachOMagic.FatCigam:
+ case MachOMagic.FatCigam64:
+ // The fat magics are stored big-endian, so a little-endian read of a well-formed
+ // universal binary sees the swapped spelling.
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_UnexpectedFatFile, "This is a universal binary rather than a single Mach-O image. Read it with MachOFatFile and pick a slice.");
+ return;
+ case ArchiveMagic:
+ // A universal static library is a fat file whose slices are ar archives rather
+ // than images, so this is what reading one slice by slice arrives at.
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_UnexpectedArchive, "This is an ar archive rather than a Mach-O image. Read it with ArArchiveFile.");
+ return;
+ default:
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_InvalidMagic, $"Invalid Mach-O magic 0x{magic:X8}");
+ return;
+ }
+
+ uint numberOfCommands;
+ uint sizeOfCommands;
+ if (Is64Bit)
+ {
+ if (!reader.TryReadData(sizeof(RawMachHeader64), out RawMachHeader64 header))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_InvalidMagic, "Truncated 64-bit Mach-O header");
+ return;
+ }
+ CpuType = (MachOCpuType)header.CpuType;
+ CpuSubType = header.CpuSubType;
+ FileType = (MachOFileType)header.FileType;
+ Flags = (MachOHeaderFlags)header.Flags;
+ Reserved = header.Reserved;
+ numberOfCommands = header.NumberOfCommands;
+ sizeOfCommands = header.SizeOfCommands;
+ }
+ else
+ {
+ if (!reader.TryReadData(sizeof(RawMachHeader32), out RawMachHeader32 header))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_InvalidMagic, "Truncated 32-bit Mach-O header");
+ return;
+ }
+ CpuType = (MachOCpuType)header.CpuType;
+ CpuSubType = header.CpuSubType;
+ FileType = (MachOFileType)header.FileType;
+ Flags = (MachOHeaderFlags)header.Flags;
+ numberOfCommands = header.NumberOfCommands;
+ sizeOfCommands = header.SizeOfCommands;
+ }
+
+ ReadLoadCommands(reader, numberOfCommands, sizeOfCommands);
+ if (reader.Diagnostics.HasErrors) return;
+
+ ReadContent(reader);
+ }
+
+ // The first four bytes of "!\n", read the way the magic above is.
+ private const uint ArchiveMagic = 0x72613C21;
+
+ ///
+ /// Walks the load command table. The table is bounded by the sizeofcmds the header
+ /// declares, each command by its own cmdsize, so a command cannot reach into the one
+ /// after it or into the content beyond the table.
+ ///
+ private void ReadLoadCommands(MachOReader reader, uint numberOfCommands, uint sizeOfCommands)
+ {
+ var commandAlignment = MachOLoadCommand.GetSizeAlignment(Is64Bit);
+ var tableEnd = HeaderSize + (ulong)sizeOfCommands;
+
+ if (tableEnd > reader.Length)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_TruncatedLoadCommand,
+ $"The header declares {sizeOfCommands} bytes of load commands, which extend past the end of the file");
+ return;
+ }
+
+ for (uint i = 0; i < numberOfCommands; i++)
+ {
+ var commandPosition = reader.Position;
+ if (commandPosition + 8 > tableEnd)
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Load command {i} starts past the end of the load command table");
+ return;
+ }
+
+ var type = (MachOLoadCommandType)reader.ReadU32();
+ var commandSize = reader.ReadU32();
+ reader.Position = commandPosition;
+
+ if (commandSize < 8 || (commandSize % commandAlignment) != 0)
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_InvalidLoadCommandSize, $"Load command {i} has an invalid cmdsize of {commandSize}, which must be at least 8 and a multiple of {commandAlignment}");
+ return;
+ }
+
+ if (commandPosition + commandSize > tableEnd)
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Load command {i} of size {commandSize} extends past the end of the load command table");
+ return;
+ }
+
+ MachOLoadCommand command = type switch
+ {
+ MachOLoadCommandType.Segment => new MachOSegment { Is64Bit = false },
+ MachOLoadCommandType.Segment64 => new MachOSegment { Is64Bit = true },
+ MachOLoadCommandType.LoadDylib
+ or MachOLoadCommandType.IdDylib
+ or MachOLoadCommandType.LoadWeakDylib
+ or MachOLoadCommandType.ReexportDylib
+ or MachOLoadCommandType.LoadUpwardDylib
+ or MachOLoadCommandType.LazyLoadDylib => new MachODylibCommand { Is64Bit = Is64Bit },
+ MachOLoadCommandType.RPath
+ or MachOLoadCommandType.LoadDylinker
+ or MachOLoadCommandType.IdDylinker
+ or MachOLoadCommandType.DyldEnvironment => new MachOPathCommand { Is64Bit = Is64Bit },
+ MachOLoadCommandType.SymbolTable => new MachOSymbolTableCommand(),
+ MachOLoadCommandType.TwoLevelHints => new MachOTwoLevelHintsCommand(),
+ MachOLoadCommandType.Thread or MachOLoadCommandType.UnixThread => new MachOThreadCommand(),
+ MachOLoadCommandType.Main => new MachOMainCommand(),
+ MachOLoadCommandType.Uuid => new MachOUuidCommand(),
+ MachOLoadCommandType.BuildVersion => new MachOBuildVersionCommand(),
+ MachOLoadCommandType.SourceVersion => new MachOSourceVersionCommand(),
+ MachOLoadCommandType.VersionMinMacOSX
+ or MachOLoadCommandType.VersionMinIPhoneOS
+ or MachOLoadCommandType.VersionMinTvOS
+ or MachOLoadCommandType.VersionMinWatchOS => new MachOVersionMinCommand(),
+ MachOLoadCommandType.DyldInfo or MachOLoadCommandType.DyldInfoOnly => new MachODyldInfoCommand(),
+ MachOLoadCommandType.DynamicSymbolTable => new MachODynamicSymbolTableCommand(),
+ MachOLoadCommandType.CodeSignature
+ or MachOLoadCommandType.FunctionStarts
+ or MachOLoadCommandType.DataInCode
+ or MachOLoadCommandType.DyldExportsTrie
+ or MachOLoadCommandType.DyldChainedFixups
+ or MachOLoadCommandType.SegmentSplitInfo
+ or MachOLoadCommandType.DylibCodeSignDrs
+ or MachOLoadCommandType.LinkerOptimizationHint => new MachOLinkEditDataCommand(),
+ _ => new MachOUnknownLoadCommand(),
+ };
+
+ command.Type = type;
+ command.Position = commandPosition;
+ command.Size = commandSize;
+
+ if (commandSize < command.MinimumCommandSize)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidLoadCommandSize,
+ $"Load command {i} of type {type} has a cmdsize of {commandSize}, which is smaller than the {command.MinimumCommandSize} bytes its fixed part needs");
+ return;
+ }
+
+ LoadCommands.Add(command);
+
+ command.Read(reader);
+ if (reader.Diagnostics.HasErrors) return;
+
+ // A command that read past its own cmdsize took bytes belonging to the next one, so
+ // whatever it decoded is not what the file says.
+ if (reader.Position > commandPosition + commandSize)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_LoadCommandOverread,
+ $"Load command {i} of type {type} read to 0x{reader.Position:X}, past the 0x{commandPosition + commandSize:X} its cmdsize allows");
+ return;
+ }
+
+ reader.Position = commandPosition + commandSize;
+ }
+
+ if (reader.Position != tableEnd)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_LoadCommandTableSizeMismatch,
+ $"The load commands end at 0x{reader.Position:X} but the header declares the table ends at 0x{tableEnd:X}");
+ }
+ }
+
+ ///
+ /// Turns the rest of the file into content, so that every byte belongs to something. The
+ /// header and the load command table come first, then whatever the commands point at, and
+ /// the gaps between them are kept as they are rather than regenerated.
+ ///
+ private void ReadContent(MachOReader reader)
+ {
+ var commandsEnd = (ulong)LoadCommandsEndOffset;
+ var fileLength = reader.Length;
+
+ Content.Add(new MachOHeaderContent { Position = 0, Size = HeaderSize });
+ Content.Add(new MachOLoadCommandTable { Position = HeaderSize, Size = SizeOfCommands });
+
+ var regions = CollectKnownRegions(reader, commandsEnd, fileLength);
+ if (reader.Diagnostics.HasErrors) return;
+
+ regions.Sort((left, right) => left.Offset.CompareTo(right.Offset));
+
+ var cursor = commandsEnd;
+ var firstGap = true;
+
+ foreach (var region in regions)
+ {
+ // Overlapping regions would mean the same bytes belong to two things; keep the first.
+ if (region.Offset < cursor) continue;
+
+ if (region.Offset > cursor)
+ {
+ var gap = AddStreamContent(reader, cursor, region.Offset - cursor, firstGap);
+ if (firstGap)
+ {
+ LoadCommandPadding = gap;
+ firstGap = false;
+ }
+ }
+
+ reader.Position = region.Offset;
+ var content = region.Section is null
+ ? new MachOStreamContent(reader.ReadAsStream(region.Size))
+ : new MachOSectionData(region.Section, reader.ReadAsStream(region.Size));
+ content.Position = region.Offset;
+ Content.Add(content);
+ cursor = region.Offset + region.Size;
+ firstGap = false;
+ }
+
+ if (cursor < fileLength)
+ {
+ var trailing = AddStreamContent(reader, cursor, fileLength - cursor, firstGap);
+ if (firstGap) LoadCommandPadding = trailing;
+ }
+ }
+
+ ///
+ /// A run of bytes something in the image points at.
+ ///
+ private readonly record struct KnownRegion(ulong Offset, ulong Size, MachOSection? Section);
+
+ ///
+ /// Finds every run of bytes the header or a load command points at. What is left over between
+ /// them is padding, and is kept verbatim.
+ ///
+ private List CollectKnownRegions(MachOReader reader, ulong commandsEnd, ulong fileLength)
+ {
+ var regions = new List();
+
+ // Sizes are counts from the file times an entry size, computed wide: a count large enough
+ // to wrap a 32-bit product would otherwise name a small region and leave the rest of the
+ // table looking like padding.
+ void Add(ulong offset, ulong size, MachOSection? section = null)
+ {
+ // An offset of zero means the data is absent rather than at the start of the file.
+ if (offset < commandsEnd || size == 0) return;
+
+ if (offset + size > fileLength)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidContentFileRange,
+ $"Content at 0x{offset:X} for 0x{size:X} bytes extends past the end of the file");
+ return;
+ }
+
+ regions.Add(new KnownRegion(offset, size, section));
+ }
+
+ foreach (var segment in Segments)
+ {
+ foreach (var section in segment.Sections)
+ {
+ if (!section.IsZeroFill)
+ {
+ Add(section.FileOffset, section.Size, section);
+ }
+
+ Add(section.RelocationOffset, (ulong)section.NumberOfRelocations * MachORelocation.EntrySize);
+ }
+ }
+
+ foreach (var command in LoadCommands)
+ {
+ switch (command)
+ {
+ case MachOSymbolTableCommand symtab:
+ Add(symtab.SymbolOffset, symtab.SymbolCount * (ulong)MachOSymbolTableCommand.GetSymbolSize(Is64Bit));
+ Add(symtab.StringOffset, symtab.StringSize);
+ break;
+ case MachODynamicSymbolTableCommand dysymtab:
+ Add(dysymtab.TableOfContentsOffset, (ulong)dysymtab.TableOfContentsCount * MachODynamicSymbolTableCommand.TableOfContentsEntrySize);
+ Add(dysymtab.ModuleTableOffset, (ulong)dysymtab.ModuleTableCount * MachODynamicSymbolTableCommand.GetModuleTableEntrySize(Is64Bit));
+ Add(dysymtab.ExternalReferenceOffset, (ulong)dysymtab.ExternalReferenceCount * MachODynamicSymbolTableCommand.ExternalReferenceEntrySize);
+ Add(dysymtab.IndirectSymbolOffset, (ulong)dysymtab.IndirectSymbolCount * MachODynamicSymbolTableCommand.IndirectSymbolEntrySize);
+ Add(dysymtab.ExternalRelocationOffset, (ulong)dysymtab.ExternalRelocationCount * MachORelocation.EntrySize);
+ Add(dysymtab.LocalRelocationOffset, (ulong)dysymtab.LocalRelocationCount * MachORelocation.EntrySize);
+ break;
+ case MachODyldInfoCommand dyldInfo:
+ Add(dyldInfo.RebaseOffset, dyldInfo.RebaseSize);
+ Add(dyldInfo.BindOffset, dyldInfo.BindSize);
+ Add(dyldInfo.WeakBindOffset, dyldInfo.WeakBindSize);
+ Add(dyldInfo.LazyBindOffset, dyldInfo.LazyBindSize);
+ Add(dyldInfo.ExportOffset, dyldInfo.ExportSize);
+ break;
+ case MachOLinkEditDataCommand data:
+ Add(data.DataOffset, data.DataSize);
+ break;
+ case MachOTwoLevelHintsCommand hints:
+ Add(hints.Offset, (ulong)hints.HintCount * MachOTwoLevelHintsCommand.HintSize);
+ break;
+ }
+ }
+
+ return regions;
+ }
+
+ private MachOStreamContent AddStreamContent(MachOReader reader, ulong offset, ulong size, bool isLoadCommandPadding)
+ {
+ reader.Position = offset;
+ var stream = reader.ReadAsStream(size);
+ MachOStreamContent content = isLoadCommandPadding
+ ? new MachOLoadCommandPadding(stream)
+ : new MachOStreamContent(stream);
+ content.Position = offset;
+ Content.Add(content);
+ return content;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Relocations.cs b/src/LibObjectFile/MachO/MachOFile.Relocations.cs
new file mode 100644
index 0000000..cc3d828
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Relocations.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Reads the relocations of a section.
+ ///
+ /// The section whose relocations to read.
+ /// The relocations, or an empty list if the section has none.
+ ///
+ /// Only an object file normally carries these. A linked image resolves them at link time and
+ /// keeps just what the loader still needs, as dyld info opcodes or chained fixups.
+ ///
+ /// A pair of entries can describe one fixup between them, with the second carrying the other
+ /// half. They are returned in file order so a caller can pair them up.
+ ///
+ ///
+ /// is null.
+ /// The relocations lie outside the content of this image.
+ public IReadOnlyList ReadRelocations(MachOSection section)
+ {
+ ArgumentNullException.ThrowIfNull(section);
+
+ if (section.NumberOfRelocations == 0) return [];
+
+ var bytes = ReadFileBytes(section.RelocationOffset, (ulong)section.NumberOfRelocations * MachORelocation.EntrySize, $"relocations of {section.SegmentName},{section.Name}");
+ var relocations = new MachORelocation[section.NumberOfRelocations];
+ for (var i = 0; i < relocations.Length; i++)
+ {
+ relocations[i] = MachORelocation.Decode(
+ BitConverter.ToUInt32(bytes, i * (int)MachORelocation.EntrySize),
+ BitConverter.ToUInt32(bytes, i * (int)MachORelocation.EntrySize + 4));
+ }
+
+ return relocations;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Sign.cs b/src/LibObjectFile/MachO/MachOFile.Sign.cs
new file mode 100644
index 0000000..2c68833
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Sign.cs
@@ -0,0 +1,174 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+using System.Linq;
+using LibObjectFile.MachO.CodeSign;
+using LibObjectFile.Utils;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Replaces this image's code signature with an ad-hoc one, adding the signature and its load
+ /// command if the image is unsigned.
+ ///
+ ///
+ /// The signing identity to record. Apple's tooling uses the binary's file name, and this is
+ /// the string the kernel reports as the code's identity.
+ ///
+ ///
+ /// An ad-hoc signature carries no certificate; it asserts only that the image matches its own
+ /// hashes. Apple Silicon refuses to execute an unsigned image, so an edited arm64 binary has
+ /// to be re-signed to stay runnable. Any change to the image invalidates the signature, so
+ /// this has to be the last thing done before writing.
+ ///
+ /// is null.
+ /// is empty.
+ ///
+ /// The image has no __LINKEDIT or __TEXT segment, has content past the point
+ /// the signature would occupy, or has no room left for the signature load command.
+ ///
+ public void AdHocSign(string identifier)
+ {
+ ArgumentException.ThrowIfNullOrEmpty(identifier);
+
+ var linkEdit = FindSegment("__LINKEDIT")
+ ?? throw new InvalidOperationException("The image has no __LINKEDIT segment, so there is nowhere to put a signature.");
+ var text = FindSegment("__TEXT")
+ ?? throw new InvalidOperationException("The image has no __TEXT segment, so the signature cannot describe its executable range.");
+
+ // Everything below can fail, and a half-signed image is worse than an unsigned one, so
+ // what changes is remembered first and put back if it does. The state is captured before
+ // the command is added, since adding one is itself a change to undo.
+ var previousFileSize = linkEdit.FileSize;
+ var previousVmSize = linkEdit.VmSize;
+ var previousStale = IsCodeSignatureStale;
+ var added = new List();
+ MachOContent? removed = null;
+ var removedIndex = -1;
+ MachOLinkEditDataCommand? addedCommand = null;
+
+ var command = CodeSignature;
+ var previousOffset = command?.DataOffset ?? 0;
+ var previousSize = command?.DataSize ?? 0;
+
+ try
+ {
+ if (command is null)
+ {
+ command = new MachOLinkEditDataCommand
+ {
+ Type = MachOLoadCommandType.CodeSignature,
+ Size = MachOLinkEditDataCommand.CommandSize,
+ };
+ AppendCommand(command);
+ addedCommand = command;
+ }
+ // A signature covers everything before it, so it has to be the last thing in the file.
+ // Whatever occupied that place before, including a previous signature, is replaced.
+ if (command.DataOffset != 0)
+ {
+ for (var i = Content.Count - 1; i >= 0; i--)
+ {
+ if (Content[i].Position == command.DataOffset)
+ {
+ removed = Content[i];
+ removedIndex = i;
+ Content.RemoveAt(i);
+ break;
+ }
+ }
+ }
+
+ var contentEnd = ComputeFileSize();
+
+ // LC_CODE_SIGNATURE records where the signature is in a 32-bit dataoff, so an image
+ // this large cannot carry one. Casting would point the offset back into the image.
+ if (contentEnd > uint.MaxValue)
+ {
+ throw new InvalidOperationException($"The image is 0x{contentEnd:X} bytes, too large for the 32-bit offset LC_CODE_SIGNATURE records.");
+ }
+
+ var signatureOffset = AlignHelper.AlignUp((uint)contentEnd, (uint)MachOCodeSignatureConstants.SignatureAlignment);
+
+ var builder = new MachOAdHocSignatureBuilder(identifier)
+ {
+ CodeLimit = signatureOffset,
+ ExecSegmentBase = text.FileOffset,
+ ExecSegmentLimit = text.FileSize,
+ ExecSegmentFlags = FileType == MachOFileType.Execute ? MachOCodeSignatureConstants.ExecSegMainBinary : 0,
+ };
+ var signatureSize = builder.ComputeSize();
+
+ command.DataOffset = signatureOffset;
+ command.DataSize = signatureSize;
+
+ linkEdit.FileSize = signatureOffset + signatureSize - linkEdit.FileOffset;
+
+ // The signature is far larger than the slack the linker left, so __LINKEDIT grows and
+ // has to be rounded to a segment boundary the way codesign rounds it. Without this it
+ // ends mid-page, which is a shape no linker and no signing tool produces.
+ linkEdit.VmSize = AlignHelper.AlignUp(Math.Max(linkEdit.VmSize, linkEdit.FileSize), SegmentAlignment);
+
+ // Aligning the signature can leave a gap, and every byte of the file has to belong to
+ // some content, so the padding is added rather than left as a hole in the list.
+ if (signatureOffset > contentEnd)
+ {
+ var gap = new MachOStreamContent(new MemoryStream(new byte[signatureOffset - contentEnd]))
+ {
+ Position = contentEnd,
+ };
+ Content.Add(gap);
+ added.Add(gap);
+ }
+
+ var placeholder = new MachOStreamContent(new MemoryStream(new byte[signatureSize])) { Position = signatureOffset };
+ Content.Add(placeholder);
+ added.Add(placeholder);
+
+ // Signing is what makes the image match its signature again, so the edit is settled here
+ // rather than after the digests are taken. Writing checks this, and the digests are taken
+ // by writing the image out.
+ IsCodeSignatureStale = false;
+
+ // The digests cover the image as it will finally be written, so the header, the command
+ // table and the signature's own offsets all have to be settled before they are taken.
+ var image = new MemoryStream();
+ Write(image);
+ placeholder.Content = new MemoryStream(builder.Build(image.GetBuffer().AsSpan(0, (int)signatureOffset)));
+ }
+ catch
+ {
+ foreach (var content in added)
+ {
+ Content.Remove(content);
+ }
+
+ if (removed is not null)
+ {
+ Content.Insert(removedIndex, removed);
+ }
+
+ if (addedCommand is not null)
+ {
+ LoadCommands.Remove(addedCommand);
+ }
+ else if (command is not null)
+ {
+ command.DataOffset = previousOffset;
+ command.DataSize = previousSize;
+ }
+
+ linkEdit.FileSize = previousFileSize;
+ linkEdit.VmSize = previousVmSize;
+ IsCodeSignatureStale = previousStale;
+ throw;
+ }
+ }
+
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Symbols.cs b/src/LibObjectFile/MachO/MachOFile.Symbols.cs
new file mode 100644
index 0000000..ad2d863
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Symbols.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Reads the symbol table.
+ ///
+ ///
+ /// The symbols in table order, or an empty list if the image has no LC_SYMTAB. The
+ /// order matters, because LC_DYSYMTAB describes runs of it by index.
+ ///
+ ///
+ /// This decodes a snapshot rather than returning a live view. Writing the file does not carry
+ /// changes made to the returned symbols back into it, since resizing the table would move
+ /// everything after it in __LINKEDIT.
+ ///
+ /// The table lies outside the content of this image.
+ public IReadOnlyList ReadSymbolTable()
+ {
+ var command = LoadCommands.OfType().FirstOrDefault();
+ if (command is null || command.SymbolCount == 0) return [];
+
+ var entrySize = MachOSymbolTableCommand.GetSymbolSize(Is64Bit);
+ var entries = ReadFileBytes(command.SymbolOffset, (ulong)command.SymbolCount * entrySize, "symbol table");
+ var strings = ReadFileBytes(command.StringOffset, command.StringSize, "string table");
+
+ var symbols = new List((int)command.SymbolCount);
+ for (var i = 0; i < command.SymbolCount; i++)
+ {
+ var span = entries.AsSpan((int)(i * entrySize));
+ var symbol = Is64Bit ? ReadSymbol64(span) : ReadSymbol32(span);
+ symbol.Name = ReadString(strings, symbol.NameOffset);
+ symbols.Add(symbol);
+ }
+
+ return symbols;
+ }
+
+ ///
+ /// Reads the indirect symbol table, which is what the stub and symbol pointer sections index
+ /// into through their reserved1 field.
+ ///
+ /// The symbol table indices, or an empty list if the image has no indirect table.
+ ///
+ /// Two values are not indices: and
+ /// mark entries the loader has nothing to bind.
+ ///
+ /// The table lies outside the content of this image.
+ public IReadOnlyList ReadIndirectSymbolTable()
+ {
+ var command = LoadCommands.OfType().FirstOrDefault();
+ if (command is null || command.IndirectSymbolCount == 0) return [];
+
+ var bytes = ReadFileBytes(command.IndirectSymbolOffset, (ulong)command.IndirectSymbolCount * MachODynamicSymbolTableCommand.IndirectSymbolEntrySize, "indirect symbol table");
+ var indices = new uint[command.IndirectSymbolCount];
+ for (var i = 0; i < indices.Length; i++)
+ {
+ indices[i] = BitConverter.ToUInt32(bytes, i * (int)MachODynamicSymbolTableCommand.IndirectSymbolEntrySize);
+ }
+
+ return indices;
+ }
+
+ /// The entry is a local symbol the loader does not bind (INDIRECT_SYMBOL_LOCAL).
+ public const uint IndirectSymbolLocal = 0x80000000;
+
+ /// The entry is an absolute symbol the loader does not bind (INDIRECT_SYMBOL_ABS).
+ public const uint IndirectSymbolAbsolute = 0x40000000;
+
+ private static unsafe MachOSymbol ReadSymbol64(ReadOnlySpan span)
+ {
+ var raw = System.Runtime.InteropServices.MemoryMarshal.Read(span);
+ return new MachOSymbol
+ {
+ NameOffset = raw.StringIndex,
+ RawType = raw.Type,
+ SectionIndex = raw.SectionIndex,
+ Description = raw.Description,
+ Value = raw.Value,
+ };
+ }
+
+ private static unsafe MachOSymbol ReadSymbol32(ReadOnlySpan span)
+ {
+ var raw = System.Runtime.InteropServices.MemoryMarshal.Read(span);
+ return new MachOSymbol
+ {
+ NameOffset = raw.StringIndex,
+ RawType = raw.Type,
+ SectionIndex = raw.SectionIndex,
+ Description = raw.Description,
+ Value = raw.Value,
+ };
+ }
+
+ private static string ReadString(byte[] strings, uint offset)
+ {
+ if (offset >= strings.Length) return string.Empty;
+
+ var span = strings.AsSpan((int)offset);
+ var end = span.IndexOf((byte)0);
+ if (end < 0) end = span.Length;
+ return Encoding.UTF8.GetString(span.Slice(0, end));
+ }
+
+ ///
+ /// Reads a run of bytes at a file offset out of whichever content covers it.
+ ///
+ private byte[] ReadFileBytes(uint offset, ulong length, string what)
+ {
+ if (length == 0) return [];
+
+ // The length is a count from the file multiplied by an entry size, so it is computed in
+ // 64 bits: a count large enough to wrap a 32-bit product would otherwise pass this check
+ // as a small length and then be read past.
+ if (length > int.MaxValue || offset + length > uint.MaxValue)
+ {
+ Throw(offset, length, what);
+ }
+
+ foreach (var content in Content)
+ {
+ if (content is not MachOStreamContent stream || offset < content.Position) continue;
+
+ var start = offset - content.Position;
+ if (start + length > (ulong)stream.Content.Length) continue;
+
+ var buffer = new byte[length];
+ stream.Content.Position = (long)start;
+ stream.Content.ReadExactly(buffer);
+ return buffer;
+ }
+
+ Throw(offset, length, what);
+ return [];
+ }
+
+ [System.Diagnostics.CodeAnalysis.DoesNotReturn]
+ private static void Throw(uint offset, ulong length, string what)
+ {
+ var message = $"The {what} at 0x{offset:X} for 0x{length:X} bytes is not covered by any content of this image.";
+ var diagnostics = new DiagnosticBag();
+ diagnostics.Error(DiagnosticId.MACHO_ERR_DataOutsideImage, message);
+ throw new ObjectFileException(message, diagnostics);
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Verify.cs b/src/LibObjectFile/MachO/MachOFile.Verify.cs
new file mode 100644
index 0000000..bdb172b
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Verify.cs
@@ -0,0 +1,192 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Checks this image for inconsistencies.
+ ///
+ /// What was found. Empty if nothing was.
+ public DiagnosticBag Verify()
+ {
+ var diagnostics = new DiagnosticBag();
+ Verify(diagnostics);
+ return diagnostics;
+ }
+
+ ///
+ /// Checks this image for inconsistencies.
+ ///
+ /// Receives what was found.
+ /// is null.
+ public void Verify(DiagnosticBag diagnostics)
+ {
+ ArgumentNullException.ThrowIfNull(diagnostics);
+ Verify(new MachOVisitorContext(this, diagnostics));
+ }
+
+ ///
+ public override void Verify(MachOVisitorContext context)
+ {
+ VerifyContentCoversTheFile(context);
+ VerifySegments(context);
+ VerifySectionContent(context);
+ VerifyLoadCommands(context);
+
+ if (IsCodeSignatureStale)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_StaleCodeSignature,
+ "The image has been edited since it was signed, so its signature no longer covers what it contains.");
+ }
+ }
+
+ ///
+ /// A section header says where its bytes are and how many there are, while the bytes
+ /// themselves are a separate piece of content. Nothing keeps the two in step, so a header
+ /// edited on its own would describe a section that is not there, and the round-trip would
+ /// still match because both were written from what they each hold.
+ ///
+ private void VerifySectionContent(MachOVisitorContext context)
+ {
+ var byPosition = new Dictionary();
+ foreach (var content in Content)
+ {
+ if (content is MachOSectionData data)
+ {
+ byPosition[data.Position] = data;
+ }
+ }
+
+ foreach (var segment in Segments)
+ {
+ foreach (var section in segment.Sections)
+ {
+ if (section.IsZeroFill || section.Size == 0) continue;
+
+ if (!byPosition.TryGetValue(section.FileOffset, out var data))
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_SectionContentMismatch,
+ $"Section {section.SegmentName},{section.Name} says its bytes are at 0x{section.FileOffset:X}, but no content of this image is there");
+ continue;
+ }
+
+ if (!ReferenceEquals(data.Section, section))
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_SectionContentMismatch,
+ $"The content at 0x{section.FileOffset:X} belongs to {data.Section.SegmentName},{data.Section.Name} rather than to {section.SegmentName},{section.Name}");
+ }
+ else if (data.Size != section.Size)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_SectionContentMismatch,
+ $"Section {section.SegmentName},{section.Name} says it is 0x{section.Size:X} bytes but its content is 0x{data.Size:X}");
+ }
+ }
+ }
+ }
+
+ ///
+ /// Every byte of the file belongs to exactly one content, so the list has to run from the
+ /// start of the file with no gap and no overlap. A gap would be bytes nothing writes, and an
+ /// overlap would be bytes written twice with the later one winning silently.
+ ///
+ private void VerifyContentCoversTheFile(MachOVisitorContext context)
+ {
+ ulong expected = 0;
+ foreach (var content in Content)
+ {
+ if (content.Position != expected)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_ContentNotContiguous,
+ $"{content} starts at 0x{content.Position:X} but the content before it ends at 0x{expected:X}");
+ return;
+ }
+
+ expected = content.Position + content.Size;
+ }
+ }
+
+ private void VerifySegments(MachOVisitorContext context)
+ {
+ // loader.h draws the line here: non-MH_OBJECT files have "all of their segments with the
+ // proper sections in each, and padded to the specified segment alignment", while
+ // "the MH_OBJECT format has all of its sections in one segment for compactness. There is
+ // no padding to a specified segment boundary". Packed for compactness means a section's
+ // address does not track its file offset, so the invariant below is for linked images
+ // only. Apple's own crt1.o breaks it.
+ var isLinkedImage = FileType != MachOFileType.Object;
+
+ foreach (var segment in Segments)
+ {
+ if (segment.Is64Bit != Is64Bit)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidImageBitness,
+ $"Segment {segment.Name} is {(segment.Is64Bit ? "64" : "32")}-bit in a {(Is64Bit ? "64" : "32")}-bit image");
+ }
+
+ foreach (var section in segment.Sections)
+ {
+ if (section.IsZeroFill || section.Size == 0) continue;
+
+ // This is the invariant the whole format rests on: a section's address is its
+ // segment's address plus its distance from the segment's file offset. Break it
+ // and the loader maps the section somewhere other than where the code expects.
+ if (isLinkedImage && section.Address - segment.VmAddress != section.FileOffset - segment.FileOffset)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_SectionAddressMismatch,
+ $"Section {section.SegmentName},{section.Name} is at address 0x{section.Address:X} and file offset 0x{section.FileOffset:X}, which do not agree with segment {segment.Name} at address 0x{segment.VmAddress:X} and file offset 0x{segment.FileOffset:X}");
+ }
+
+ if (section.FileOffset < segment.FileOffset || section.FileOffset + section.Size > segment.FileEndOffset)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_SectionOutsideSegment,
+ $"Section {section.SegmentName},{section.Name} spans [0x{section.FileOffset:X}, 0x{section.FileOffset + section.Size:X}) which is outside segment {segment.Name} at [0x{segment.FileOffset:X}, 0x{segment.FileEndOffset:X})");
+ }
+ }
+ }
+ }
+
+ private void VerifyLoadCommands(MachOVisitorContext context)
+ {
+ var alignment = MachOLoadCommand.GetSizeAlignment(Is64Bit);
+
+ foreach (var command in LoadCommands)
+ {
+ if (command.Size < 8 || command.Size % alignment != 0)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidCommandAlignment,
+ $"{command} has a size of {command.Size}, which must be at least 8 and a multiple of {alignment} so that dyld can walk the table");
+ }
+ }
+
+ var sizeOfCommands = ComputeSizeOfCommands();
+ if (sizeOfCommands > uint.MaxValue)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit,
+ $"The load commands total 0x{sizeOfCommands:X} bytes, which the 32-bit sizeofcmds field cannot record");
+ }
+
+ if (AvailableLoadCommandSpace < 0)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_NoRoomForLoadCommands,
+ $"The load commands end at {LoadCommandsEndOffset} but the content after them starts at {ContentStartOffset}");
+ }
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.Write.cs b/src/LibObjectFile/MachO/MachOFile.Write.cs
new file mode 100644
index 0000000..675051c
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.Write.cs
@@ -0,0 +1,90 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+partial class MachOFile
+{
+ ///
+ /// Writes this image to a stream.
+ ///
+ /// The stream to write to.
+ /// is null.
+ ///
+ /// The load commands no longer fit in the space before the content after them. See
+ /// .
+ ///
+ public void Write(Stream stream)
+ {
+ if (!TryWrite(stream, out var diagnostics))
+ {
+ throw new ObjectFileException($"Unexpected error while writing the Mach-O file", diagnostics);
+ }
+ }
+
+ ///
+ /// Tries to write this image to a stream.
+ ///
+ /// The stream to write to.
+ /// The diagnostics collected while writing.
+ /// true if the image was written without errors; otherwise false.
+ /// is null.
+ public bool TryWrite(Stream stream, out DiagnosticBag diagnostics)
+ {
+ ArgumentNullException.ThrowIfNull(stream);
+
+ var writer = new MachOWriter(this, stream, new DiagnosticBag());
+ diagnostics = writer.Diagnostics;
+
+ Verify(writer.VisitorContext);
+ if (diagnostics.HasErrors)
+ {
+ return false;
+ }
+
+ UpdateLayout(writer.VisitorContext);
+ if (diagnostics.HasErrors)
+ {
+ return false;
+ }
+
+ Write(writer);
+ stream.Flush();
+
+ return !diagnostics.HasErrors;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ if (IsCodeSignatureStale)
+ {
+ writer.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_StaleCodeSignature,
+ "The image has been edited since it was signed, so its signature no longer covers what it contains. Sign it again before writing.");
+ return;
+ }
+
+ if (LoadCommandsEndOffset > ContentStartOffset)
+ {
+ writer.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_NoRoomForLoadCommands,
+ $"The load commands need {LoadCommandsEndOffset} bytes but the content after them starts at {ContentStartOffset}. The image has {AvailableLoadCommandSpace} bytes of space left.");
+ return;
+ }
+
+ var basePosition = writer.Position;
+
+ foreach (var content in Content)
+ {
+ writer.Position = basePosition + content.Position;
+ content.WriteContent(writer);
+ if (writer.Diagnostics.HasErrors) return;
+ }
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFile.cs b/src/LibObjectFile/MachO/MachOFile.cs
new file mode 100644
index 0000000..6d4b290
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFile.cs
@@ -0,0 +1,349 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using LibObjectFile.Collections;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A Mach-O image that can be read, modified and written back.
+///
+///
+/// The writer places content at the offsets recorded in the load commands rather than computing
+/// a fresh layout. Every byte outside the header and the load command table keeps its position,
+/// so addresses, relocations and code stay valid across a read-modify-write cycle. That is what
+/// makes in-place load command injection and code signing possible on an existing image, and it
+/// is also why growing the load command table is bounded by
+/// rather than free.
+///
+public sealed partial class MachOFile : MachOObject
+{
+ private readonly ObjectList _loadCommands;
+ private readonly ObjectList _content;
+
+ ///
+ /// Initializes a new empty instance.
+ ///
+ public MachOFile()
+ {
+ _loadCommands = new ObjectList(this);
+ _content = new ObjectList(this);
+ }
+
+ ///
+ /// Gets or sets whether this image uses the 64-bit layout.
+ ///
+ public bool Is64Bit { get; set; }
+
+ ///
+ /// Gets or sets the CPU architecture of this image.
+ ///
+ public MachOCpuType CpuType { get; set; }
+
+ ///
+ /// Gets or sets the CPU subtype. Its meaning depends on , so it is kept
+ /// as the raw value rather than being interpreted.
+ ///
+ public uint CpuSubType { get; set; }
+
+ ///
+ /// Gets or sets the kind of image.
+ ///
+ public MachOFileType FileType { get; set; }
+
+ ///
+ /// Gets or sets the header flags.
+ ///
+ public MachOHeaderFlags Flags { get; set; }
+
+ ///
+ /// Gets or sets the reserved header field, present only in 64-bit images.
+ ///
+ public uint Reserved { get; set; }
+
+ ///
+ /// Gets the load commands of this image, in file order. The order is meaningful: dyld
+ /// identifies a library by the position of its command among the others.
+ ///
+ public ObjectList LoadCommands => _loadCommands;
+
+ ///
+ /// Gets everything in this image, in file order: the header, the load command table, each
+ /// section's bytes, each table in __LINKEDIT, and the padding between them.
+ ///
+ ///
+ /// Every byte of the file belongs to one of these, so writing the list back out reproduces
+ /// the image and a layout is a single walk over it.
+ ///
+ public ObjectList Content => _content;
+
+ ///
+ /// Gets the segments of this image, in load command order.
+ ///
+ public IEnumerable Segments => LoadCommands.OfType();
+
+ ///
+ /// Gets the libraries this image links against, in the order dyld assigns their ordinals.
+ ///
+ public IEnumerable LinkedLibraries
+ => LoadCommands.OfType().Where(c => c.Type != MachOLoadCommandType.IdDylib);
+
+ ///
+ /// Gets the runpaths @rpath expands to, in search order.
+ ///
+ public IEnumerable RunPaths
+ => LoadCommands.OfType().Where(c => c.Type == MachOLoadCommandType.RPath);
+
+ ///
+ /// Gets the command giving this image's own install name, present only in a dylib.
+ ///
+ public MachODylibCommand? IdDylib
+ => LoadCommands.OfType().FirstOrDefault(c => c.Type == MachOLoadCommandType.IdDylib);
+
+ ///
+ /// Gets the size of the Mach-O header, which is 32 bytes for a 64-bit image and 28 otherwise.
+ ///
+ public uint HeaderSize => Is64Bit ? 32u : 28u;
+
+ ///
+ /// The smallest a Mach-O header can be, which is the 32-bit one. Nothing shorter than this
+ /// can hold an image.
+ ///
+ internal const uint MinHeaderSize = 28;
+
+ ///
+ /// Gets the segment alignment of this image's architecture, which segment sizes are rounded
+ /// up to.
+ ///
+ ///
+ /// loader.h calls this "the specified segment alignment", a link time choice rather than
+ /// anything the header records, so it is taken from what the toolchain does per
+ /// architecture: signing an arm64 image rounds __LINKEDIT from 0x4A50 to 0x8000, which only
+ /// 16KB explains, while the linker's own x86 and x86_64 images carry sizes such as 0x30D000
+ /// that are not 16KB multiples at all.
+ ///
+ /// This is not the page size a code signature hashes in, which is 4KB on every architecture
+ /// including arm64.
+ ///
+ ///
+ public ulong SegmentAlignment => CpuType == MachOCpuType.Arm64 ? 0x4000ul : 0x1000ul;
+
+ ///
+ /// Gets the total size of the load commands, the value stored in sizeofcmds.
+ ///
+ ///
+ /// Saturates instead of wrapping when the commands do not fit the 32-bit field. A wrap here
+ /// would report a large table as a small one, and
+ /// would then offer room that does not exist.
+ /// reports the condition.
+ ///
+ public uint SizeOfCommands
+ {
+ get
+ {
+ var total = ComputeSizeOfCommands();
+ return total > uint.MaxValue ? uint.MaxValue : (uint)total;
+ }
+ }
+
+ ///
+ /// Totals the load commands without narrowing, so a table that overruns sizeofcmds can
+ /// be told from one that exactly fills it.
+ ///
+ internal ulong ComputeSizeOfCommands()
+ {
+ ulong total = 0;
+ foreach (var command in LoadCommands)
+ {
+ total += command.Size;
+ }
+ return total;
+ }
+
+ ///
+ /// Gets the file offset one past the end of the load command table.
+ ///
+ public ulong LoadCommandsEndOffset => HeaderSize + ComputeSizeOfCommands();
+
+ ///
+ /// Gets the command locating this image's code signature, or null if it is unsigned.
+ ///
+ public MachOLinkEditDataCommand? CodeSignature
+ => LoadCommands.OfType().FirstOrDefault(c => c.Type == MachOLoadCommandType.CodeSignature);
+
+ ///
+ /// Gets whether this image has been changed since it was signed, which would leave the
+ /// signature covering bytes that are no longer there.
+ ///
+ ///
+ /// Set by the editing operations and cleared by . Writing an image in
+ /// this state fails, because the result would look signed and be refused at execution. Sign
+ /// again after editing.
+ ///
+ /// This follows the editing operations, not arbitrary writes to the model. Reaching into a
+ /// load command or a piece of content directly still invalidates a signature without being
+ /// noticed here.
+ ///
+ ///
+ public bool IsCodeSignatureStale { get; internal set; }
+
+ ///
+ /// Gets the padding the linker left after the load command table, or null if there is none.
+ ///
+ ///
+ /// Linkers leave this gap so commands can be added later without moving anything. Adding a
+ /// command consumes it from the front, which is how install_name_tool edits an image
+ /// without relinking it.
+ ///
+ public MachOContent? LoadCommandPadding { get; internal set; }
+
+ ///
+ /// Gets the file offset at which content after the load commands and their padding begins.
+ ///
+ public ulong ContentStartOffset
+ => LoadCommandPadding is { } padding ? padding.Position + padding.Size : LoadCommandsEndOffset;
+
+ ///
+ /// Gets the number of bytes the load command table can still grow by without moving any
+ /// content. A negative value means the current commands no longer fit.
+ ///
+ public long AvailableLoadCommandSpace
+ {
+ get
+ {
+ // Both ends are clamped so that a model carrying nonsensical offsets reports no room
+ // rather than wrapping into a large positive number and letting an edit through.
+ var start = (long)Math.Min(ContentStartOffset, long.MaxValue);
+ var end = (long)Math.Min(LoadCommandsEndOffset, long.MaxValue);
+ return start - end;
+ }
+ }
+
+ ///
+ /// Rewrites the file offsets that point at relocatable data: the tables in __LINKEDIT
+ /// and the relocations of each section. This lets that data be moved without each caller
+ /// knowing which commands record where it is.
+ ///
+ /// Maps an old file offset to its new one.
+ ///
+ /// Placement is deliberately not included. A segment's file offset and a section's file
+ /// offset say where something is mapped, not merely where it is stored: a section's address
+ /// is its segment's address plus its distance from the segment's file offset, so changing
+ /// either moves the thing in memory and invalidates what refers to it. Moving a segment or a
+ /// section is a different operation from relocating a blob nothing addresses, and this is
+ /// only the second one.
+ ///
+ /// No bytes are moved here; only the offsets recording where they are.
+ ///
+ ///
+ /// is null.
+ public void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+
+ foreach (var command in LoadCommands)
+ {
+ command.UpdateFileOffsets(mapper);
+ }
+ }
+
+ ///
+ /// Computes the size this image occupies when written, without writing it.
+ ///
+ /// The number of bytes produces.
+ ///
+ /// Content sits at the offsets recorded for it, so the size is where the furthest of it
+ /// ends. An image with no content at all is just its header, commands and their padding.
+ ///
+ public ulong ComputeFileSize()
+ {
+ ulong end = 0;
+ foreach (var content in Content)
+ {
+ end = Math.Max(end, content.Position + content.Size);
+ }
+ return end;
+ }
+
+ ///
+ /// Finds a segment by name.
+ ///
+ /// The segment name, such as __TEXT.
+ /// The segment, or null if this image has no segment with that name.
+ public MachOSegment? FindSegment(string name)
+ {
+ ArgumentNullException.ThrowIfNull(name);
+ foreach (var segment in Segments)
+ {
+ if (segment.Name == name) return segment;
+ }
+ return null;
+ }
+
+ ///
+ /// Places the content of this image.
+ ///
+ ///
+ /// Only the header, the load command table and the padding between the table and what follows
+ /// it are placed. Everything else keeps the position recorded for it, because a section's
+ /// address is its segment's address plus its distance from the segment's file offset, so
+ /// moving content in the file would move it in memory. The padding is what absorbs a command
+ /// table that has grown, which is the whole of the room an edit has to work in.
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ {
+ foreach (var content in Content)
+ {
+ content.UpdateLayout(context);
+ }
+
+ ulong cursor = 0;
+ for (var i = 0; i < Content.Count; i++)
+ {
+ var content = Content[i];
+
+ switch (content)
+ {
+ case MachOHeaderContent:
+ case MachOLoadCommandTable:
+ content.Position = cursor;
+ break;
+
+ case MachOLoadCommandPadding padding:
+ padding.Position = cursor;
+
+ // The padding runs from the end of the commands to whatever comes next, so a
+ // table that has grown eats into it and one that has shrunk gives back.
+ var next = i + 1 < Content.Count ? Content[i + 1].Position : cursor;
+ if (next < cursor)
+ {
+ context.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_NoRoomForLoadCommands,
+ $"The load commands now end at {cursor} but the content after them starts at {next}, so they no longer fit.");
+ return;
+ }
+ padding.Size = next - cursor;
+ break;
+ }
+
+ cursor = content.Position + content.Size;
+ }
+ }
+
+ ///
+ protected override void PrintName(StringBuilder builder) => builder.Append(nameof(MachOFile));
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"{CpuType} {FileType}, {(Is64Bit ? "64-bit" : "32-bit")}, Commands = {LoadCommands.Count}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOFileType.cs b/src/LibObjectFile/MachO/MachOFileType.cs
new file mode 100644
index 0000000..0b58c99
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOFileType.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// Kind of Mach-O image, as stored in the filetype header field.
+///
+public enum MachOFileType : uint
+{
+ /// Relocatable object file (MH_OBJECT).
+ Object = 0x1,
+ /// Demand paged executable (MH_EXECUTE).
+ Execute = 0x2,
+ /// Fixed VM shared library, obsolete (MH_FVMLIB).
+ FixedVMLibrary = 0x3,
+ /// Core dump (MH_CORE).
+ Core = 0x4,
+ /// Preloaded executable (MH_PRELOAD).
+ Preload = 0x5,
+ /// Dynamically bound shared library (MH_DYLIB).
+ Dylib = 0x6,
+ /// Dynamic link editor (MH_DYLINKER).
+ Dylinker = 0x7,
+ /// Dynamically bound bundle (MH_BUNDLE).
+ Bundle = 0x8,
+ /// Shared library stub for static linking only (MH_DYLIB_STUB).
+ DylibStub = 0x9,
+ /// Companion file carrying only debug sections (MH_DSYM).
+ Dsym = 0xa,
+ /// x86_64 kexts (MH_KEXT_BUNDLE).
+ KextBundle = 0xb,
+ /// Set of Mach-O images linked together, used by kernel collections (MH_FILESET).
+ Fileset = 0xc,
+}
diff --git a/src/LibObjectFile/MachO/MachOHeaderFlags.cs b/src/LibObjectFile/MachO/MachOHeaderFlags.cs
new file mode 100644
index 0000000..1284f6e
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOHeaderFlags.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Flags of a Mach-O image, as stored in the flags header field.
+///
+[Flags]
+public enum MachOHeaderFlags : uint
+{
+ /// No flags set.
+ None = 0,
+ /// The image has no undefined references (MH_NOUNDEFS).
+ NoUndefs = 0x1,
+ /// Output of an incremental link, not re-linkable (MH_INCRLINK).
+ IncrementalLink = 0x2,
+ /// The image is input for the dynamic linker and cannot be re-linked (MH_DYLDLINK).
+ DyldLink = 0x4,
+ /// Undefined references are bound by the dynamic linker when loaded (MH_BINDATLOAD).
+ BindAtLoad = 0x8,
+ /// The image has its dynamic undefined references prebound (MH_PREBOUND).
+ Prebound = 0x10,
+ /// The image has its read-only and read-write segments split (MH_SPLIT_SEGS).
+ SplitSegs = 0x20,
+ /// The shared library init routine is to be run lazily, obsolete (MH_LAZY_INIT).
+ LazyInit = 0x40,
+ /// The image uses two-level namespace bindings (MH_TWOLEVEL).
+ TwoLevel = 0x80,
+ /// The executable forces all images to use flat namespace bindings (MH_FORCE_FLAT).
+ ForceFlat = 0x100,
+ /// No multiple definitions of symbols exist, so no lookup is needed (MH_NOMULTIDEFS).
+ NoMultiDefs = 0x200,
+ /// Do not notify the prebinding agent about this executable (MH_NOFIXPREBINDING).
+ NoFixPrebinding = 0x400,
+ /// The binary is not prebound but can have its prebinding redone (MH_PREBINDABLE).
+ Prebindable = 0x800,
+ /// All two-level namespace modules of dependent libraries are bound (MH_ALLMODSBOUND).
+ AllModsBound = 0x1000,
+ /// Sections of object files were divided into subsections by symbol (MH_SUBSECTIONS_VIA_SYMBOLS).
+ SubsectionsViaSymbols = 0x2000,
+ /// The binary has been canonicalized (MH_CANONICAL).
+ Canonical = 0x4000,
+ /// The final linked image contains external weak symbols (MH_WEAK_DEFINES).
+ WeakDefines = 0x8000,
+ /// The final linked image uses weak symbols (MH_BINDS_TO_WEAK).
+ BindsToWeak = 0x10000,
+ /// Stack segments are made executable (MH_ALLOW_STACK_EXECUTION).
+ AllowStackExecution = 0x20000,
+ /// The binary is safe for use in processes with uid zero (MH_ROOT_SAFE).
+ RootSafe = 0x40000,
+ /// The binary is safe for use in processes when issetugid is true (MH_SETUID_SAFE).
+ SetuidSafe = 0x80000,
+ /// The image has no re-exported dylibs, so sub-library resolution can be skipped (MH_NO_REEXPORTED_DYLIBS).
+ NoReexportedDylibs = 0x100000,
+ /// The image is position independent and loads at a random address (MH_PIE).
+ PositionIndependent = 0x200000,
+ /// The linker should strip this dylib if it is not used (MH_DEAD_STRIPPABLE_DYLIB).
+ DeadStrippableDylib = 0x400000,
+ /// The image has thread-local variable descriptors (MH_HAS_TLV_DESCRIPTORS).
+ HasTlvDescriptors = 0x800000,
+ /// The image has no executable heap pages (MH_NO_HEAP_EXECUTION).
+ NoHeapExecution = 0x1000000,
+ /// The code was linked for use in an application extension (MH_APP_EXTENSION_SAFE).
+ AppExtensionSafe = 0x2000000,
+ /// The external symbols in the symbol table do not agree with the dyld info (MH_NLIST_OUTOFSYNC_WITH_DYLDINFO).
+ NListOutOfSyncWithDyldInfo = 0x4000000,
+ /// The image supports running in a simulator (MH_SIM_SUPPORT).
+ SimSupport = 0x8000000,
+ /// The dylib is part of the shared cache rather than a standalone file (MH_DYLIB_IN_CACHE).
+ DylibInCache = 0x80000000,
+}
diff --git a/src/LibObjectFile/MachO/MachOLinkEditDataCommand.cs b/src/LibObjectFile/MachO/MachOLinkEditDataCommand.cs
new file mode 100644
index 0000000..e85e0d5
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOLinkEditDataCommand.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A load command locating a blob inside __LINKEDIT, covering LC_CODE_SIGNATURE,
+/// LC_FUNCTION_STARTS, LC_DATA_IN_CODE, LC_DYLD_EXPORTS_TRIE,
+/// LC_DYLD_CHAINED_FIXUPS, LC_SEGMENT_SPLIT_INFO, LC_DYLIB_CODE_SIGN_DRS
+/// and LC_LINKER_OPTIMIZATION_HINT.
+///
+///
+/// The command only records where the blob is; the bytes themselves live in the
+/// __LINKEDIT segment's content and are not interpreted here.
+///
+public sealed class MachOLinkEditDataCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 16;
+
+ ///
+ /// Gets or sets the file offset of the blob.
+ ///
+ public uint DataOffset { get; set; }
+
+ ///
+ /// Gets or sets the size in bytes of the blob.
+ ///
+ public uint DataSize { get; set; }
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+ if (DataOffset != 0) DataOffset = mapper(DataOffset);
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawLinkEditDataCommand), out RawLinkEditDataCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated link edit data command at 0x{Position:X}");
+ return;
+ }
+
+ DataOffset = raw.DataOffset;
+ DataSize = raw.DataSize;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.Write(new RawLinkEditDataCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ DataOffset = DataOffset,
+ DataSize = DataSize,
+ });
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Type = {Type}, DataOffset = 0x{DataOffset:X}, DataSize = 0x{DataSize:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOLoadCommand.cs b/src/LibObjectFile/MachO/MachOLoadCommand.cs
new file mode 100644
index 0000000..c0d91ec
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOLoadCommand.cs
@@ -0,0 +1,61 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Base class for a Mach-O load command.
+///
+///
+/// is the command's cmdsize, which covers the
+/// cmd and cmdsize fields themselves. It is always a multiple of 4 on 32-bit
+/// images and of 8 on 64-bit ones, because dyld walks the list by adding it to a pointer.
+///
+public abstract class MachOLoadCommand : MachOObject
+{
+ ///
+ /// Gets or sets the type of this load command.
+ ///
+ public MachOLoadCommandType Type { get; set; }
+
+ ///
+ /// Gets the smallest cmdsize this kind of command can legally have, which is the size
+ /// of its fixed part.
+ ///
+ ///
+ /// A command declaring less than this cannot hold the fields its type is defined to have, so
+ /// reading it would consume bytes belonging to whatever follows. This is checked before a
+ /// command is read rather than after, since by then the damage is done.
+ ///
+ public virtual uint MinimumCommandSize => 8;
+
+ ///
+ /// Rewrites every file offset this command records, so a layout can move the data they point
+ /// at without each caller having to know which commands carry offsets.
+ ///
+ /// Maps an old file offset to its new one.
+ ///
+ /// An offset of zero means the data is absent rather than located at the start of the file,
+ /// so it is left alone and is not called for it.
+ ///
+ public virtual void UpdateFileOffsets(Func mapper)
+ {
+ }
+
+ ///
+ /// Gets the alignment cmdsize has to satisfy for the given image width.
+ ///
+ /// Whether the containing image is 64-bit.
+ /// 4 for a 32-bit image, 8 for a 64-bit one.
+ public static uint GetSizeAlignment(bool is64Bit) => is64Bit ? 8u : 4u;
+
+ ///
+ protected override bool PrintMembers(System.Text.StringBuilder builder)
+ {
+ builder.Append($"Type = {Type}, Size = {Size}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOLoadCommandType.cs b/src/LibObjectFile/MachO/MachOLoadCommandType.cs
new file mode 100644
index 0000000..254e194
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOLoadCommandType.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// Type of a load command, as stored in the cmd field of a Mach-O load command.
+///
+///
+/// The high bit is part of the stored value, not a
+/// separate flag. It marks commands that dyld must understand to load the image at all, so a
+/// loader that does not recognise one has to refuse the file rather than skip the command.
+///
+public enum MachOLoadCommandType : uint
+{
+ /// Set on commands that dyld is required to understand rather than skip.
+ RequiredByDynamicLinker = 0x80000000,
+
+ /// 32-bit segment of the file mapped into memory (LC_SEGMENT).
+ Segment = 0x1,
+ /// Symbol table and string table location (LC_SYMTAB).
+ SymbolTable = 0x2,
+ /// Gdb symbol table info, obsolete (LC_SYMSEG).
+ SymbolSegment = 0x3,
+ /// Thread state, without a stack (LC_THREAD).
+ Thread = 0x4,
+ /// Thread state and stack, used as the entry point before LC_MAIN (LC_UNIXTHREAD).
+ UnixThread = 0x5,
+ /// Load a fixed VM shared library, obsolete (LC_LOADFVMLIB).
+ LoadFixedVMLibrary = 0x6,
+ /// Fixed VM shared library identification, obsolete (LC_IDFVMLIB).
+ IdFixedVMLibrary = 0x7,
+ /// Object identification, obsolete (LC_IDENT).
+ Identification = 0x8,
+ /// Fixed VM file inclusion, obsolete (LC_FVMFILE).
+ FixedVMFile = 0x9,
+ /// Prepage command, obsolete (LC_PREPAGE).
+ Prepage = 0xa,
+ /// Dynamic link editor symbol table info (LC_DYSYMTAB).
+ DynamicSymbolTable = 0xb,
+ /// Load a dynamically linked shared library (LC_LOAD_DYLIB).
+ LoadDylib = 0xc,
+ /// Identification of a dynamically linked shared library (LC_ID_DYLIB).
+ IdDylib = 0xd,
+ /// Load a dynamic linker (LC_LOAD_DYLINKER).
+ LoadDylinker = 0xe,
+ /// Dynamic linker identification (LC_ID_DYLINKER).
+ IdDylinker = 0xf,
+ /// Modules prebound for a dynamically linked shared library (LC_PREBOUND_DYLIB).
+ PreboundDylib = 0x10,
+ /// Image routines (LC_ROUTINES).
+ Routines = 0x11,
+ /// Sub framework (LC_SUB_FRAMEWORK).
+ SubFramework = 0x12,
+ /// Sub umbrella (LC_SUB_UMBRELLA).
+ SubUmbrella = 0x13,
+ /// Sub client (LC_SUB_CLIENT).
+ SubClient = 0x14,
+ /// Sub library (LC_SUB_LIBRARY).
+ SubLibrary = 0x15,
+ /// Two-level namespace lookup hints (LC_TWOLEVEL_HINTS).
+ TwoLevelHints = 0x16,
+ /// Prebind checksum (LC_PREBIND_CKSUM).
+ PrebindChecksum = 0x17,
+ /// Load a weak dylib, tolerated as missing at runtime (LC_LOAD_WEAK_DYLIB).
+ LoadWeakDylib = 0x18 | RequiredByDynamicLinker,
+ /// 64-bit segment of the file mapped into memory (LC_SEGMENT_64).
+ Segment64 = 0x19,
+ /// 64-bit image routines (LC_ROUTINES_64).
+ Routines64 = 0x1a,
+ /// The image UUID (LC_UUID).
+ Uuid = 0x1b,
+ /// Runpath additions used to resolve @rpath (LC_RPATH).
+ RPath = 0x1c | RequiredByDynamicLinker,
+ /// Local of code signature (LC_CODE_SIGNATURE).
+ CodeSignature = 0x1d,
+ /// Local of info to split segments (LC_SEGMENT_SPLIT_INFO).
+ SegmentSplitInfo = 0x1e,
+ /// Load and re-export a dylib (LC_REEXPORT_DYLIB).
+ ReexportDylib = 0x1f | RequiredByDynamicLinker,
+ /// Delay load of a dylib until first use (LC_LAZY_LOAD_DYLIB).
+ LazyLoadDylib = 0x20,
+ /// Encrypted segment information (LC_ENCRYPTION_INFO).
+ EncryptionInfo = 0x21,
+ /// Compressed dyld information (LC_DYLD_INFO).
+ DyldInfo = 0x22,
+ /// Compressed dyld information only, with no classic relocations (LC_DYLD_INFO_ONLY).
+ DyldInfoOnly = 0x22 | RequiredByDynamicLinker,
+ /// Load an upward dylib (LC_LOAD_UPWARD_DYLIB).
+ LoadUpwardDylib = 0x23 | RequiredByDynamicLinker,
+ /// Build for macOS minimum OS version (LC_VERSION_MIN_MACOSX).
+ VersionMinMacOSX = 0x24,
+ /// Build for iPhoneOS minimum OS version (LC_VERSION_MIN_IPHONEOS).
+ VersionMinIPhoneOS = 0x25,
+ /// Compressed table of function start addresses (LC_FUNCTION_STARTS).
+ FunctionStarts = 0x26,
+ /// String for dyld to treat like an environment variable (LC_DYLD_ENVIRONMENT).
+ DyldEnvironment = 0x27,
+ /// Replacement for LC_UNIXTHREAD as the entry point (LC_MAIN).
+ Main = 0x28 | RequiredByDynamicLinker,
+ /// Table of non-instructions in __text (LC_DATA_IN_CODE).
+ DataInCode = 0x29,
+ /// Source version used to build the binary (LC_SOURCE_VERSION).
+ SourceVersion = 0x2a,
+ /// Code signing designated requirements copied from linked dylibs (LC_DYLIB_CODE_SIGN_DRS).
+ DylibCodeSignDrs = 0x2b,
+ /// 64-bit encrypted segment information (LC_ENCRYPTION_INFO_64).
+ EncryptionInfo64 = 0x2c,
+ /// Linker options embedded in object files (LC_LINKER_OPTION).
+ LinkerOption = 0x2d,
+ /// Optimization hints in object files (LC_LINKER_OPTIMIZATION_HINT).
+ LinkerOptimizationHint = 0x2e,
+ /// Build for tvOS minimum OS version (LC_VERSION_MIN_TVOS).
+ VersionMinTvOS = 0x2f,
+ /// Build for watchOS minimum OS version (LC_VERSION_MIN_WATCHOS).
+ VersionMinWatchOS = 0x30,
+ /// Arbitrary data included within a Mach-O file (LC_NOTE).
+ Note = 0x31,
+ /// Build for platform minimum OS version, replacing the per-platform commands (LC_BUILD_VERSION).
+ BuildVersion = 0x32,
+ /// Exported symbols trie, split out of the dyld info (LC_DYLD_EXPORTS_TRIE).
+ DyldExportsTrie = 0x33 | RequiredByDynamicLinker,
+ /// Chained fixups, replacing the rebase and bind opcode streams (LC_DYLD_CHAINED_FIXUPS).
+ DyldChainedFixups = 0x34 | RequiredByDynamicLinker,
+ /// Entry in a fileset, used by the kernel collections (LC_FILESET_ENTRY).
+ FilesetEntry = 0x35 | RequiredByDynamicLinker,
+ /// Atom information for live linking (LC_ATOM_INFO).
+ AtomInfo = 0x36,
+}
diff --git a/src/LibObjectFile/MachO/MachOMagic.cs b/src/LibObjectFile/MachO/MachOMagic.cs
new file mode 100644
index 0000000..97dfda9
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOMagic.cs
@@ -0,0 +1,41 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// The magic values a Mach-O or universal binary can start with.
+///
+///
+/// The Cigam spellings are the byte-swapped forms, written by a host of the opposite
+/// endianness. The fat magics work the other way round from the thin ones: a universal binary
+/// header is always big-endian, so is what a big-endian reader sees and
+/// is what a little-endian host reads out of a well-formed file.
+///
+public static class MachOMagic
+{
+ /// 32-bit Mach-O, host-endian (MH_MAGIC).
+ public const uint Magic32 = 0xfeedface;
+
+ /// 32-bit Mach-O, byte-swapped (MH_CIGAM).
+ public const uint Cigam32 = 0xcefaedfe;
+
+ /// 64-bit Mach-O, host-endian (MH_MAGIC_64).
+ public const uint Magic64 = 0xfeedfacf;
+
+ /// 64-bit Mach-O, byte-swapped (MH_CIGAM_64).
+ public const uint Cigam64 = 0xcffaedfe;
+
+ /// Universal binary with 32-bit slice offsets (FAT_MAGIC).
+ public const uint FatMagic = 0xcafebabe;
+
+ /// Universal binary with 32-bit slice offsets, byte-swapped (FAT_CIGAM).
+ public const uint FatCigam = 0xbebafeca;
+
+ /// Universal binary with 64-bit slice offsets (FAT_MAGIC_64).
+ public const uint FatMagic64 = 0xcafebabf;
+
+ /// Universal binary with 64-bit slice offsets, byte-swapped (FAT_CIGAM_64).
+ public const uint FatCigam64 = 0xbfbafeca;
+}
diff --git a/src/LibObjectFile/MachO/MachOMainCommand.cs b/src/LibObjectFile/MachO/MachOMainCommand.cs
new file mode 100644
index 0000000..84c3b8c
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOMainCommand.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The entry point load command (LC_MAIN).
+///
+///
+/// This replaced LC_UNIXTHREAD, which specified the entry point by handing dyld a whole
+/// register state to restore. is a file offset rather than an address,
+/// so it moves with the content it points at.
+///
+public sealed class MachOMainCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 24;
+
+ ///
+ /// Gets or sets the file offset of the entry point.
+ ///
+ public ulong EntryOffset { get; set; }
+
+ ///
+ /// Gets or sets the initial stack size, or zero to let the system choose.
+ ///
+ public ulong StackSize { get; set; }
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawEntryPointCommand), out RawEntryPointCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_MAIN at 0x{Position:X}");
+ return;
+ }
+
+ EntryOffset = raw.EntryOffset;
+ StackSize = raw.StackSize;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ => writer.Write(new RawEntryPointCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ EntryOffset = EntryOffset,
+ StackSize = StackSize,
+ });
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"EntryOffset = 0x{EntryOffset:X}, StackSize = 0x{StackSize:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOObject.cs b/src/LibObjectFile/MachO/MachOObject.cs
new file mode 100644
index 0000000..b37605f
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOObject.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// Base class for all elements of a .
+///
+public abstract class MachOObject : ObjectFileElement
+{
+}
diff --git a/src/LibObjectFile/MachO/MachOPathCommand.cs b/src/LibObjectFile/MachO/MachOPathCommand.cs
new file mode 100644
index 0000000..3a24eff
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOPathCommand.cs
@@ -0,0 +1,54 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A load command carrying a single path, covering LC_RPATH, LC_LOAD_DYLINKER,
+/// LC_ID_DYLINKER and LC_DYLD_ENVIRONMENT.
+///
+public sealed class MachOPathCommand : MachOPathLoadCommand
+{
+ ///
+ /// Gets or sets the path carried by this command. For LC_RPATH this is a directory
+ /// that @rpath expands to, and for the dylinker commands it is the path of the loader.
+ ///
+ public string Path
+ {
+ get => Value;
+ set => Value = value;
+ }
+
+ ///
+ protected override unsafe uint FixedSize => (uint)sizeof(RawPathCommand);
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ var commandPosition = reader.Position;
+ if (!reader.TryReadData(sizeof(RawPathCommand), out RawPathCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated path command at 0x{commandPosition:X}");
+ return;
+ }
+
+ ReadValue(reader, commandPosition, raw.PathOffset);
+ }
+
+ ///
+ public override unsafe void Write(MachOWriter writer)
+ {
+ writer.Write(new RawPathCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ PathOffset = (uint)sizeof(RawPathCommand),
+ });
+ WriteValue(writer);
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOPathLoadCommand.cs b/src/LibObjectFile/MachO/MachOPathLoadCommand.cs
new file mode 100644
index 0000000..0fb1221
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOPathLoadCommand.cs
@@ -0,0 +1,130 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.Utils;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Base class for the load commands that store a string inline after a fixed header.
+///
+///
+/// The string is stored NUL-terminated and then padded with more NULs until cmdsize is
+/// aligned. The original cmdsize is kept as long as the string still fits, so re-writing
+/// an untouched command reproduces the linker's padding byte for byte rather than the smallest
+/// encoding this library would have chosen.
+///
+public abstract class MachOPathLoadCommand : MachOLoadCommand
+{
+ private string _value = string.Empty;
+
+ ///
+ /// Gets or sets the string carried by this command.
+ ///
+ /// The value is null.
+ public string Value
+ {
+ get => _value;
+ set
+ {
+ ArgumentNullException.ThrowIfNull(value);
+ _value = value;
+ }
+ }
+
+ ///
+ /// Gets or sets whether the containing image is 64-bit, which sets the cmdsize alignment.
+ ///
+ public bool Is64Bit { get; set; }
+
+ ///
+ /// Gets the size of the fixed part preceding the string, including cmd and cmdsize.
+ ///
+ protected abstract uint FixedSize { get; }
+
+ ///
+ public override uint MinimumCommandSize => FixedSize + 1;
+
+ ///
+ /// Gets the smallest valid cmdsize for the current .
+ ///
+ public uint MinimumSize => ComputeMinimumSize(Value);
+
+ ///
+ /// Gets the smallest valid cmdsize this command would need to hold
+ /// .
+ ///
+ /// The string to size for.
+ /// The cmdsize required.
+ ///
+ /// This lets a caller find out whether a new value fits before assigning it, so an edit that
+ /// turns out not to fit does not leave the command half changed.
+ ///
+ /// is null.
+ public uint ComputeMinimumSize(string value)
+ {
+ ArgumentNullException.ThrowIfNull(value);
+
+ // The string is stored NUL-terminated, hence the extra byte.
+ var unaligned = FixedSize + (uint)Encoding.UTF8.GetByteCount(value) + 1;
+ return AlignHelper.AlignUp(unaligned, GetSizeAlignment(Is64Bit));
+ }
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ {
+ var minimum = MinimumSize;
+ if (Size < minimum)
+ {
+ Size = minimum;
+ }
+ }
+
+ ///
+ /// Reads the string that follows the fixed part.
+ ///
+ /// The reader, positioned anywhere.
+ /// The file offset of the start of this command.
+ /// The offset of the string from the start of this command.
+ protected void ReadValue(MachOReader reader, ulong commandPosition, uint stringOffset)
+ {
+ if (stringOffset < FixedSize || stringOffset >= Size)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidLoadCommandSize,
+ $"The string offset {stringOffset} in {Type} at 0x{commandPosition:X} is outside the command, which is {Size} bytes");
+ return;
+ }
+
+ var length = (int)(Size - stringOffset);
+ var buffer = new byte[length];
+ reader.Position = commandPosition + stringOffset;
+ reader.ReadExactly(buffer);
+
+ var end = Array.IndexOf(buffer, (byte)0);
+ if (end < 0) end = length;
+ Value = Encoding.UTF8.GetString(buffer, 0, end);
+ }
+
+ ///
+ /// Writes the string and the NUL padding that brings the command up to .
+ ///
+ /// The writer, positioned at the end of the fixed part.
+ protected void WriteValue(MachOWriter writer)
+ {
+ var bytes = Encoding.UTF8.GetBytes(Value);
+ writer.Write(bytes);
+ writer.WriteZero((int)Size - (int)FixedSize - bytes.Length);
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Type = {Type}, Value = {Value}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOPlatform.cs b/src/LibObjectFile/MachO/MachOPlatform.cs
new file mode 100644
index 0000000..4a71df1
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOPlatform.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// The platform an image was built for, as recorded by LC_BUILD_VERSION.
+///
+///
+/// This replaced the per-platform LC_VERSION_MIN_* commands, which could not express
+/// targets such as Catalyst or a simulator without inventing a command for each.
+///
+public enum MachOPlatform : uint
+{
+ /// No platform recorded.
+ Unknown = 0,
+ /// macOS (PLATFORM_MACOS).
+ MacOS = 1,
+ /// iOS (PLATFORM_IOS).
+ IOS = 2,
+ /// tvOS (PLATFORM_TVOS).
+ TvOS = 3,
+ /// watchOS (PLATFORM_WATCHOS).
+ WatchOS = 4,
+ /// The bridgeOS of a T2 coprocessor (PLATFORM_BRIDGEOS).
+ BridgeOS = 5,
+ /// An iOS app running on macOS (PLATFORM_MACCATALYST).
+ MacCatalyst = 6,
+ /// The iOS simulator (PLATFORM_IOSSIMULATOR).
+ IOSSimulator = 7,
+ /// The tvOS simulator (PLATFORM_TVOSSIMULATOR).
+ TvOSSimulator = 8,
+ /// The watchOS simulator (PLATFORM_WATCHOSSIMULATOR).
+ WatchOSSimulator = 9,
+ /// A driver extension (PLATFORM_DRIVERKIT).
+ DriverKit = 10,
+ /// visionOS (PLATFORM_VISIONOS).
+ VisionOS = 11,
+ /// The visionOS simulator (PLATFORM_VISIONOSSIMULATOR).
+ VisionOSSimulator = 12,
+}
diff --git a/src/LibObjectFile/MachO/MachOPrinter.cs b/src/LibObjectFile/MachO/MachOPrinter.cs
new file mode 100644
index 0000000..7fa000a
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOPrinter.cs
@@ -0,0 +1,275 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.IO;
+using System.Linq;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Extension methods for to print their layout in text form, similar to
+/// otool -h -l.
+///
+///
+/// The field names are the ones the format uses rather than the ones this library exposes, so
+/// that output can be read next to otool's without translating between them.
+///
+public static class MachOPrinter
+{
+ ///
+ /// Prints a to the specified writer.
+ ///
+ /// The image to print.
+ /// The destination text writer.
+ /// or is null.
+ public static void Print(this MachOFile file, TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+ ArgumentNullException.ThrowIfNull(writer);
+
+ PrintHeader(file, writer);
+ PrintLoadCommands(file, writer);
+ }
+
+ ///
+ /// Prints the Mach-O header.
+ ///
+ /// The image to print.
+ /// The destination text writer.
+ /// or is null.
+ public static void PrintHeader(MachOFile file, TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+ ArgumentNullException.ThrowIfNull(writer);
+
+ writer.WriteLine("Mach header:");
+ Field(writer, "magic", file.Is64Bit ? "MH_MAGIC_64" : "MH_MAGIC");
+ Field(writer, "cputype", file.CpuType.ToString());
+ Field(writer, "cpusubtype", $"0x{file.CpuSubType:x8}");
+ Field(writer, "filetype", file.FileType.ToString());
+ Field(writer, "ncmds", file.LoadCommands.Count.ToString());
+ Field(writer, "sizeofcmds", file.SizeOfCommands.ToString());
+ Field(writer, "flags", file.Flags == MachOHeaderFlags.None ? "0x0" : $"0x{(uint)file.Flags:x} {file.Flags}");
+ writer.WriteLine();
+ }
+
+ ///
+ /// Prints every load command, in the order the image stores them.
+ ///
+ /// The image to print.
+ /// The destination text writer.
+ /// or is null.
+ public static void PrintLoadCommands(MachOFile file, TextWriter writer)
+ {
+ ArgumentNullException.ThrowIfNull(file);
+ ArgumentNullException.ThrowIfNull(writer);
+
+ for (var i = 0; i < file.LoadCommands.Count; i++)
+ {
+ var command = file.LoadCommands[i];
+ writer.WriteLine($"Load command {i}");
+ Field(writer, "cmd", DescribeType(command.Type));
+ Field(writer, "cmdsize", command.Size.ToString());
+ PrintCommandBody(command, writer);
+ }
+ }
+
+ private static void PrintCommandBody(MachOLoadCommand command, TextWriter writer)
+ {
+ switch (command)
+ {
+ case MachOSegment segment:
+ PrintSegment(segment, writer);
+ break;
+ case MachODylibCommand dylib:
+ Field(writer, "name", dylib.Name);
+ Field(writer, "timestamp", dylib.Timestamp.ToString());
+ Field(writer, "current version", MachOVersion.Decode(dylib.CurrentVersion).ToString());
+ Field(writer, "compatibility version", MachOVersion.Decode(dylib.CompatibilityVersion).ToString());
+ break;
+ case MachOPathCommand path:
+ Field(writer, "path", path.Path);
+ break;
+ case MachOSymbolTableCommand symtab:
+ Field(writer, "symoff", symtab.SymbolOffset.ToString());
+ Field(writer, "nsyms", symtab.SymbolCount.ToString());
+ Field(writer, "stroff", symtab.StringOffset.ToString());
+ Field(writer, "strsize", symtab.StringSize.ToString());
+ break;
+ case MachODynamicSymbolTableCommand dysymtab:
+ Field(writer, "nlocalsym", dysymtab.LocalSymbolCount.ToString());
+ Field(writer, "nextdefsym", dysymtab.ExternalSymbolCount.ToString());
+ Field(writer, "nundefsym", dysymtab.UndefinedSymbolCount.ToString());
+ Field(writer, "indirectsymoff", dysymtab.IndirectSymbolOffset.ToString());
+ Field(writer, "nindirectsyms", dysymtab.IndirectSymbolCount.ToString());
+ break;
+ case MachODyldInfoCommand dyldInfo:
+ Field(writer, "rebase_off", dyldInfo.RebaseOffset.ToString());
+ Field(writer, "rebase_size", dyldInfo.RebaseSize.ToString());
+ Field(writer, "bind_off", dyldInfo.BindOffset.ToString());
+ Field(writer, "bind_size", dyldInfo.BindSize.ToString());
+ Field(writer, "lazy_bind_off", dyldInfo.LazyBindOffset.ToString());
+ Field(writer, "lazy_bind_size", dyldInfo.LazyBindSize.ToString());
+ Field(writer, "export_off", dyldInfo.ExportOffset.ToString());
+ Field(writer, "export_size", dyldInfo.ExportSize.ToString());
+ break;
+ case MachOLinkEditDataCommand linkEdit:
+ Field(writer, "dataoff", linkEdit.DataOffset.ToString());
+ Field(writer, "datasize", linkEdit.DataSize.ToString());
+ break;
+ case MachOMainCommand main:
+ Field(writer, "entryoff", main.EntryOffset.ToString());
+ Field(writer, "stacksize", main.StackSize.ToString());
+ break;
+ case MachOUuidCommand uuid:
+ Field(writer, "uuid", uuid.Uuid.ToString("D").ToUpperInvariant());
+ break;
+ case MachOVersionMinCommand versionMin:
+ Field(writer, "version", versionMin.MinOS.ToString());
+ Field(writer, "sdk", versionMin.Sdk.ToString());
+ break;
+ case MachOBuildVersionCommand build:
+ Field(writer, "platform", build.Platform.ToString());
+ Field(writer, "minos", build.MinOS.ToString());
+ Field(writer, "sdk", build.Sdk.ToString());
+ Field(writer, "ntools", build.Tools.Count.ToString());
+ foreach (var tool in build.Tools)
+ {
+ Field(writer, "tool", tool.Tool.ToString());
+ Field(writer, "version", tool.Version.ToString());
+ }
+ break;
+ case MachOSourceVersionCommand source:
+ Field(writer, "version", source.Version.ToString());
+ break;
+ case MachOTwoLevelHintsCommand hints:
+ Field(writer, "offset", hints.Offset.ToString());
+ Field(writer, "nhints", hints.HintCount.ToString());
+ break;
+ case MachOThreadCommand thread:
+ foreach (var state in thread.States)
+ {
+ Field(writer, "flavor", state.Flavor.ToString());
+ Field(writer, "count", state.Registers.Length.ToString());
+ }
+ break;
+ case MachOUnknownLoadCommand unknown:
+ Field(writer, "payload", $"{unknown.Payload.Length} bytes");
+ break;
+ }
+ }
+
+ private static void PrintSegment(MachOSegment segment, TextWriter writer)
+ {
+ Field(writer, "segname", segment.Name);
+ Field(writer, "vmaddr", $"0x{segment.VmAddress:x8}");
+ Field(writer, "vmsize", $"0x{segment.VmSize:x8}");
+ Field(writer, "fileoff", segment.FileOffset.ToString());
+ Field(writer, "filesize", segment.FileSize.ToString());
+ Field(writer, "maxprot", DescribeProtection(segment.MaxProtection));
+ Field(writer, "initprot", DescribeProtection(segment.InitProtection));
+ Field(writer, "nsects", segment.Sections.Count.ToString());
+ Field(writer, "flags", $"0x{(uint)segment.SegmentFlags:x}");
+
+ foreach (var section in segment.Sections)
+ {
+ writer.WriteLine("Section");
+ Field(writer, "sectname", section.Name);
+ Field(writer, "segname", section.SegmentName);
+ Field(writer, "addr", $"0x{section.Address:x8}");
+ Field(writer, "size", $"0x{section.Size:x8}");
+ Field(writer, "offset", section.FileOffset.ToString());
+ Field(writer, "align", $"2^{section.Align} ({1 << (int)section.Align})");
+ Field(writer, "reloff", section.RelocationOffset.ToString());
+ Field(writer, "nreloc", section.NumberOfRelocations.ToString());
+ Field(writer, "type", section.SectionType.ToString());
+ Field(writer, "attributes", section.Attributes == MachOSectionAttributes.None ? "(none)" : section.Attributes.ToString());
+ Field(writer, "reserved1", section.Reserved1.ToString());
+ Field(writer, "reserved2", section.Reserved2.ToString());
+ }
+ }
+
+ ///
+ /// Names a command by its LC_ spelling rather than the enumeration's, so output can be
+ /// compared with otool directly. The mapping is written out rather than derived from
+ /// the enumeration names, because several do not follow from them: LC_UNIXTHREAD is
+ /// one word and LC_VERSION_MIN_MACOSX breaks in places the casing does not.
+ /// An unmodelled command keeps its raw value, so the output stays useful against an image
+ /// built by a newer linker.
+ ///
+ private static string DescribeType(MachOLoadCommandType type) => type switch
+ {
+ MachOLoadCommandType.Segment => "LC_SEGMENT",
+ MachOLoadCommandType.SymbolTable => "LC_SYMTAB",
+ MachOLoadCommandType.SymbolSegment => "LC_SYMSEG",
+ MachOLoadCommandType.Thread => "LC_THREAD",
+ MachOLoadCommandType.UnixThread => "LC_UNIXTHREAD",
+ MachOLoadCommandType.LoadFixedVMLibrary => "LC_LOADFVMLIB",
+ MachOLoadCommandType.IdFixedVMLibrary => "LC_IDFVMLIB",
+ MachOLoadCommandType.Identification => "LC_IDENT",
+ MachOLoadCommandType.FixedVMFile => "LC_FVMFILE",
+ MachOLoadCommandType.Prepage => "LC_PREPAGE",
+ MachOLoadCommandType.DynamicSymbolTable => "LC_DYSYMTAB",
+ MachOLoadCommandType.LoadDylib => "LC_LOAD_DYLIB",
+ MachOLoadCommandType.IdDylib => "LC_ID_DYLIB",
+ MachOLoadCommandType.LoadDylinker => "LC_LOAD_DYLINKER",
+ MachOLoadCommandType.IdDylinker => "LC_ID_DYLINKER",
+ MachOLoadCommandType.PreboundDylib => "LC_PREBOUND_DYLIB",
+ MachOLoadCommandType.Routines => "LC_ROUTINES",
+ MachOLoadCommandType.SubFramework => "LC_SUB_FRAMEWORK",
+ MachOLoadCommandType.SubUmbrella => "LC_SUB_UMBRELLA",
+ MachOLoadCommandType.SubClient => "LC_SUB_CLIENT",
+ MachOLoadCommandType.SubLibrary => "LC_SUB_LIBRARY",
+ MachOLoadCommandType.TwoLevelHints => "LC_TWOLEVEL_HINTS",
+ MachOLoadCommandType.PrebindChecksum => "LC_PREBIND_CKSUM",
+ MachOLoadCommandType.LoadWeakDylib => "LC_LOAD_WEAK_DYLIB",
+ MachOLoadCommandType.Segment64 => "LC_SEGMENT_64",
+ MachOLoadCommandType.Routines64 => "LC_ROUTINES_64",
+ MachOLoadCommandType.Uuid => "LC_UUID",
+ MachOLoadCommandType.RPath => "LC_RPATH",
+ MachOLoadCommandType.CodeSignature => "LC_CODE_SIGNATURE",
+ MachOLoadCommandType.SegmentSplitInfo => "LC_SEGMENT_SPLIT_INFO",
+ MachOLoadCommandType.ReexportDylib => "LC_REEXPORT_DYLIB",
+ MachOLoadCommandType.LazyLoadDylib => "LC_LAZY_LOAD_DYLIB",
+ MachOLoadCommandType.EncryptionInfo => "LC_ENCRYPTION_INFO",
+ MachOLoadCommandType.DyldInfo => "LC_DYLD_INFO",
+ MachOLoadCommandType.DyldInfoOnly => "LC_DYLD_INFO_ONLY",
+ MachOLoadCommandType.LoadUpwardDylib => "LC_LOAD_UPWARD_DYLIB",
+ MachOLoadCommandType.VersionMinMacOSX => "LC_VERSION_MIN_MACOSX",
+ MachOLoadCommandType.VersionMinIPhoneOS => "LC_VERSION_MIN_IPHONEOS",
+ MachOLoadCommandType.FunctionStarts => "LC_FUNCTION_STARTS",
+ MachOLoadCommandType.DyldEnvironment => "LC_DYLD_ENVIRONMENT",
+ MachOLoadCommandType.Main => "LC_MAIN",
+ MachOLoadCommandType.DataInCode => "LC_DATA_IN_CODE",
+ MachOLoadCommandType.SourceVersion => "LC_SOURCE_VERSION",
+ MachOLoadCommandType.DylibCodeSignDrs => "LC_DYLIB_CODE_SIGN_DRS",
+ MachOLoadCommandType.EncryptionInfo64 => "LC_ENCRYPTION_INFO_64",
+ MachOLoadCommandType.LinkerOption => "LC_LINKER_OPTION",
+ MachOLoadCommandType.LinkerOptimizationHint => "LC_LINKER_OPTIMIZATION_HINT",
+ MachOLoadCommandType.VersionMinTvOS => "LC_VERSION_MIN_TVOS",
+ MachOLoadCommandType.VersionMinWatchOS => "LC_VERSION_MIN_WATCHOS",
+ MachOLoadCommandType.Note => "LC_NOTE",
+ MachOLoadCommandType.BuildVersion => "LC_BUILD_VERSION",
+ MachOLoadCommandType.DyldExportsTrie => "LC_DYLD_EXPORTS_TRIE",
+ MachOLoadCommandType.DyldChainedFixups => "LC_DYLD_CHAINED_FIXUPS",
+ MachOLoadCommandType.FilesetEntry => "LC_FILESET_ENTRY",
+ MachOLoadCommandType.AtomInfo => "LC_ATOM_INFO",
+ _ => $"0x{(uint)type:x8}",
+ };
+
+ private static string DescribeProtection(MachOVmProtection protection)
+ {
+ if (protection == MachOVmProtection.None) return "---";
+
+ Span result = ['-', '-', '-'];
+ if ((protection & MachOVmProtection.Read) != 0) result[0] = 'r';
+ if ((protection & MachOVmProtection.Write) != 0) result[1] = 'w';
+ if ((protection & MachOVmProtection.Execute) != 0) result[2] = 'x';
+ return new string(result);
+ }
+
+ private static void Field(TextWriter writer, string name, string value)
+ => writer.WriteLine($"{name,22} {value}");
+}
diff --git a/src/LibObjectFile/MachO/MachOReader.cs b/src/LibObjectFile/MachO/MachOReader.cs
new file mode 100644
index 0000000..2b649e7
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOReader.cs
@@ -0,0 +1,50 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.IO;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Reads a from a .
+///
+public sealed class MachOReader : ObjectFileReaderWriter
+{
+ internal MachOReader(MachOFile file, Stream stream, MachOReaderOptions options) : base(file, stream)
+ {
+ Options = options;
+ VisitorContext = new MachOVisitorContext(file, Diagnostics);
+ }
+
+ internal MachOReader(MachOFile file, Stream stream, MachOReaderOptions options, DiagnosticBag diagnostics) : base(file, stream, diagnostics)
+ {
+ Options = options;
+ VisitorContext = new MachOVisitorContext(file, Diagnostics);
+ }
+
+ ///
+ /// Gets the file being read.
+ ///
+ public new MachOFile File => (MachOFile)base.File;
+
+ ///
+ /// Gets the context carrying the diagnostics collected while reading.
+ ///
+ public MachOVisitorContext VisitorContext { get; }
+
+ ///
+ /// Gets the options used for reading.
+ ///
+ public MachOReaderOptions Options { get; }
+
+ ///
+ public override bool KeepOriginalStreamForSubStreams => Options.UseSubStream;
+
+ ///
+ /// Converts a reader to the visitor context it carries.
+ ///
+ /// The reader.
+ public static implicit operator MachOVisitorContext(MachOReader reader) => reader.VisitorContext;
+}
diff --git a/src/LibObjectFile/MachO/MachOReaderOptions.cs b/src/LibObjectFile/MachO/MachOReaderOptions.cs
new file mode 100644
index 0000000..ae96dcd
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOReaderOptions.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// Options for reading a .
+///
+public sealed class MachOReaderOptions
+{
+ ///
+ /// Gets or sets whether segment content is read as a view over the input stream instead of
+ /// being copied into memory. The input stream then has to outlive the returned file.
+ ///
+ public bool UseSubStream { get; set; }
+}
diff --git a/src/LibObjectFile/MachO/MachORelocation.cs b/src/LibObjectFile/MachO/MachORelocation.cs
new file mode 100644
index 0000000..d19936b
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachORelocation.cs
@@ -0,0 +1,136 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A relocation entry, telling the linker how to fix up one place in a section.
+///
+///
+/// Two forms share the same eight bytes. The ordinary form names a symbol or a section by number.
+/// The scattered form carries the target address outright, for when the fixed-up value does not
+/// land inside the thing it refers to and a section number would be ambiguous; it has no room
+/// left for an external flag, so it is always local.
+///
+public sealed class MachORelocation
+{
+ /// Bit of the first word marking an entry as scattered (R_SCATTERED).
+ public const uint ScatteredMask = 0x80000000;
+
+ /// The size of one entry, which both forms share.
+ public const uint EntrySize = 8;
+
+ ///
+ /// Gets or sets the offset of the fixed-up field from the start of its section.
+ ///
+ public int Address { get; set; }
+
+ ///
+ /// Gets or sets the symbol index when is true, and the one-based
+ /// section number otherwise. Always zero for a scattered entry.
+ ///
+ public uint SymbolOrSectionNumber { get; set; }
+
+ ///
+ /// Gets or sets the address of the target, for a scattered entry only.
+ ///
+ public int Value { get; set; }
+
+ ///
+ /// Gets or sets whether the fixed-up field holds a displacement from the program counter
+ /// rather than an address.
+ ///
+ public bool IsPcRelative { get; set; }
+
+ ///
+ /// Gets or sets the width of the fixed-up field as a power of two, so 2 means four bytes.
+ ///
+ public byte LengthLog2 { get; set; }
+
+ ///
+ /// Gets or sets whether is a symbol index. Never true
+ /// for a scattered entry, which has no room for the flag.
+ ///
+ public bool IsExternal { get; set; }
+
+ ///
+ /// Gets or sets whether this entry carries its target address rather than a symbol or
+ /// section number.
+ ///
+ public bool IsScattered { get; set; }
+
+ ///
+ /// Gets or sets the type, whose meaning depends on the image's architecture. Interpret it as
+ /// , or
+ /// accordingly.
+ ///
+ public byte RawType { get; set; }
+
+ ///
+ /// Gets the width of the fixed-up field in bytes.
+ ///
+ public int LengthInBytes => 1 << LengthLog2;
+
+ ///
+ /// Decodes an entry from the two words it occupies.
+ ///
+ /// The first word, which is the address unless the top bit marks it scattered.
+ /// The second word.
+ /// The decoded entry.
+ public static MachORelocation Decode(uint word0, uint word1)
+ {
+ if ((word0 & ScatteredMask) != 0)
+ {
+ return new MachORelocation
+ {
+ IsScattered = true,
+ Address = (int)(word0 & 0x00ffffff),
+ RawType = (byte)((word0 >> 24) & 0xf),
+ LengthLog2 = (byte)((word0 >> 28) & 0x3),
+ IsPcRelative = ((word0 >> 30) & 1) != 0,
+ Value = (int)word1,
+ };
+ }
+
+ return new MachORelocation
+ {
+ Address = (int)word0,
+ SymbolOrSectionNumber = word1 & 0x00ffffff,
+ IsPcRelative = ((word1 >> 24) & 1) != 0,
+ LengthLog2 = (byte)((word1 >> 25) & 0x3),
+ IsExternal = ((word1 >> 27) & 1) != 0,
+ RawType = (byte)((word1 >> 28) & 0xf),
+ };
+ }
+
+ ///
+ /// Encodes this entry back into the two words it occupies.
+ ///
+ /// The first and second words.
+ public (uint Word0, uint Word1) Encode()
+ {
+ if (IsScattered)
+ {
+ var packed = ScatteredMask
+ | ((uint)Address & 0x00ffffff)
+ | ((uint)(RawType & 0xf) << 24)
+ | ((uint)(LengthLog2 & 0x3) << 28)
+ | (IsPcRelative ? 1u << 30 : 0u);
+ return (packed, (uint)Value);
+ }
+
+ var info = (SymbolOrSectionNumber & 0x00ffffff)
+ | (IsPcRelative ? 1u << 24 : 0u)
+ | ((uint)(LengthLog2 & 0x3) << 25)
+ | (IsExternal ? 1u << 27 : 0u)
+ | ((uint)(RawType & 0xf) << 28);
+ return ((uint)Address, info);
+ }
+
+ ///
+ public override string ToString()
+ => $"{nameof(MachORelocation)} {{ Address = 0x{Address:X}, Type = {RawType}, {LengthInBytes} bytes{(IsPcRelative ? ", pcrel" : string.Empty)}{(IsExternal ? ", external" : string.Empty)}{(IsScattered ? ", scattered" : string.Empty)} }}";
+}
diff --git a/src/LibObjectFile/MachO/MachORelocationType.cs b/src/LibObjectFile/MachO/MachORelocationType.cs
new file mode 100644
index 0000000..1687b6e
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachORelocationType.cs
@@ -0,0 +1,84 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// Relocation types for 32-bit Intel and the other architectures using the generic set.
+///
+///
+/// The numbering restarts for every architecture, so a value only means something once the
+/// image's is known.
+///
+public enum MachOGenericRelocationType : byte
+{
+ /// The relocated field takes the symbol's address (GENERIC_RELOC_VANILLA).
+ Vanilla = 0,
+ /// The second half of a pair, following the entry it belongs to (GENERIC_RELOC_PAIR).
+ Pair = 1,
+ /// The difference between two section-relative addresses (GENERIC_RELOC_SECTDIFF).
+ SectionDifference = 2,
+ /// A prebound lazy pointer (GENERIC_RELOC_PB_LA_PTR).
+ PreboundLazyPointer = 3,
+ /// A section difference where the subtrahend is local (GENERIC_RELOC_LOCAL_SECTDIFF).
+ LocalSectionDifference = 4,
+ /// A thread local variable reference (GENERIC_RELOC_TLV).
+ ThreadLocalVariable = 5,
+}
+
+///
+/// Relocation types for 64-bit Intel.
+///
+public enum MachOX86_64RelocationType : byte
+{
+ /// An absolute address (X86_64_RELOC_UNSIGNED).
+ Unsigned = 0,
+ /// A signed 32-bit displacement (X86_64_RELOC_SIGNED).
+ Signed = 1,
+ /// The target of a call or jump (X86_64_RELOC_BRANCH).
+ Branch = 2,
+ /// A load through the global offset table (X86_64_RELOC_GOT_LOAD).
+ GotLoad = 3,
+ /// Any other global offset table reference (X86_64_RELOC_GOT).
+ Got = 4,
+ /// The subtrahend of a difference, paired with the entry after it (X86_64_RELOC_SUBTRACTOR).
+ Subtractor = 5,
+ /// A signed displacement with an implicit addend of one (X86_64_RELOC_SIGNED_1).
+ Signed1 = 6,
+ /// A signed displacement with an implicit addend of two (X86_64_RELOC_SIGNED_2).
+ Signed2 = 7,
+ /// A signed displacement with an implicit addend of four (X86_64_RELOC_SIGNED_4).
+ Signed4 = 8,
+ /// A thread local variable reference (X86_64_RELOC_TLV).
+ ThreadLocalVariable = 9,
+}
+
+///
+/// Relocation types for 64-bit ARM.
+///
+public enum MachOArm64RelocationType : byte
+{
+ /// An absolute address (ARM64_RELOC_UNSIGNED).
+ Unsigned = 0,
+ /// The subtrahend of a difference, paired with the entry after it (ARM64_RELOC_SUBTRACTOR).
+ Subtractor = 1,
+ /// The target of a 26-bit branch (ARM64_RELOC_BRANCH26).
+ Branch26 = 2,
+ /// The page of a target, for adrp (ARM64_RELOC_PAGE21).
+ Page21 = 3,
+ /// The offset within a page (ARM64_RELOC_PAGEOFF12).
+ PageOffset12 = 4,
+ /// The page of a global offset table entry (ARM64_RELOC_GOT_LOAD_PAGE21).
+ GotLoadPage21 = 5,
+ /// The offset of a global offset table entry within its page (ARM64_RELOC_GOT_LOAD_PAGEOFF12).
+ GotLoadPageOffset12 = 6,
+ /// A pointer to a global offset table entry (ARM64_RELOC_POINTER_TO_GOT).
+ PointerToGot = 7,
+ /// The page of a thread local variable pointer (ARM64_RELOC_TLVP_LOAD_PAGE21).
+ ThreadLocalPage21 = 8,
+ /// The offset of a thread local variable pointer within its page (ARM64_RELOC_TLVP_LOAD_PAGEOFF12).
+ ThreadLocalPageOffset12 = 9,
+ /// An addend applied to the entry after it (ARM64_RELOC_ADDEND).
+ Addend = 10,
+}
diff --git a/src/LibObjectFile/MachO/MachOSection.cs b/src/LibObjectFile/MachO/MachOSection.cs
new file mode 100644
index 0000000..ad20f21
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSection.cs
@@ -0,0 +1,112 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A section inside a .
+///
+///
+/// A section records an absolute file offset rather than one relative to its segment, and it
+/// repeats the name of the segment containing it. Sections whose is a
+/// zero-fill kind occupy address space but no file space, and their
+/// does not point at bytes belonging to them.
+///
+public sealed class MachOSection : MachOObject
+{
+ ///
+ /// Gets or sets the name of the section, such as __text. Truncated to 16 bytes on write.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the name of the segment containing this section, such as __TEXT.
+ ///
+ public string SegmentName { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the virtual address of this section.
+ ///
+ public ulong Address { get; set; }
+
+ ///
+ /// Gets or sets the file offset of this section. Meaningless for zero-fill sections.
+ ///
+ public uint FileOffset { get; set; }
+
+ ///
+ /// Gets or sets the alignment of this section as a power of two.
+ ///
+ public uint Align { get; set; }
+
+ ///
+ /// Gets or sets the file offset of the relocation entries for this section.
+ ///
+ public uint RelocationOffset { get; set; }
+
+ ///
+ /// Gets or sets the number of relocation entries for this section.
+ ///
+ public uint NumberOfRelocations { get; set; }
+
+ ///
+ /// Gets or sets the type of this section, held in the low byte of the raw flags.
+ ///
+ public MachOSectionType SectionType { get; set; }
+
+ ///
+ /// Gets or sets the attributes of this section, held in the upper three bytes of the raw flags.
+ ///
+ public MachOSectionAttributes Attributes { get; set; }
+
+ ///
+ /// Gets or sets the first reserved field. It holds the indirect symbol table index for
+ /// stub and symbol pointer sections, and the ordinal for others.
+ ///
+ public uint Reserved1 { get; set; }
+
+ ///
+ /// Gets or sets the second reserved field. It holds the stub size for
+ /// sections.
+ ///
+ public uint Reserved2 { get; set; }
+
+ ///
+ /// Gets or sets the third reserved field, present only in 64-bit sections.
+ ///
+ public uint Reserved3 { get; set; }
+
+ ///
+ /// Gets a value indicating whether this section occupies no space in the file.
+ ///
+ public bool IsZeroFill => SectionType is MachOSectionType.ZeroFill or MachOSectionType.GBZeroFill or MachOSectionType.ThreadLocalZeroFill;
+
+ ///
+ /// Gets the raw flags value combining and .
+ ///
+ public uint RawFlags => (uint)SectionType | (uint)Attributes;
+
+ ///
+ /// Sets and from a raw flags value.
+ ///
+ /// The raw section flags.
+ public void SetRawFlags(uint rawFlags)
+ {
+ SectionType = (MachOSectionType)(rawFlags & 0xff);
+ Attributes = (MachOSectionAttributes)(rawFlags & 0xffffff00);
+ }
+
+ ///
+ protected override void PrintName(StringBuilder builder) => builder.Append(nameof(MachOSection));
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"{SegmentName},{Name} Address = 0x{Address:X}, Size = 0x{Size:X}, FileOffset = 0x{FileOffset:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOSectionAttributes.cs b/src/LibObjectFile/MachO/MachOSectionAttributes.cs
new file mode 100644
index 0000000..0cc85c0
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSectionAttributes.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Attributes of a section, held in the upper three bytes of the section flags field
+/// (SECTION_ATTRIBUTES). The low byte holds the instead.
+///
+[Flags]
+public enum MachOSectionAttributes : uint
+{
+ /// No attributes set.
+ None = 0,
+ /// Section contains only true machine instructions (S_ATTR_PURE_INSTRUCTIONS).
+ PureInstructions = 0x80000000,
+ /// Section contains coalesced symbols that are not to be in the table of contents (S_ATTR_NO_TOC).
+ NoToc = 0x40000000,
+ /// Static symbols in this section can be stripped (S_ATTR_STRIP_STATIC_SYMS).
+ StripStaticSyms = 0x20000000,
+ /// No dead stripping (S_ATTR_NO_DEAD_STRIP).
+ NoDeadStrip = 0x10000000,
+ /// Blocks are live if they reference live blocks (S_ATTR_LIVE_SUPPORT).
+ LiveSupport = 0x08000000,
+ /// Used in code that can be modified, such as dyld stubs (S_ATTR_SELF_MODIFYING_CODE).
+ SelfModifyingCode = 0x04000000,
+ /// A debug section, which the linker treats specially (S_ATTR_DEBUG).
+ Debug = 0x02000000,
+ /// Section contains some machine instructions (S_ATTR_SOME_INSTRUCTIONS).
+ SomeInstructions = 0x00000400,
+ /// Section has external relocation entries (S_ATTR_EXT_RELOC).
+ ExternalReloc = 0x00000200,
+ /// Section has local relocation entries (S_ATTR_LOC_RELOC).
+ LocalReloc = 0x00000100,
+}
diff --git a/src/LibObjectFile/MachO/MachOSectionType.cs b/src/LibObjectFile/MachO/MachOSectionType.cs
new file mode 100644
index 0000000..7ad4512
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSectionType.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// Type of a section, held in the low byte of the section flags field
+/// (SECTION_TYPE).
+///
+///
+/// The type decides whether the section occupies space in the file. ,
+/// and have a virtual size but no
+/// file content, so their recorded offset does not point at bytes belonging to them.
+///
+public enum MachOSectionType : byte
+{
+ /// Regular section (S_REGULAR).
+ Regular = 0x0,
+ /// Zero-filled on demand, occupying no file space (S_ZEROFILL).
+ ZeroFill = 0x1,
+ /// Only literal C strings (S_CSTRING_LITERALS).
+ CStringLiterals = 0x2,
+ /// Only 4-byte literals (S_4BYTE_LITERALS).
+ FourByteLiterals = 0x3,
+ /// Only 8-byte literals (S_8BYTE_LITERALS).
+ EightByteLiterals = 0x4,
+ /// Only pointers to literals (S_LITERAL_POINTERS).
+ LiteralPointers = 0x5,
+ /// Only non-lazy symbol pointers (S_NON_LAZY_SYMBOL_POINTERS).
+ NonLazySymbolPointers = 0x6,
+ /// Only lazy symbol pointers (S_LAZY_SYMBOL_POINTERS).
+ LazySymbolPointers = 0x7,
+ /// Only symbol stubs; the stub size is in reserved2 (S_SYMBOL_STUBS).
+ SymbolStubs = 0x8,
+ /// Only function pointers for initialization (S_MOD_INIT_FUNC_POINTERS).
+ ModInitFuncPointers = 0x9,
+ /// Only function pointers for termination (S_MOD_TERM_FUNC_POINTERS).
+ ModTermFuncPointers = 0xa,
+ /// Only symbols that are to be coalesced (S_COALESCED).
+ Coalesced = 0xb,
+ /// Zero-filled on demand, and may be larger than 4GB (S_GB_ZEROFILL).
+ GBZeroFill = 0xc,
+ /// Only pairs of function pointers for interposing (S_INTERPOSING).
+ Interposing = 0xd,
+ /// Only 16-byte literals (S_16BYTE_LITERALS).
+ SixteenByteLiterals = 0xe,
+ /// Contains DTrace object format data (S_DTRACE_DOF).
+ DtraceDof = 0xf,
+ /// Only lazy symbol pointers to lazy loaded dylibs (S_LAZY_DYLIB_SYMBOL_POINTERS).
+ LazyDylibSymbolPointers = 0x10,
+ /// Thread local data (S_THREAD_LOCAL_REGULAR).
+ ThreadLocalRegular = 0x11,
+ /// Thread local zero-filled data (S_THREAD_LOCAL_ZEROFILL).
+ ThreadLocalZeroFill = 0x12,
+ /// Thread local variable descriptors (S_THREAD_LOCAL_VARIABLES).
+ ThreadLocalVariables = 0x13,
+ /// Pointers to thread local variable descriptors (S_THREAD_LOCAL_VARIABLE_POINTERS).
+ ThreadLocalVariablePointers = 0x14,
+ /// Functions to call to initialize a thread local variable (S_THREAD_LOCAL_INIT_FUNCTION_POINTERS).
+ ThreadLocalInitFunctionPointers = 0x15,
+ /// 32-bit offsets to initializers (S_INIT_FUNC_OFFSETS).
+ InitFuncOffsets = 0x16,
+}
diff --git a/src/LibObjectFile/MachO/MachOSegment.cs b/src/LibObjectFile/MachO/MachOSegment.cs
new file mode 100644
index 0000000..ae0e26a
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSegment.cs
@@ -0,0 +1,372 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using LibObjectFile.Collections;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A segment load command (LC_SEGMENT or LC_SEGMENT_64) and the file content it maps.
+///
+///
+/// A segment describes a mapping rather than owning bytes: the bytes live in
+/// , and a segment names the file range that is mapped and the
+/// address it is mapped at. The __TEXT segment starts at file offset zero, so it covers
+/// the header and the load commands that describe it as well as its own sections.
+///
+public sealed class MachOSegment : MachOLoadCommand
+{
+ private readonly ObjectList _sections;
+
+ ///
+ /// Initializes a new instance.
+ ///
+ public MachOSegment()
+ {
+ _sections = new ObjectList(this);
+ }
+
+ ///
+ /// Gets or sets the name of the segment, such as __TEXT. Truncated to 16 bytes on write.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the virtual address this segment is mapped at.
+ ///
+ public ulong VmAddress { get; set; }
+
+ ///
+ /// Gets or sets the number of bytes of address space this segment occupies. It may exceed
+ /// , in which case the remainder is zero-filled at load time.
+ ///
+ public ulong VmSize { get; set; }
+
+ ///
+ /// Gets or sets the offset in the file of the bytes this segment maps.
+ ///
+ public ulong FileOffset { get; set; }
+
+ ///
+ /// Gets or sets the number of bytes in the file this segment maps.
+ ///
+ public ulong FileSize { get; set; }
+
+ ///
+ /// Gets or sets the highest protection this segment may be given.
+ ///
+ public MachOVmProtection MaxProtection { get; set; }
+
+ ///
+ /// Gets or sets the protection this segment is mapped with.
+ ///
+ public MachOVmProtection InitProtection { get; set; }
+
+ ///
+ /// Gets or sets the flags of this segment.
+ ///
+ public MachOSegmentFlags SegmentFlags { get; set; }
+
+ ///
+ /// Gets the sections contained in this segment.
+ ///
+ public ObjectList Sections => _sections;
+
+ ///
+ /// Gets or sets whether this segment uses the 64-bit layout.
+ ///
+ public bool Is64Bit { get; set; }
+
+ ///
+ /// Gets the offset one past the last byte this segment maps in the file.
+ ///
+ public ulong FileEndOffset => FileOffset + FileSize;
+
+ ///
+ /// Gets the size in bytes a segment load command occupies for the given section count.
+ ///
+ /// Whether the segment uses the 64-bit layout.
+ /// The number of sections in the segment.
+ /// The value to store in cmdsize.
+ ///
+ /// is negative, or so large that the command would not fit
+ /// in the 32-bit cmdsize field.
+ ///
+ public static unsafe uint ComputeCommandSize(bool is64Bit, int sectionCount)
+ {
+ ArgumentOutOfRangeException.ThrowIfNegative(sectionCount);
+
+ // Computed wide so that a count large enough to overflow is rejected rather than
+ // wrapping to a small, plausible-looking size.
+ var total = GetFixedCommandSize(is64Bit) + (ulong)sectionCount * GetSectionSize(is64Bit);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(total, uint.MaxValue, nameof(sectionCount));
+
+ return (uint)total;
+ }
+
+ ///
+ /// Gets the size of a segment command excluding its section headers.
+ ///
+ /// Whether the segment uses the 64-bit layout.
+ /// The size of a segment_command_64 or a segment_command.
+ public static unsafe uint GetFixedCommandSize(bool is64Bit)
+ => is64Bit ? (uint)sizeof(RawSegmentCommand64) : (uint)sizeof(RawSegmentCommand32);
+
+ ///
+ /// Gets the size of one section header.
+ ///
+ /// Whether the containing segment uses the 64-bit layout.
+ /// The size of a section_64 or a section.
+ public static unsafe uint GetSectionSize(bool is64Bit)
+ => is64Bit ? (uint)sizeof(RawSection64) : (uint)sizeof(RawSection32);
+
+ ///
+ public override unsafe uint MinimumCommandSize
+ => Is64Bit ? (uint)sizeof(RawSegmentCommand64) : (uint)sizeof(RawSegmentCommand32);
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ => Size = ComputeCommandSize(Is64Bit, Sections.Count);
+
+ ///
+ ///
+ /// A section's relocations are a run of bytes elsewhere in the file, so their offset moves
+ /// with them. The section's own file offset is not mapped here: it is placement, fixed by
+ /// the address the section is mapped at, and cannot be changed without moving the section in
+ /// memory too.
+ ///
+ public override void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+
+ foreach (var section in Sections)
+ {
+ if (section.RelocationOffset != 0) section.RelocationOffset = mapper(section.RelocationOffset);
+ }
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (Is64Bit)
+ {
+ if (!reader.TryReadData(sizeof(RawSegmentCommand64), out RawSegmentCommand64 raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated 64-bit segment command at 0x{Position:X}");
+ return;
+ }
+
+ Name = MachOName.Read(new ReadOnlySpan(raw.SegmentName, MachOName.Length));
+ VmAddress = raw.VmAddress;
+ VmSize = raw.VmSize;
+ FileOffset = raw.FileOffset;
+ FileSize = raw.FileSize;
+ MaxProtection = (MachOVmProtection)raw.MaxProtection;
+ InitProtection = (MachOVmProtection)raw.InitProtection;
+ SegmentFlags = (MachOSegmentFlags)raw.Flags;
+ ReadSections(reader, raw.NumberOfSections);
+ }
+ else
+ {
+ if (!reader.TryReadData(sizeof(RawSegmentCommand32), out RawSegmentCommand32 raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated 32-bit segment command at 0x{Position:X}");
+ return;
+ }
+
+ Name = MachOName.Read(new ReadOnlySpan(raw.SegmentName, MachOName.Length));
+ VmAddress = raw.VmAddress;
+ VmSize = raw.VmSize;
+ FileOffset = raw.FileOffset;
+ FileSize = raw.FileSize;
+ MaxProtection = (MachOVmProtection)raw.MaxProtection;
+ InitProtection = (MachOVmProtection)raw.InitProtection;
+ SegmentFlags = (MachOSegmentFlags)raw.Flags;
+ ReadSections(reader, raw.NumberOfSections);
+ }
+ }
+
+ private unsafe void ReadSections(MachOReader reader, uint count)
+ {
+ Sections.Clear();
+
+ // The section headers follow the fixed part inside this command, so a count that does not
+ // fit would read whatever comes after it. The largest count that fits is derived by
+ // division: multiplying the declared count out could wrap and pass a check it should not.
+ var fixedSize = GetFixedCommandSize(Is64Bit);
+ if (Size < fixedSize || count > (Size - fixedSize) / GetSectionSize(Is64Bit))
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_InvalidLoadCommandSize,
+ $"Segment {Name} declares {count} sections, which do not fit in its cmdsize of {Size}");
+ return;
+ }
+
+ for (uint i = 0; i < count; i++)
+ {
+ var section = new MachOSection();
+ if (Is64Bit)
+ {
+ if (!reader.TryReadData(sizeof(RawSection64), out RawSection64 raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated 64-bit section header in segment {Name}");
+ return;
+ }
+
+ section.Name = MachOName.Read(new ReadOnlySpan(raw.SectionName, MachOName.Length));
+ section.SegmentName = MachOName.Read(new ReadOnlySpan(raw.SegmentName, MachOName.Length));
+ section.Address = raw.Address;
+ section.Size = raw.Size;
+ section.FileOffset = raw.Offset;
+ section.Align = raw.Align;
+ section.RelocationOffset = raw.RelocationOffset;
+ section.NumberOfRelocations = raw.NumberOfRelocations;
+ section.SetRawFlags(raw.Flags);
+ section.Reserved1 = raw.Reserved1;
+ section.Reserved2 = raw.Reserved2;
+ section.Reserved3 = raw.Reserved3;
+ }
+ else
+ {
+ if (!reader.TryReadData(sizeof(RawSection32), out RawSection32 raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated 32-bit section header in segment {Name}");
+ return;
+ }
+
+ section.Name = MachOName.Read(new ReadOnlySpan(raw.SectionName, MachOName.Length));
+ section.SegmentName = MachOName.Read(new ReadOnlySpan(raw.SegmentName, MachOName.Length));
+ section.Address = raw.Address;
+ section.Size = raw.Size;
+ section.FileOffset = raw.Offset;
+ section.Align = raw.Align;
+ section.RelocationOffset = raw.RelocationOffset;
+ section.NumberOfRelocations = raw.NumberOfRelocations;
+ section.SetRawFlags(raw.Flags);
+ section.Reserved1 = raw.Reserved1;
+ section.Reserved2 = raw.Reserved2;
+ }
+
+ Sections.Add(section);
+ }
+ }
+
+ ///
+ public override unsafe void Write(MachOWriter writer)
+ {
+ if (Is64Bit)
+ {
+ var raw = new RawSegmentCommand64
+ {
+ Cmd = (uint)Type,
+ CmdSize = ComputeCommandSize(true, Sections.Count),
+ VmAddress = VmAddress,
+ VmSize = VmSize,
+ FileOffset = FileOffset,
+ FileSize = FileSize,
+ MaxProtection = (uint)MaxProtection,
+ InitProtection = (uint)InitProtection,
+ NumberOfSections = (uint)Sections.Count,
+ Flags = (uint)SegmentFlags,
+ };
+ MachOName.Write(new Span(raw.SegmentName, MachOName.Length), Name);
+ writer.Write(raw);
+ }
+ else
+ {
+ // A 32-bit segment stores these as 32-bit fields, and casting a value that does not
+ // fit would write a different segment rather than fail.
+ if (VmAddress > uint.MaxValue || VmSize > uint.MaxValue || FileOffset > uint.MaxValue || FileSize > uint.MaxValue)
+ {
+ writer.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit,
+ $"Segment {Name} has a value that does not fit a 32-bit image: VmAddress = 0x{VmAddress:X}, VmSize = 0x{VmSize:X}, FileOffset = 0x{FileOffset:X}, FileSize = 0x{FileSize:X}");
+ return;
+ }
+
+ var raw = new RawSegmentCommand32
+ {
+ Cmd = (uint)Type,
+ CmdSize = ComputeCommandSize(false, Sections.Count),
+ VmAddress = (uint)VmAddress,
+ VmSize = (uint)VmSize,
+ FileOffset = (uint)FileOffset,
+ FileSize = (uint)FileSize,
+ MaxProtection = (uint)MaxProtection,
+ InitProtection = (uint)InitProtection,
+ NumberOfSections = (uint)Sections.Count,
+ Flags = (uint)SegmentFlags,
+ };
+ MachOName.Write(new Span(raw.SegmentName, MachOName.Length), Name);
+ writer.Write(raw);
+ }
+
+ foreach (var section in Sections)
+ {
+ WriteSection(writer, section);
+ }
+ }
+
+ private unsafe void WriteSection(MachOWriter writer, MachOSection section)
+ {
+ if (Is64Bit)
+ {
+ var raw = new RawSection64
+ {
+ Address = section.Address,
+ Size = section.Size,
+ Offset = section.FileOffset,
+ Align = section.Align,
+ RelocationOffset = section.RelocationOffset,
+ NumberOfRelocations = section.NumberOfRelocations,
+ Flags = section.RawFlags,
+ Reserved1 = section.Reserved1,
+ Reserved2 = section.Reserved2,
+ Reserved3 = section.Reserved3,
+ };
+ MachOName.Write(new Span(raw.SectionName, MachOName.Length), section.Name);
+ MachOName.Write(new Span(raw.SegmentName, MachOName.Length), section.SegmentName);
+ writer.Write(raw);
+ }
+ else
+ {
+ if (section.Address > uint.MaxValue || section.Size > uint.MaxValue)
+ {
+ writer.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_ValueTooLargeFor32Bit,
+ $"Section {section.SegmentName},{section.Name} has a value that does not fit a 32-bit image: Address = 0x{section.Address:X}, Size = 0x{section.Size:X}");
+ return;
+ }
+
+ var raw = new RawSection32
+ {
+ Address = (uint)section.Address,
+ Size = (uint)section.Size,
+ Offset = section.FileOffset,
+ Align = section.Align,
+ RelocationOffset = section.RelocationOffset,
+ NumberOfRelocations = section.NumberOfRelocations,
+ Flags = section.RawFlags,
+ Reserved1 = section.Reserved1,
+ Reserved2 = section.Reserved2,
+ };
+ MachOName.Write(new Span(raw.SectionName, MachOName.Length), section.Name);
+ MachOName.Write(new Span(raw.SegmentName, MachOName.Length), section.SegmentName);
+ writer.Write(raw);
+ }
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"{Name} VmAddress = 0x{VmAddress:X}, VmSize = 0x{VmSize:X}, FileOffset = 0x{FileOffset:X}, FileSize = 0x{FileSize:X}, Sections = {Sections.Count}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOSegmentFlags.cs b/src/LibObjectFile/MachO/MachOSegmentFlags.cs
new file mode 100644
index 0000000..18cf787
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSegmentFlags.cs
@@ -0,0 +1,27 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Flags of a segment, as stored in the flags field of a segment load command.
+///
+[Flags]
+public enum MachOSegmentFlags : uint
+{
+ /// No flags set.
+ None = 0,
+ /// The file contents of this segment are for the high part of the VM space (SG_HIGHVM).
+ HighVM = 0x1,
+ /// The segment is the virtual memory module table, obsolete (SG_FVMLIB).
+ FixedVMLibrary = 0x2,
+ /// The segment contents are exactly the file contents and may be mapped directly (SG_NORELOC).
+ NoReloc = 0x4,
+ /// The first page is protected, and the rest becomes read-only after relocation (SG_PROTECTED_VERSION_1).
+ ProtectedVersion1 = 0x8,
+ /// The segment is read-only after the fixups have been applied (SG_READ_ONLY).
+ ReadOnly = 0x10,
+}
diff --git a/src/LibObjectFile/MachO/MachOSourceVersionCommand.cs b/src/LibObjectFile/MachO/MachOSourceVersionCommand.cs
new file mode 100644
index 0000000..c3e37e9
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSourceVersionCommand.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The source version load command (LC_SOURCE_VERSION), recording the version of the
+/// source the image was built from.
+///
+public sealed class MachOSourceVersionCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 16;
+
+ ///
+ /// Gets or sets the packed source version, which holds five parts in 24, 10, 10, 10 and 10 bits.
+ ///
+ public ulong SourceVersion { get; set; }
+
+ ///
+ /// Gets the first four components of the source version.
+ ///
+ public Version Version => MachOVersion.DecodeSource(SourceVersion);
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawSourceVersionCommand), out RawSourceVersionCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_SOURCE_VERSION at 0x{Position:X}");
+ return;
+ }
+
+ SourceVersion = raw.Version;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ => writer.Write(new RawSourceVersionCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ Version = SourceVersion,
+ });
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Version = {Version}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOSymbol.cs b/src/LibObjectFile/MachO/MachOSymbol.cs
new file mode 100644
index 0000000..bb8d5dd
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSymbol.cs
@@ -0,0 +1,98 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// An entry of the symbol table.
+///
+///
+/// The type byte packs four things at once: whether the entry is debug information, whether the
+/// symbol is external or private external, and what it refers to. They are separated here, since
+/// reading the byte as a single value gets the common cases wrong.
+///
+public sealed class MachOSymbol
+{
+ /// Bits of the type byte holding debug information rather than a symbol (N_STAB).
+ public const byte StabMask = 0xe0;
+
+ /// Bit of the type byte marking a symbol as private external (N_PEXT).
+ public const byte PrivateExternalMask = 0x10;
+
+ /// Bits of the type byte holding the (N_TYPE).
+ public const byte KindMask = 0x0e;
+
+ /// Bit of the type byte marking a symbol as external (N_EXT).
+ public const byte ExternalMask = 0x01;
+
+ ///
+ /// Gets or sets the name of the symbol.
+ ///
+ public string Name { get; set; } = string.Empty;
+
+ ///
+ /// Gets or sets the offset of the name in the string table.
+ ///
+ public uint NameOffset { get; set; }
+
+ ///
+ /// Gets or sets the raw type byte.
+ ///
+ public byte RawType { get; set; }
+
+ ///
+ /// Gets or sets the one-based index of the section defining this symbol, or zero when no
+ /// section does (NO_SECT).
+ ///
+ public byte SectionIndex { get; set; }
+
+ ///
+ /// Gets or sets the description field, whose meaning depends on the symbol. For an undefined
+ /// symbol in a two-level image its high byte is the ordinal of the library defining it.
+ ///
+ public ushort Description { get; set; }
+
+ ///
+ /// Gets or sets the value of the symbol, which is its address when
+ /// is .
+ ///
+ public ulong Value { get; set; }
+
+ ///
+ /// Gets a value indicating whether this entry is debug information rather than a symbol.
+ ///
+ ///
+ /// A debug entry reuses the other fields for its own purposes, so the rest of this type does
+ /// not describe it.
+ ///
+ public bool IsDebug => (RawType & StabMask) != 0;
+
+ ///
+ /// Gets what this symbol refers to. Meaningless when is true.
+ ///
+ public MachOSymbolKind Kind => (MachOSymbolKind)(RawType & KindMask);
+
+ ///
+ /// Gets a value indicating whether the symbol is visible outside this image.
+ ///
+ public bool IsExternal => (RawType & ExternalMask) != 0;
+
+ ///
+ /// Gets a value indicating whether the symbol is visible only to what was linked with it.
+ ///
+ public bool IsPrivateExternal => (RawType & PrivateExternalMask) != 0;
+
+ ///
+ /// Gets the ordinal of the library expected to define this symbol, for an undefined symbol
+ /// in a two-level namespace image. The ordinal is a one-based index into the dylib commands
+ /// in the order they appear, which is why inserting one anywhere but last renumbers them.
+ ///
+ public int LibraryOrdinal => (Description >> 8) & 0xff;
+
+ ///
+ public override string ToString()
+ => $"{nameof(MachOSymbol)} {{ {Name}, {Kind}{(IsExternal ? ", external" : string.Empty)}, Value = 0x{Value:X} }}";
+}
diff --git a/src/LibObjectFile/MachO/MachOSymbolKind.cs b/src/LibObjectFile/MachO/MachOSymbolKind.cs
new file mode 100644
index 0000000..f1fe63a
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSymbolKind.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+namespace LibObjectFile.MachO;
+
+///
+/// What a symbol refers to, held in the N_TYPE bits of a symbol's type byte.
+///
+public enum MachOSymbolKind : byte
+{
+ /// The symbol is not defined in this image (N_UNDF).
+ Undefined = 0x0,
+ /// The symbol has an absolute value that no section owns (N_ABS).
+ Absolute = 0x2,
+ /// The symbol is an alias for another, named by its string index (N_INDR).
+ Indirect = 0xa,
+ /// The symbol was undefined in a prebound image (N_PBUD).
+ PreboundUndefined = 0xc,
+ /// The symbol is defined in the section given by its section index (N_SECT).
+ Section = 0xe,
+}
diff --git a/src/LibObjectFile/MachO/MachOSymbolTableCommand.cs b/src/LibObjectFile/MachO/MachOSymbolTableCommand.cs
new file mode 100644
index 0000000..7442a53
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOSymbolTableCommand.cs
@@ -0,0 +1,103 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The symbol table load command (LC_SYMTAB), locating the symbol and string tables in
+/// __LINKEDIT.
+///
+///
+/// counts entries rather than bytes, so the size of the table depends
+/// on whether the image is 32- or 64-bit. is a byte count.
+///
+public sealed class MachOSymbolTableCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 24;
+
+ ///
+ /// Gets or sets the file offset of the symbol table.
+ ///
+ public uint SymbolOffset { get; set; }
+
+ ///
+ /// Gets or sets the number of entries in the symbol table.
+ ///
+ public uint SymbolCount { get; set; }
+
+ ///
+ /// Gets or sets the file offset of the string table.
+ ///
+ public uint StringOffset { get; set; }
+
+ ///
+ /// Gets or sets the size in bytes of the string table.
+ ///
+ public uint StringSize { get; set; }
+
+ ///
+ /// Gets the size in bytes of one symbol table entry for the given image width.
+ ///
+ /// Whether the containing image is 64-bit.
+ /// 16 for a 64-bit image, 12 for a 32-bit one.
+ public static uint GetSymbolSize(bool is64Bit) => is64Bit ? 16u : 12u;
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+ if (SymbolOffset != 0) SymbolOffset = mapper(SymbolOffset);
+ if (StringOffset != 0) StringOffset = mapper(StringOffset);
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawSymtabCommand), out RawSymtabCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_SYMTAB at 0x{Position:X}");
+ return;
+ }
+
+ SymbolOffset = raw.SymbolOffset;
+ SymbolCount = raw.SymbolCount;
+ StringOffset = raw.StringOffset;
+ StringSize = raw.StringSize;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.Write(new RawSymtabCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ SymbolOffset = SymbolOffset,
+ SymbolCount = SymbolCount,
+ StringOffset = StringOffset,
+ StringSize = StringSize,
+ });
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Symbols = {SymbolCount} at 0x{SymbolOffset:X}, Strings = 0x{StringSize:X} bytes at 0x{StringOffset:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOThreadCommand.cs b/src/LibObjectFile/MachO/MachOThreadCommand.cs
new file mode 100644
index 0000000..93f89c2
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOThreadCommand.cs
@@ -0,0 +1,133 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A thread state load command (LC_THREAD or LC_UNIXTHREAD).
+///
+///
+/// Before LC_MAIN, an executable specified its entry point by handing the kernel a whole
+/// register state to restore, with the program counter set to the entry. Which register holds
+/// the program counter depends on the architecture and the flavour, so the register words are
+/// kept as they are rather than being interpreted here.
+///
+/// LC_UNIXTHREAD also asks the kernel to set up a stack, which is the difference from
+/// LC_THREAD.
+///
+///
+public sealed class MachOThreadCommand : MachOLoadCommand
+{
+ ///
+ /// Gets the thread states carried by this command. The format allows more than one, though
+ /// a linker emits a single state.
+ ///
+ public List States { get; } = [];
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context)
+ {
+ uint total = 8;
+ foreach (var state in States)
+ {
+ total += 8 + (uint)state.Registers.Length * 4;
+ }
+ Size = total;
+ }
+
+ ///
+ public override void Read(MachOReader reader)
+ {
+ var commandPosition = reader.Position;
+ reader.ReadU32();
+ reader.ReadU32();
+
+ States.Clear();
+ var end = commandPosition + Size;
+ while (reader.Position + 8 <= end)
+ {
+ var flavor = reader.ReadU32();
+ var count = reader.ReadU32();
+
+ if (reader.Position + (ulong)count * 4 > end)
+ {
+ reader.Diagnostics.Error(
+ DiagnosticId.MACHO_ERR_TruncatedLoadCommand,
+ $"The thread state at 0x{commandPosition:X} claims {count} registers, which do not fit in the command");
+ return;
+ }
+
+ var registers = new uint[count];
+ for (var i = 0; i < count; i++)
+ {
+ registers[i] = reader.ReadU32();
+ }
+
+ States.Add(new MachOThreadState(flavor, registers));
+ }
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.WriteU32((uint)Type);
+ writer.WriteU32((uint)Size);
+
+ foreach (var state in States)
+ {
+ writer.WriteU32(state.Flavor);
+ writer.WriteU32((uint)state.Registers.Length);
+ foreach (var register in state.Registers)
+ {
+ writer.WriteU32(register);
+ }
+ }
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Type = {Type}, States = {States.Count}");
+ return true;
+ }
+}
+
+///
+/// One register state inside a .
+///
+public sealed class MachOThreadState
+{
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The architecture-specific flavour identifying the register layout.
+ /// The register words.
+ /// is null.
+ public MachOThreadState(uint flavor, uint[] registers)
+ {
+ ArgumentNullException.ThrowIfNull(registers);
+ Flavor = flavor;
+ Registers = registers;
+ }
+
+ ///
+ /// Gets or sets the flavour, which says which register layout the words follow. The values
+ /// are architecture-specific, so the same number means different things on x86 and ARM.
+ ///
+ public uint Flavor { get; set; }
+
+ ///
+ /// Gets or sets the register words, counted in 32-bit units even on a 64-bit architecture
+ /// where each register spans two of them.
+ ///
+ public uint[] Registers { get; set; }
+
+ ///
+ public override string ToString() => $"{nameof(MachOThreadState)} {{ Flavor = {Flavor}, Registers = {Registers.Length} }}";
+}
diff --git a/src/LibObjectFile/MachO/MachOTwoLevelHintsCommand.cs b/src/LibObjectFile/MachO/MachOTwoLevelHintsCommand.cs
new file mode 100644
index 0000000..539f9d6
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOTwoLevelHintsCommand.cs
@@ -0,0 +1,82 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The two-level namespace hint table load command (LC_TWOLEVEL_HINTS).
+///
+///
+/// The table lets the loader skip searching for a symbol it already knows the library and index
+/// of. It is a relic of the prebinding era and current linkers no longer emit it, but images
+/// from that era still carry one, and it records a file offset that has to move with the table.
+///
+public sealed class MachOTwoLevelHintsCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 16;
+
+ /// The size of one hint entry (twolevel_hint).
+ public const uint HintSize = 4;
+
+ ///
+ /// Gets or sets the file offset of the hint table.
+ ///
+ public uint Offset { get; set; }
+
+ ///
+ /// Gets or sets the number of hints, each a 4-byte entry.
+ ///
+ public uint HintCount { get; set; }
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override void UpdateFileOffsets(Func mapper)
+ {
+ ArgumentNullException.ThrowIfNull(mapper);
+ if (Offset != 0) Offset = mapper(Offset);
+ }
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawTwoLevelHintsCommand), out RawTwoLevelHintsCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_TWOLEVEL_HINTS at 0x{Position:X}");
+ return;
+ }
+
+ Offset = raw.Offset;
+ HintCount = raw.HintCount;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ => writer.Write(new RawTwoLevelHintsCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ Offset = Offset,
+ HintCount = HintCount,
+ });
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Hints = {HintCount} at 0x{Offset:X}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOUnknownLoadCommand.cs b/src/LibObjectFile/MachO/MachOUnknownLoadCommand.cs
new file mode 100644
index 0000000..da8a47c
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOUnknownLoadCommand.cs
@@ -0,0 +1,43 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A load command that is kept as raw bytes rather than being decoded.
+///
+///
+/// This is the fallback for command types the library does not model, and it is what makes a
+/// byte-exact round-trip possible for images using commands added after this code was written.
+/// The payload excludes the cmd and cmdsize fields, which are rebuilt on write.
+///
+public sealed class MachOUnknownLoadCommand : MachOLoadCommand
+{
+ ///
+ /// Gets or sets the raw payload following the cmd and cmdsize fields.
+ ///
+ public byte[] Payload { get; set; } = [];
+
+ ///
+ public override void Read(MachOReader reader)
+ {
+ reader.ReadU32();
+ reader.ReadU32();
+ Payload = new byte[Size - 8];
+ reader.ReadExactly(Payload);
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ {
+ writer.WriteU32((uint)Type);
+ writer.WriteU32((uint)(8 + Payload.Length));
+ writer.Write(Payload);
+ }
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = (ulong)(8 + Payload.Length);
+}
diff --git a/src/LibObjectFile/MachO/MachOUuidCommand.cs b/src/LibObjectFile/MachO/MachOUuidCommand.cs
new file mode 100644
index 0000000..d9d2fde
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOUuidCommand.cs
@@ -0,0 +1,73 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// The image identifier load command (LC_UUID).
+///
+///
+/// The identifier is what pairs a binary with its separate debug companion, so a debugger will
+/// refuse a dSYM whose value does not match.
+///
+public sealed class MachOUuidCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 24;
+
+ ///
+ /// Gets or sets the identifier of this image.
+ ///
+ ///
+ /// The 16 bytes are stored in the order they appear in the file, which is the big-endian
+ /// order reads with bigEndian: true.
+ /// Reading them the other way round would reverse the first three fields and print an
+ /// identifier that no other tool agrees with.
+ ///
+ public Guid Uuid { get; set; }
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawUuidCommand), out RawUuidCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated LC_UUID at 0x{Position:X}");
+ return;
+ }
+
+ Uuid = new Guid(new ReadOnlySpan(raw.Uuid, 16), bigEndian: true);
+ }
+
+ ///
+ public override unsafe void Write(MachOWriter writer)
+ {
+ var raw = new RawUuidCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ };
+ Uuid.TryWriteBytes(new Span(raw.Uuid, 16), bigEndian: true, out _);
+ writer.Write(raw);
+ }
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Uuid = {Uuid:D}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOVersion.cs b/src/LibObjectFile/MachO/MachOVersion.cs
new file mode 100644
index 0000000..18a5453
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOVersion.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Helpers for the packed version numbers Mach-O stores.
+///
+public static class MachOVersion
+{
+ ///
+ /// Unpacks a 32-bit version, which stores its three parts in 16, 8 and 8 bits. This is the
+ /// encoding used by dylib versions and by the minimum OS version commands.
+ ///
+ /// The packed value.
+ /// The version it encodes.
+ public static Version Decode(uint packed)
+ => new((int)(packed >> 16), (int)((packed >> 8) & 0xff), (int)(packed & 0xff));
+
+ ///
+ /// Packs a version into the 16.8.8 encoding.
+ ///
+ /// The version to pack. A negative build or revision is treated as zero.
+ /// The packed value.
+ /// is null.
+ /// A component does not fit in the encoding.
+ public static uint Encode(Version version)
+ {
+ ArgumentNullException.ThrowIfNull(version);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(version.Major, 0xffff);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(version.Minor, 0xff);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(Math.Max(version.Build, 0), 0xff);
+
+ return ((uint)version.Major << 16) | ((uint)version.Minor << 8) | (uint)Math.Max(version.Build, 0);
+ }
+
+ ///
+ /// Unpacks a 64-bit source version, which stores five parts in 24, 10, 10, 10 and 10 bits.
+ /// The last part is dropped, since holds only four.
+ ///
+ /// The packed value.
+ /// The first four components of the version.
+ public static Version DecodeSource(ulong packed)
+ => new((int)(packed >> 40), (int)((packed >> 30) & 0x3ff), (int)((packed >> 20) & 0x3ff), (int)((packed >> 10) & 0x3ff));
+}
diff --git a/src/LibObjectFile/MachO/MachOVersionMinCommand.cs b/src/LibObjectFile/MachO/MachOVersionMinCommand.cs
new file mode 100644
index 0000000..a0ef3c1
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOVersionMinCommand.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+using System.Text;
+using LibObjectFile.Diagnostics;
+using LibObjectFile.MachO.Internal;
+
+namespace LibObjectFile.MachO;
+
+///
+/// A minimum OS version load command, covering LC_VERSION_MIN_MACOSX,
+/// LC_VERSION_MIN_IPHONEOS, LC_VERSION_MIN_TVOS and LC_VERSION_MIN_WATCHOS.
+///
+///
+/// The platform is encoded in the command type, which is why there is one command per platform.
+/// replaced the whole family with a single command
+/// carrying the platform as a field.
+///
+public sealed class MachOVersionMinCommand : MachOLoadCommand
+{
+ ///
+ /// The size of this command, which is fixed.
+ ///
+ public const uint CommandSize = 16;
+
+ ///
+ /// Gets or sets the packed minimum OS version.
+ ///
+ public uint MinOSVersion { get; set; }
+
+ ///
+ /// Gets or sets the packed SDK version the image was built against.
+ ///
+ public uint SdkVersion { get; set; }
+
+ ///
+ /// Gets the minimum OS version.
+ ///
+ public Version MinOS => MachOVersion.Decode(MinOSVersion);
+
+ ///
+ /// Gets the SDK version.
+ ///
+ public Version Sdk => MachOVersion.Decode(SdkVersion);
+
+ ///
+ public override uint MinimumCommandSize => CommandSize;
+
+ ///
+ protected override void UpdateLayoutCore(MachOVisitorContext context) => Size = CommandSize;
+
+ ///
+ public override unsafe void Read(MachOReader reader)
+ {
+ if (!reader.TryReadData(sizeof(RawVersionMinCommand), out RawVersionMinCommand raw))
+ {
+ reader.Diagnostics.Error(DiagnosticId.MACHO_ERR_TruncatedLoadCommand, $"Truncated {Type} at 0x{Position:X}");
+ return;
+ }
+
+ MinOSVersion = raw.Version;
+ SdkVersion = raw.Sdk;
+ }
+
+ ///
+ public override void Write(MachOWriter writer)
+ => writer.Write(new RawVersionMinCommand
+ {
+ Cmd = (uint)Type,
+ CmdSize = (uint)Size,
+ Version = MinOSVersion,
+ Sdk = SdkVersion,
+ });
+
+ ///
+ protected override bool PrintMembers(StringBuilder builder)
+ {
+ builder.Append($"Type = {Type}, MinOS = {MinOS}, Sdk = {Sdk}");
+ return true;
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOVisitorContext.cs b/src/LibObjectFile/MachO/MachOVisitorContext.cs
new file mode 100644
index 0000000..e137022
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOVisitorContext.cs
@@ -0,0 +1,17 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Context used when laying out or verifying a .
+///
+public sealed class MachOVisitorContext : VisitorContextBase
+{
+ internal MachOVisitorContext(MachOFile file, DiagnosticBag diagnostics) : base(file, diagnostics)
+ {
+ }
+}
diff --git a/src/LibObjectFile/MachO/MachOVmProtection.cs b/src/LibObjectFile/MachO/MachOVmProtection.cs
new file mode 100644
index 0000000..568b954
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOVmProtection.cs
@@ -0,0 +1,24 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Virtual memory protection applied to a segment, as stored in the maxprot and
+/// initprot fields of a segment load command.
+///
+[Flags]
+public enum MachOVmProtection : uint
+{
+ /// No access (VM_PROT_NONE).
+ None = 0,
+ /// Read access (VM_PROT_READ).
+ Read = 0x1,
+ /// Write access (VM_PROT_WRITE).
+ Write = 0x2,
+ /// Execute access (VM_PROT_EXECUTE).
+ Execute = 0x4,
+}
diff --git a/src/LibObjectFile/MachO/MachOWriter.cs b/src/LibObjectFile/MachO/MachOWriter.cs
new file mode 100644
index 0000000..3fd7185
--- /dev/null
+++ b/src/LibObjectFile/MachO/MachOWriter.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Alexandre Mutel. All rights reserved.
+// This file is licensed under the BSD-Clause 2 license.
+// See the license.txt file in the project root for more information.
+
+using System.IO;
+using LibObjectFile.Diagnostics;
+
+namespace LibObjectFile.MachO;
+
+///
+/// Writes a to a .
+///
+public sealed class MachOWriter : ObjectFileReaderWriter
+{
+ internal MachOWriter(MachOFile file, Stream stream, DiagnosticBag diagnostics) : base(file, stream, diagnostics)
+ {
+ VisitorContext = new MachOVisitorContext(file, Diagnostics);
+ }
+
+ ///
+ /// Gets the file being written.
+ ///
+ public new MachOFile File => (MachOFile)base.File;
+
+ ///
+ /// Gets the context carrying the diagnostics collected while writing.
+ ///
+ public MachOVisitorContext VisitorContext { get; }
+
+ ///
+ public override bool KeepOriginalStreamForSubStreams => false;
+
+ ///
+ /// Converts a writer to the visitor context it carries.
+ ///
+ /// The writer.
+ public static implicit operator MachOVisitorContext(MachOWriter writer) => writer.VisitorContext;
+}