Skip to content

Commit 1b9d7cf

Browse files
Add doc for the Mach-O API
The readme listed Mach-O among the formats left for contributors, and the manual did not mention it. Adds it to both, following how the other formats are covered: a feature list in the readme and a section in the manual with an overview, reading, writing, editing and signing. The overview states the relationship a caller has to know about, that a section's address is its segment's address plus the section's distance from the segment's file offset, since that is what decides which parts of an image a layout may move and why adding a load command is bounded by the padding the linker left. - readme.md: Mach-O added to the supported formats and dropped from the longer term plan - doc/readme.md: the manual section
1 parent 0d7501c commit 1b9d7cf

2 files changed

Lines changed: 129 additions & 1 deletion

File tree

‎doc/readme.md‎

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ This is the manual of LibObjectFile with the following API covered:
55
- [ELF Object File Format](#elf-object-file-format) via the `ElfFile` API
66
- [Archive ar File Format](#archive-ar-file-format) via the `ArArchiveFile` API
77
- [PE File Format](#pe-object-file-format) via the `PEFile` API
8+
- [Mach-O File Format](#mach-o-object-file-format) via the `MachOFile` API
89

910
## ELF Object File Format
1011

@@ -545,3 +546,119 @@ Assert.AreEqual(156, process.ExitCode);
545546
### Links
546547

547548
- [PE and COFF Specification](https://docs.microsoft.com/en-us/windows/win32/debug/pe-format)
549+
550+
## Mach-O Object File Format
551+
552+
### Overview
553+
554+
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.
555+
556+
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.
557+
558+
#### Content
559+
560+
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.
561+
562+
Padding is kept as the bytes that were read rather than regenerated, because a linker pads executable sections with `nop` rather than zeros.
563+
564+
#### Addresses and what a layout may move
565+
566+
A section's address is its segment's address plus the section's distance from the segment's file offset:
567+
568+
```
569+
section.Address - segment.VmAddress == section.FileOffset - segment.FileOffset
570+
```
571+
572+
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.
573+
574+
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.
575+
576+
#### Universal binaries
577+
578+
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:
579+
580+
```csharp
581+
if (MachOFatFile.IsFat(inputStream))
582+
{
583+
var fat = MachOFatFile.Read(inputStream);
584+
foreach (var slice in fat.Slices)
585+
{
586+
Console.WriteLine($"{slice.CpuType}: {slice.File!.FileType}");
587+
}
588+
}
589+
```
590+
591+
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.
592+
593+
### Reading a Mach-O File
594+
595+
The Mach-O API allows to read from a `System.IO.Stream` via the method `MachOFile.Read`:
596+
597+
```csharp
598+
MachOFile machO = MachOFile.Read(inputStream);
599+
foreach (var segment in machO.Segments)
600+
{
601+
Console.WriteLine($"{segment.Name} at 0x{segment.VmAddress:X}");
602+
}
603+
```
604+
605+
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.
606+
607+
The symbol table, the indirect symbol table and the relocations of a section are decoded on demand:
608+
609+
```csharp
610+
foreach (var symbol in machO.ReadSymbolTable())
611+
{
612+
Console.WriteLine($"{symbol.Name} {symbol.Kind}");
613+
}
614+
```
615+
616+
### Writing a Mach-O File
617+
618+
A `MachOFile` is written back with `MachOFile.Write`:
619+
620+
```csharp
621+
machO.Write(outputStream);
622+
```
623+
624+
Content is written at the offset recorded for it, so an image that has not been edited comes back byte for byte. Use `MachOFile.Verify` to check an image before writing it.
625+
626+
### Editing load commands
627+
628+
`MachOFile` offers the operations `install_name_tool` provides, appending to the padding the linker left so that no content moves:
629+
630+
```csharp
631+
machO.AddLoadDylib("@executable_path/../Frameworks/mylib.dylib");
632+
machO.AddRPath("@executable_path/../Frameworks");
633+
machO.ChangeDylibName("/usr/lib/libfoo.dylib", "@rpath/libfoo.dylib");
634+
```
635+
636+
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.
637+
638+
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.
639+
640+
### Code signing
641+
642+
Apple Silicon refuses to execute an unsigned image, so an edited `arm64` binary has to be signed again to stay runnable:
643+
644+
```csharp
645+
machO.AddRPath("@executable_path/../Frameworks");
646+
machO.AdHocSign("mytool");
647+
machO.Write(outputStream);
648+
```
649+
650+
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.
651+
652+
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.
653+
654+
### Printing a Mach-O File
655+
656+
`MachOFile.Print` writes the header and every load command in a form close to `otool -h -l`:
657+
658+
```csharp
659+
machO.Print(Console.Out);
660+
```
661+
662+
### Links
663+
664+
- [OS X ABI Mach-O File Format Reference](https://github.com/aidansteele/osx-abi-macho-file-format-reference)

‎readme.md‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ LibObjectFile is a .NET library to read, manipulate and write linker and executa
88
>
99
> - **PE** image file format (Portable Executable / DLL)
1010
> - **ELF** object-file format
11+
> - **Mach-O** object-file format, including universal binaries
1112
> - **DWARF** debugging format (version 4)
1213
> - **Archive `ar`** file format (Common, GNU and BSD variants)
1314
>
14-
> There is a longer term plan to support other file formats (e.g COFF, MACH-O, .lib) but as I don't
15+
> There is a longer term plan to support other file formats (e.g COFF, .lib) but as I don't
1516
> have a need for them right now, it is left as an exercise for PR contributors! ;)
1617
1718
## Usage
@@ -54,6 +55,16 @@ elf.Write(outStream);
5455
- Program headers with or without sections
5556
- `ElfFile.AddNeededLibrary` injects a `DT_NEEDED` dependency into an existing image without moving any section (address-preserving, `patchelf`-style)
5657
- Print with `readelf` similar output
58+
- Good support for the **Mach-O file format**:
59+
- Support byte-to-byte roundtrip
60+
- Read and write from/to a `System.IO.Stream`
61+
- 32 and 64 bit, `i386`, `x86_64` and `arm64`
62+
- Universal (fat) binaries via `MachOFatFile`
63+
- All load commands are decoded, and commands this library does not model round-trip verbatim
64+
- Symbol table, indirect symbol table and relocations
65+
- `MachOFile.AddLoadDylib`, `AddRPath` and `ChangeDylibName` edit an existing image without moving any section (address-preserving, `install_name_tool`-style)
66+
- `MachOFile.AdHocSign` writes an ad-hoc code signature, which Apple Silicon requires in order to execute an image
67+
- `MachOFile.Print` to print the content of a Mach-O file with `otool` similar output
5768
- Support for **DWARF debugging format**:
5869
- Partial support of Version 4 (currently still the default for GCC)
5970
- Support for the sections: `.debug_info`, `.debug_line`, `.debug_aranges`, `.debug_abbrev` and `.debug_str`

0 commit comments

Comments
 (0)