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; +}