diff --git a/.gitignore b/.gitignore index b90654c..0f3f312 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,6 @@ xcuserdata/ # End of https://www.toptal.com/developers/gitignore/api/xcode build/ -build.xcresult/ \ No newline at end of file +build.xcresult/ +/archive.xcarchive +/.* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..c8cc487 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,272 @@ +# AGENTS.md + +Notes for AI agents working in this repository: **read this page first, then +`README.md`.** It covers how to build, run and verify the project and where the +sharp edges are — not what the app does. + +--- + +## 1. What this repository is + +This is a fork of the upstream **macdependency** project. Features that exist +only here are marked **`*This fork.*`** in `README.md` — **keep that marking up +to date** when you touch a feature, otherwise a reader cannot tell what is +upstream and what is ours. + +Work on the fork. Do not push to the upstream repository. + +--- + +## 2. Layout + +``` +MachO/ # the parsing library (C++, a framework target) +├── MachO.xcodeproj +├── MachO_Prefix.h # library prefix header +├── machoarchitecture.* # one architecture: segments/sections, read by address +├── machoheader.* # MachOHeader::FileType (MH_EXECUTE / MH_DYLIB / …) +├── dylibcommand.* # DylibCommand::DependencyType (Normal / Weak / Delayed / Id) +├── internalfile.* # InternalFile / DiskInternalFile / MemoryInternalFile +├── memoryinternalfile.* # an image that only exists in the dyld shared cache +├── objcmetadata.* # Objective-C metadata parsing +├── objctypedecoder.* # Objective-C type-encoding decoding +├── symboltableentry*.{h,cpp} # SymbolTableEntry::Type / Kind +└── dyldcacheimage.* # images served from the shared cache + +MacDependency/ # the Cocoa document app (Objective-C++, ARC) +├── MacDependency.xcodeproj +├── MacDependency_Prefix.h +├── MyDocument.{h,mm} # the document window; wires table, tree and log together +├── MachOModel.{h,mm} # one row of the dependency tree (fileKind / textColor) +├── SymbolTableController.{h,mm} +├── SymbolTableHeaderView.{h,m} # header chevrons + filter menus (this fork) +├── SymbolTableEntryModel.* +├── SymbolTableEntryTypeFormatter.* # Export / Import labels +├── SymbolTableEntryKindFormatter.* # C / C++ / Swift / ObjC class / … labels +├── ObjCClassDefinitionWindowController.* +├── Base.lproj/*.xib # MyDocument.xib / MainMenu.xib +└── en.lproj/Localizable.strings # UTF-16LE! + +docs/ +└── objc-class-definition-progress.md # the main progress log for the ObjC work +.github/workflows/build.yml # CI: builds MacDependency on macos-latest +images/ +``` + +`MachO.xcodeproj` is a **subproject** of `MacDependency.xcodeproj`, and the app +links the library as `MachO.framework`. There are exactly two schemes: +`MacDependency` and `MachO`. There is no test target — CI only builds the app. + +--- + +## 3. Building + +Run this from the repository root: + +```bash +xcodebuild -project MacDependency/MacDependency.xcodeproj \ + -scheme MacDependency \ + -configuration Debug \ + -derivedDataPath /tmp/macdep-derived \ + MACOSX_DEPLOYMENT_TARGET=12.0 \ + build +``` + +The product lands in +`/tmp/macdep-derived/Build/Products/Debug/MacDependency.app`. + +**Sharp edges:** + +- **You must override the deployment target.** Both projects set + `MACOSX_DEPLOYMENT_TARGET` at *target* level (a recent macOS version) and at + *project* level (a very old one). The SDK on a current machine will not accept + a deployment target below 12.0, so always pass + `MACOSX_DEPLOYMENT_TARGET=12.0` on the command line. +- **`-derivedDataPath` requires `-scheme`.** With only `-target` it fails with + `The flag -scheme, -testprojectPath, or -xctestrun is required`. +- Keep derived data out of the working tree; `/tmp` is easiest. Any dot-directory + at the top level is ignored by `.gitignore` (`/.*`), so build output and local + agent state never get committed. +- Two warnings are pre-existing and not caused by your change: a headermap + warning from the `MachO` project, and a deprecated + `preparedCellAtColumn:row:` in `MyDocument.mm`. +- If you only changed library code under `MachO/` and just want to know whether + it compiles, building the `MachO` scheme alone is much faster. + +--- + +## 4. Code conventions + +### Library side, `MachO/` (plain C++) + +- C++11 / libc++, public types marked with the `EXPORT` macro. +- Forward-declare in headers when you can (e.g. `machoarchitecture.h` only needs + `class SegmentCommand;`). +- Reading a **memory-mapped image** always goes through + `MachOArchitecture::readFromProcess()` (which uses `vm_read_overwrite`). + **Never dereference the address directly** — it is not guaranteed readable. +- A new `.cpp` needs `settings = {COMPILER_FLAGS = "-fno-objc-arc"; }` in + `MachO.xcodeproj`, matching the rest of the library. + +### App side, `MacDependency/` (Objective-C++, ARC) + +- New files are `.mm` (Objective-C++) or `.m` (plain Objective-C); both are ARC. +- A `@property` declares an ivar named `_xxx`; writing `xxx` inside a method body + will not compile — write `self.xxx`. +- **Do not let two objects strongly reference each other under ARC.** The header + view registers itself with the controller in `awakeFromNib`, and the controller + keeps its headers in a `[NSHashTable weakObjectsHashTable]`. + +### Why the two halves are separate targets + +- Both projects enable `GCC_PRECOMPILE_PREFIX_HEADER`. `MachO_Prefix.h` and + `MacDependency_Prefix.h` pull in Cocoa plus the C++ standard headers, and the + sources rely on them without including anything themselves. +- The library is plain C++ and the app is ARC Objective-C++; their compiler flags + are incompatible, so they cannot be compiled as one target. +- App sources use directory-qualified includes such as `#import "MachO/macho.h"`, + so the repository root must be on the include path. That is what + `HEADER_SEARCH_PATHS = ./../` in the project file means (one level up from + `MacDependency/`). + +### Compile traps seen in this codebase + +| Written as | Error | Write instead | +|---|---|---| +| `rectA == rectB` (`NSRect`) | `invalid operands to binary expression` | `NSEqualRects(rectA, rectB)` | +| `filterController` (bare ivar name) | `use of undeclared identifier` | `self.filterController` | +| `[self numberOfColumns]` | `no visible @interface ... declares the selector` | `numberOfColumns` belongs to `NSTableView`, not `NSTableHeaderView` | + +--- + +## 5. Hand-edited resource files + +These are maintained by hand or by script; be careful with them. + +### `Base.lproj/MyDocument.xib` + +- Validate with + `ibtool --compile /tmp/x.nib MacDependency/Base.lproj/MyDocument.xib` — if that + fails, the xib is malformed. +- Header filtering identifies columns by **column identifier**: `Type`, `Kind`, + `Name`. Changing an identifier breaks + `SymbolTableController -hasFilterMenuForTableColumn:`. +- `columnAutoresizingStyle` on the outline view: + - `lastColumnOnly` — the last column takes the slack; + - `firstColumnOnly` — the first column takes the slack. The dependency tree + uses this so `Name (Install Name)` absorbs the width. + +### `en.lproj/Localizable.strings` + +**This file is UTF-16LE.** Appending with `echo >>` or `cat >>` corrupts the +encoding and turns every string in the app into a lookup key. + +```python +# Read and write it as utf-16 with Python, then append the new keys. +import io +path = "MacDependency/en.lproj/Localizable.strings" +with io.open(path, "r", encoding="utf-16") as f: + text = f.read() +# ...edit text... +with io.open(path, "w", encoding="utf-16") as f: + f.write(text) +``` + +### `*.xcodeproj/project.pbxproj` + +A new source file has to be registered by hand in **four** places, using +24-digit hexadecimal IDs of our own making: + +- `PBXBuildFile` section: + `B3C0DE0F1A2B3C4D5E0001xx /* Foo.m in Sources */ = {... fileRef = ...0000xx ...};` +- `PBXFileReference` section: + `B3C0DE0F1A2B3C4D5E0000xx /* Foo.m */ = {...};` +- the owning `PBXGroup`'s `children`, and the target's `PBXSourcesBuildPhase` + `files`. + +(`MachO` uses the `B2C0DE0F...` prefix, `MacDependency` uses `B3C0DE0F...`; do not +reuse a number within one project.) + +--- + +## 6. Documentation and notes + +- **`docs/objc-class-definition-progress.md`** is the main progress log for the + Objective-C class-definition work line. **Update it when you reach a milestone, + before moving on.** A feature belonging to that line goes in there; a new work + line gets its own `docs/*.md`. +- **`README.md`** is user-facing. Keep it in step with the code: + - mark fork-only features with `*This fork.*`; + - the tables (the dependency tree, the symbol table sections) must agree with + the code on column names and values. +- Development-time verification harnesses are **not** part of the repository. + How to rebuild and run them is recorded in the project notes alongside the + working copy, not here. + +--- + +## 7. Runtime traps + +- **The dependency graph has cycles** (Foundation depends on something that + depends on Foundation). Walking `MachOModel.children` **must deduplicate by + filename**, or memory blows up and the process is OOM-killed (exit 137). +- **A symbol's kind can only be told from its name prefix** — there is no + metadata for it. The protocol prefix is `__OBJC_PROTOCOL_$_` (note the **two** + leading underscores). +- **Protocol definition symbols are always `local`**, so with the Type filter set + to Export/Import you will not see `ObjC protocol` rows at all. That is not a + bug. +- **Type and Kind are two independent filters** sharing one + `SymbolTableController`. The header menu and the on-table `Export` / `Import` + buttons are **two views of the same state** and must write back to each other: + - `typeFilterMask` is the single source of truth (bit 0 = Export, bit 1 = Import); + - the header menu is **single-select** (`All types` / `Export` / `Import`), the + button group is a two-segment `selectAny`; + - **the last remaining type must not be switched off** (that would leave an + empty table); `updateTypeFilterControl` rolls it back. +- **Measure column widths against the longest label** with `sizeWithAttributes:` + instead of guessing. For reference: `Shared Cache` 85pt, `Executable File` + 91pt, `ObjC metaclass` 95pt, `Dynamic Library` 98pt, `System Shared Cache` + 133pt, `Link Type` 58pt. + +--- + +## 8. Suggested workflow + +1. Decide which side the change belongs to: `MachO/` (parsing) or + `MacDependency/` (UI, bindings). The two halves compile with different flags, + so mixing them up produces confusing link errors. +2. Run `xcodebuild` to confirm it compiles (the `MachO` scheme is a fast check for + library-only changes). +3. If you touched a xib, an outlet or a binding, syntax-check the xib with + `ibtool` and make sure the app still opens a document without raising. +4. Update the progress doc under `docs/` and `README.md` (see §6). +5. Commit messages are English and imperative, matching the existing history + (e.g. `Fix indent`, `Mark libraries served from the dyld shared cache in a + distinct color`). + +--- + +## 9. Command cheat sheet + +```bash +# Build the app (from the repository root) +xcodebuild -project MacDependency/MacDependency.xcodeproj \ + -scheme MacDependency -configuration Debug -derivedDataPath /tmp/macdep-derived \ + MACOSX_DEPLOYMENT_TARGET=12.0 build + +# Run it +open /tmp/macdep-derived/Build/Products/Debug/MacDependency.app + +# Syntax-check a xib +ibtool --compile /tmp/MyDocument.nib MacDependency/Base.lproj/MyDocument.xib + +# List the schemes +xcodebuild -list -project MacDependency/MacDependency.xcodeproj +``` + +> **Note: the default shell here is fish.** For loops, conditionals or multi-line +> scripts, write a `.sh` file and run it with `bash /tmp/x.sh`, or wrap the +> command in `bash -c '...'`. Heredocs, `$?` and `for ... done` typed straight +> into fish will fail. For searching, use `rg` — the BSD `grep` shipped here does +> not support `\|` alternation and **fails silently**, returning nothing. diff --git a/MacDependency/ArchitectureModel.h b/MacDependency/ArchitectureModel.h index 3a6c995..7e61ffe 100644 --- a/MacDependency/ArchitectureModel.h +++ b/MacDependency/ArchitectureModel.h @@ -8,7 +8,7 @@ #import #import "MachOModel.h" -#include "macho/machoarchitecture.h" +#include "MachO/machoarchitecture.h" #include "MachO/machoheader.h" @class MyDocument; diff --git a/MacDependency/ArchitectureModel.mm b/MacDependency/ArchitectureModel.mm index 8f8ebb2..faaee8c 100644 --- a/MacDependency/ArchitectureModel.mm +++ b/MacDependency/ArchitectureModel.mm @@ -238,7 +238,7 @@ - (void) initSymbols { SymbolTableCommand* command = dynamic_cast (*lcIter); if (command != 0) { for (SymbolTableCommand::SymbolTableEntriesConstIterator it = command->getSymbolTableEntryBegin(); it != command->getSymbolTableEntryEnd(); it++) { - SymbolTableEntryModel* symbolModel = [[SymbolTableEntryModel alloc] initWithEntry:*(it) demangleNamesPtr:[[document symbolTableController]demangleNamesPtr] document:document]; + SymbolTableEntryModel* symbolModel = [[SymbolTableEntryModel alloc] initWithEntry:*(it) demangleNamesPtr:[[document symbolTableController]demangleNamesPtr] document:document architecture:architecture]; [symbolEntries addObject:symbolModel]; } } diff --git a/MacDependency/AutoExpandOutlineView.m b/MacDependency/AutoExpandOutlineView.m index 0d0d1d7..e06de18 100644 --- a/MacDependency/AutoExpandOutlineView.m +++ b/MacDependency/AutoExpandOutlineView.m @@ -11,8 +11,16 @@ @implementation AutoExpandOutlineView +- (void)awakeFromNib { + [super awakeFromNib]; + // The nib does not set a delegate; become our own delegate so that + // -outlineView:willDisplayCell:forTableColumn:item: runs and we can + // push the cell title past the disclosure triangle. + if ([self delegate] == nil) { + [self setDelegate:self]; + } +} -// gets called after binding has changed - (void)reloadData; { [super reloadData]; @@ -23,4 +31,46 @@ - (void)reloadData; [self expandItem:item]; } +#pragma mark - Hierarchy indentation + +// In source-list style the outline column's cell is drawn at a fixed x +// regardless of row level, and -frameOfCellAtColumn:row: is ignored for +// drawing, so the disclosure button lands on top of the leading characters +// of the name. Instead, we keep the cell frame untouched and use the +// cell's built-in -indentationLevel (set in -outlineView:willDisplayCell:: +// forTableColumn:item:) to push the title past the button. The button +// itself is repositioned in -frameOfOutlineCellAtRow: so it indents one +// level per row depth, matching the text. + +static const CGFloat kDisclosureButtonWidth = 16.0; + +- (NSRect)frameOfOutlineCellAtRow:(NSInteger)row { + NSInteger outlineColumnIndex = [[self tableColumns] indexOfObject:[self outlineTableColumn]]; + NSRect columnRect = [self rectOfColumn:outlineColumnIndex]; + CGFloat indent = [self indentationPerLevel]; + NSInteger level = [self levelForRow:row]; + NSRect rect = [super frameOfOutlineCellAtRow:row]; + rect.origin.x = columnRect.origin.x + level * indent; + return rect; +} + +- (void)outlineView:(NSOutlineView *)outlineView willDisplayCell:(id)cell forTableColumn:(NSTableColumn *)tableColumn item:(id)item { + if (tableColumn != [outlineView outlineTableColumn]) { + // Non-outline columns: keep them flush with the row (reset indent). + if ([cell respondsToSelector:@selector(setIndentationLevel:)]) { + [cell setIndentationLevel:0]; + } + return; + } + NSInteger level = [outlineView levelForItem:item]; + CGFloat indent = [outlineView indentationPerLevel]; + // Total left offset = room for the disclosure button at this level + the + // per-level indent. The cell itself stays where AppKit placed it; the + // title is shifted by this much via -setIndentationLevel:. + CGFloat total = kDisclosureButtonWidth + level * indent; + if ([cell respondsToSelector:@selector(setIndentationLevel:)]) { + [cell setIndentationLevel:(NSInteger)total]; + } +} + @end diff --git a/MacDependency/Base.lproj/MainMenu.xib b/MacDependency/Base.lproj/MainMenu.xib index 9303f3e..225c32d 100644 --- a/MacDependency/Base.lproj/MainMenu.xib +++ b/MacDependency/Base.lproj/MainMenu.xib @@ -90,6 +90,22 @@ + + + + + + + + + + + + + + + + diff --git a/MacDependency/Base.lproj/MyDocument.xib b/MacDependency/Base.lproj/MyDocument.xib index 297ce7e..a431346 100644 --- a/MacDependency/Base.lproj/MyDocument.xib +++ b/MacDependency/Base.lproj/MyDocument.xib @@ -8,9 +8,12 @@ + + + @@ -45,25 +48,14 @@ - + - - - - - - - - - - - - + @@ -79,8 +71,8 @@ - - + + @@ -95,6 +87,22 @@ + + + + + + + + + + + + + + + + @@ -114,7 +122,7 @@ - + @@ -185,6 +193,20 @@ + + + + + + + + + + + + + + @@ -372,7 +394,7 @@ - + @@ -394,7 +416,29 @@ - + + + + + + + + + + + + + + + + + + + + + + + @@ -425,25 +469,14 @@ - + + + + - - - - - - - - - - - - - - @@ -533,30 +566,29 @@ - + - - - - - - + + + + + - + - + - + - + @@ -1305,7 +1337,11 @@ - + + + + + @@ -1316,15 +1352,50 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1423,9 +1494,11 @@ + type + kind name symb symbolFilter diff --git a/MacDependency/MacDependency.xcodeproj/project.pbxproj b/MacDependency/MacDependency.xcodeproj/project.pbxproj index fb48979..cf93de1 100644 --- a/MacDependency/MacDependency.xcodeproj/project.pbxproj +++ b/MacDependency/MacDependency.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 46; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -30,6 +30,9 @@ 8ED4567B10516D6200FAC99F /* MyDocumentWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 8ED4567A10516D6200FAC99F /* MyDocumentWindow.m */; }; 8EFDEF7A101007B300E4D54D /* ConversionStdString.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8EFDEF79101007B300E4D54D /* ConversionStdString.mm */; }; 8EFDEF9310100F1700E4D54D /* VersionFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 8EFDEF9210100F1700E4D54D /* VersionFormatter.m */; }; + B3C0DE0F1A2B3C4D5E000102 /* ObjCClassDefinitionWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = B3C0DE0F1A2B3C4D5E000002 /* ObjCClassDefinitionWindowController.m */; }; + B3C0DE0F1A2B3C4D5E000103 /* SymbolTableEntryKindFormatter.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3C0DE0F1A2B3C4D5E000004 /* SymbolTableEntryKindFormatter.mm */; }; + B3C0DE0F1A2B3C4D5E000105 /* SymbolTableHeaderView.m in Sources */ = {isa = PBXBuildFile; fileRef = B3C0DE0F1A2B3C4D5E000006 /* SymbolTableHeaderView.m */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -106,6 +109,12 @@ ADCDF194276D87BF002476ED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = en; path = en.lproj/Credits.rtf; sourceTree = ""; }; ADCDF195276D87BF002476ED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; ADCDF196276D87BF002476ED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + B3C0DE0F1A2B3C4D5E000001 /* ObjCClassDefinitionWindowController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ObjCClassDefinitionWindowController.h; sourceTree = ""; wrapsLines = 0; }; + B3C0DE0F1A2B3C4D5E000002 /* ObjCClassDefinitionWindowController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ObjCClassDefinitionWindowController.m; sourceTree = ""; wrapsLines = 0; }; + B3C0DE0F1A2B3C4D5E000003 /* SymbolTableEntryKindFormatter.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SymbolTableEntryKindFormatter.h; sourceTree = ""; wrapsLines = 0; }; + B3C0DE0F1A2B3C4D5E000004 /* SymbolTableEntryKindFormatter.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = SymbolTableEntryKindFormatter.mm; sourceTree = ""; wrapsLines = 0; }; + B3C0DE0F1A2B3C4D5E000005 /* SymbolTableHeaderView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SymbolTableHeaderView.h; sourceTree = ""; wrapsLines = 0; }; + B3C0DE0F1A2B3C4D5E000006 /* SymbolTableHeaderView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SymbolTableHeaderView.m; sourceTree = ""; wrapsLines = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -181,6 +190,10 @@ 8EFDEF9210100F1700E4D54D /* VersionFormatter.m */, 8E212D82101212FF0078924A /* SymbolTableEntryTypeFormatter.h */, 8E212D83101212FF0078924A /* SymbolTableEntryTypeFormatter.mm */, + B3C0DE0F1A2B3C4D5E000003 /* SymbolTableEntryKindFormatter.h */, + B3C0DE0F1A2B3C4D5E000004 /* SymbolTableEntryKindFormatter.mm */, + B3C0DE0F1A2B3C4D5E000005 /* SymbolTableHeaderView.h */, + B3C0DE0F1A2B3C4D5E000006 /* SymbolTableHeaderView.m */, 8EAEBA2110126AB100E1D5D2 /* SymbolTableController.h */, 8EAEBA2210126AB100E1D5D2 /* SymbolTableController.mm */, 8EB56270104EA241006A44CD /* TreeControllerExtension.h */, @@ -190,6 +203,8 @@ 8EB5640D104FE62F006A44CD /* AutoExpandOutlineView.m */, 8ED4567910516D6200FAC99F /* MyDocumentWindow.h */, 8ED4567A10516D6200FAC99F /* MyDocumentWindow.m */, + B3C0DE0F1A2B3C4D5E000001 /* ObjCClassDefinitionWindowController.h */, + B3C0DE0F1A2B3C4D5E000002 /* ObjCClassDefinitionWindowController.m */, ); name = Classes; sourceTree = ""; @@ -263,7 +278,8 @@ 2A37F4A9FDCFA73011CA2CEA /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1310; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "Konrad Windszus"; }; buildConfigurationList = C05733CB08A9546B00998B17 /* Build configuration list for PBXProject "MacDependency" */; @@ -324,10 +340,13 @@ 8ED32F48100B68FF00EBF623 /* MachOModel.mm in Sources */, 8ED330CE100BD19800EBF623 /* SymbolTableEntryModel.mm in Sources */, 8E1314FA100F7BFC00367510 /* MyDocument.mm in Sources */, + B3C0DE0F1A2B3C4D5E000102 /* ObjCClassDefinitionWindowController.m in Sources */, 8E1314FC100F7C0C00367510 /* main.m in Sources */, 8EFDEF7A101007B300E4D54D /* ConversionStdString.mm in Sources */, 8EFDEF9310100F1700E4D54D /* VersionFormatter.m in Sources */, 8E212D84101212FF0078924A /* SymbolTableEntryTypeFormatter.mm in Sources */, + B3C0DE0F1A2B3C4D5E000103 /* SymbolTableEntryKindFormatter.mm in Sources */, + B3C0DE0F1A2B3C4D5E000105 /* SymbolTableHeaderView.m in Sources */, 8EAEBA2310126AB100E1D5D2 /* SymbolTableController.mm in Sources */, 8EC4F16010400F02001F9FB8 /* ArchitecturesController.mm in Sources */, 8EC4F16A1040126A001F9FB8 /* ArchitectureModel.mm in Sources */, @@ -399,11 +418,12 @@ CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; GCC_DYNAMIC_NO_PIC = NO; GCC_MODEL_TUNING = G5; GCC_OPTIMIZATION_LEVEL = 0; INFOPLIST_FILE = Info.plist; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 14.6; PRODUCT_BUNDLE_IDENTIFIER = "com.googlecode.${PRODUCT_NAME:identifier}"; PRODUCT_NAME = MacDependency; SDKROOT = macosx; @@ -416,10 +436,11 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_OBJC_ARC = YES; CODE_SIGN_IDENTITY = "-"; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; GCC_MODEL_TUNING = G5; INFOPLIST_FILE = Info.plist; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 14.6; PRODUCT_BUNDLE_IDENTIFIER = "com.googlecode.${PRODUCT_NAME:identifier}"; PRODUCT_NAME = MacDependency; SDKROOT = macosx; @@ -430,6 +451,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_CXX_LANGUAGE_STANDARD = "c++0x"; CLANG_CXX_LIBRARY = "libc++"; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; @@ -450,6 +472,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = c99; @@ -478,7 +501,7 @@ /opt/local/include, ./../, ); - MACOSX_DEPLOYMENT_TARGET = 10.7; + MACOSX_DEPLOYMENT_TARGET = 10.13; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; }; @@ -509,6 +532,7 @@ CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + DEAD_CODE_STRIPPING = YES; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = c99; GCC_NO_COMMON_BLOCKS = YES; @@ -521,7 +545,7 @@ /opt/local/include, ./../, ); - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 10.13; SDKROOT = macosx; }; name = Release; diff --git a/MacDependency/MacDependency.xcodeproj/xcshareddata/xcschemes/MacDependency.xcscheme b/MacDependency/MacDependency.xcodeproj/xcshareddata/xcschemes/MacDependency.xcscheme index d7d53df..77183b4 100644 --- a/MacDependency/MacDependency.xcodeproj/xcshareddata/xcschemes/MacDependency.xcscheme +++ b/MacDependency/MacDependency.xcodeproj/xcshareddata/xcschemes/MacDependency.xcscheme @@ -1,6 +1,6 @@ isInMemory(); +} + +/** + * The colour a row is drawn in. + * + * Only the two states that need acting upon have a colour of their own. An + * image that lives in the dyld shared cache used to get one as well, but the + * colour made those rows harder to read than the fact was worth: the File Kind + * column says "System Shared Cache" in words, in a colour that can be read. + */ - (NSColor*) textColor { - NSColor* color; switch(state) { case StateWarning: - color = [NSColor systemOrangeColor]; - break; + return [NSColor systemYellowColor]; case StateError: - color = [NSColor systemOrangeColor]; - break; + return [NSColor systemRedColor]; default: - color = [NSColor labelColor]; + return [NSColor labelColor]; } - return color; } - (NSString*) filename { @@ -259,6 +268,43 @@ - (NSString*) dependencyType { return type; } +- (NSString*) fileKind { + // Nothing was read for this row -- the library is missing, or has no + // architecture in common with its parent. Saying which of the three it is + // would be a guess, and the row already carries the colour and the log line + // that say what went wrong. + if (architecture == nullptr) { + return NSLocalizedString(@"FILE_KIND_UNKNOWN", @"Unknown"); + } + + // An image that was served out of the dyld shared cache is the one case + // where what the file is matters less than where it comes from: there is no + // file on disk to look at, whichever type the header records. + if ([self isInMemory]) { + return NSLocalizedString(@"FILE_KIND_SHARED_CACHE", nil); + } + + switch (architecture->getHeader()->getFileType()) { + case MachOHeader::FileTypeExecutable: + case MachOHeader::FileTypePreload: + return NSLocalizedString(@"FILE_KIND_EXECUTABLE", nil); + // Everything that is loaded into another process is one of these: the + // shared libraries proper, the bundles that are loaded on demand, the + // stubs left for static linking and the dynamic linker itself. + case MachOHeader::FileTypeDylib: + case MachOHeader::FileTypeDylibStub: + case MachOHeader::FileTypeBundle: + case MachOHeader::FileTypeVmLib: + case MachOHeader::FileTypeDylinker: + case MachOHeader::FileTypeKextBundle: + return NSLocalizedString(@"FILE_KIND_DYNAMIC_LIBRARY", nil); + default: + // Object files, core files, dSYM companions: never a dependency of + // anything, but the header can say so and it is not one of the three. + return NSLocalizedString(@"FILE_KIND_UNKNOWN", @"Unknown"); + } +} + - (NSArray*) architectures { NSMutableArray* architectures = [NSMutableArray arrayWithCapacity:4]; diff --git a/MacDependency/MyDocument.h b/MacDependency/MyDocument.h index 9881d4c..6ddc9bd 100644 --- a/MacDependency/MyDocument.h +++ b/MacDependency/MyDocument.h @@ -13,6 +13,12 @@ #import "MachOModel.h" #import "PrioritySplitViewDelegate.h" #import "SymbolTableController.h" +#import "SymbolTableEntryModel.h" + +#include + +class ObjCMetadata; +@class ObjCClassDefinitionWindowController; @interface MyDocument : NSDocument { @@ -26,7 +32,15 @@ IBOutlet NSTextField* textFieldBottomBar; IBOutlet NSSplitView* mainSplitView; IBOutlet SymbolTableController* symbolTableController; + IBOutlet NSTableView* symbolsTableView; + IBOutlet NSTabView* bottomTabView; + IBOutlet NSTextView* definitionTextView; unsigned int numDependencies; + // Parsed Objective-C metadata, one entry per architecture, built on demand: + // reading the class list of a system framework is not free. + std::map* objcMetadata; + // The class definition panels that are currently open. + NSMutableArray* definitionWindows; } - (NSAttributedString*)log; - (void)setLog:(NSAttributedString *)newLog; @@ -43,4 +57,20 @@ - (IBAction)clickRevealInFinder:(id)sender; - (NSString*)dependencyStatus; - (SymbolTableController*)symbolTableController; + +/** + * Shows the declaration of the Objective-C class behind the double clicked + * symbol table row, in a window of its own. A symbol this image does not define + * is provided by one of its dependencies, and the double click takes you to + * that library and to the export it makes of the symbol instead. + */ +- (IBAction)showObjCClassDefinition:(id)sender; + +/** + * Puts the selected row of the symbol table on the pasteboard, written the way + * the table displays it: "Export _AaaBbbCcc". This is what the Edit menu's Copy + * item sends, and it does something only while the symbol table has the focus + * and one of its rows is selected. + */ +- (IBAction)copy:(id)sender; @end diff --git a/MacDependency/MyDocument.mm b/MacDependency/MyDocument.mm index a9f4c52..df0c152 100644 --- a/MacDependency/MyDocument.mm +++ b/MacDependency/MyDocument.mm @@ -11,14 +11,45 @@ #import "MachOModel.h" #import "TreeControllerExtension.h" #import "ArchitectureModel.h" +#import "ObjCClassDefinitionWindowController.h" #include "MachO/machoexception.h" +#include "MachO/objcmetadata.h" +#import +#import +// The symbols that stand for an Objective-C class, and what they mean. +static NSString* const kObjCClassSymbolPrefix = @"_OBJC_CLASS_$_"; +static NSString* const kObjCMetaclassSymbolPrefix = @"_OBJC_METACLASS_$_"; // declare private methods @interface MyDocument () - (NSString*) serializeIndexPath:(NSIndexPath*)indexPath; - (NSIndexPath*) deserializeIndexPath:(NSString*)link; +- (ObjCMetadata*) objcMetadataForArchitecture:(MachOArchitecture*)architecture; +- (NSTableView*) findSymbolsTableView; +- (NSOutlineView*) findDependenciesOutlineView; +- (void) definitionWindowWillClose:(NSNotification*)notification; +- (void) prepareDefinitionTextView; +- (SymbolTableEntryModel*) symbolModelAtRow:(NSInteger)row; +- (BOOL) objcClassName:(NSString**)className metaclass:(BOOL*)metaclass ofSymbol:(NSString*)symbol; +- (NSString*) definitionForClassName:(NSString*)className + metaclass:(BOOL)metaclass + architecture:(MachOArchitecture*)architecture; +- (void) setDefinitionInBottomTab:(NSString*)definition; +- (void) showObjCClassDefinitionWindow:(NSString*)className + metaclass:(BOOL)metaclass + definition:(NSString*)definition; +- (void) symbolSelectionDidChange:(NSNotification*)notification; +- (NSInteger) focusedSymbolRow; +- (NSString*) rowText:(NSInteger)row inTableView:(NSTableView*)tableView; +- (void) revealImportSymbol:(SymbolTableEntryModel*)model; +- (NSArray*) librariesOfSelectedImage; +- (MachOModel*) providerOfImportedSymbol:(SymbolTableEntryModel*)model; +- (MachOModel*) dependencyProvidingSymbolInProcess:(NSString*)symbol; +- (MachOModel*) dependencyWithFileNamed:(NSString*)path; +- (void) selectExportedSymbolNamed:(NSString*)name; +- (NSInteger) rowOfExportedSymbolNamed:(NSString*)name; @end @implementation MyDocument @@ -27,7 +58,7 @@ - (id)init { self = [super init]; if (self) { - + // Add your subclass-specific initialization here. // If an error occurs here, send a [self release] message and return nil. //contents = [[NSMutableArray alloc] init]; @@ -35,13 +66,23 @@ - (id)init log = [[NSAttributedString alloc] initWithString:@""]; numDependencies = 0; splitViewDelegate = [[PrioritySplitViewDelegate alloc] init]; - + objcMetadata = 0; + definitionWindows = [[NSMutableArray alloc] init]; + } return self; } - (void) dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + if (objcMetadata != 0) { + for (std::map::iterator it = objcMetadata->begin(); it != objcMetadata->end(); ++it) + delete it->second; + delete objcMetadata; + objcMetadata = 0; + } delete cache; } @@ -56,19 +97,36 @@ - (NSString *)windowNibName - (void)windowControllerDidLoadNib:(NSWindowController *) aController { [super windowControllerDidLoadNib:aController]; - - // Add any code here that needs to be executed once the windowController has loaded the document's window. + + // Add any code here that needs to be executed once the windowController has loaded the document's window. + + // Double clicking a symbol either opens the class definition of an + // Objective-C class or takes you to the library that provides it; a single + // click is enough for a class definition to show up in the bottom tab. + // The table view is only a plain NSTableView in the nib, so the connections + // are made here rather than in Interface Builder. + NSTableView* tableView = symbolsTableView ? symbolsTableView : [self findSymbolsTableView]; + if (tableView != nil) { + [tableView setTarget:self]; + [tableView setDoubleAction:@selector(showObjCClassDefinition:)]; + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(symbolSelectionDidChange:) + name:NSTableViewSelectionDidChangeNotification + object:tableView]; + } + + [self prepareDefinitionTextView]; } - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError { // Insert code here to write your document to data of the specified type. If the given outError != NULL, ensure that you set *outError when returning nil. - + // You can also choose to override -fileWrapperOfType:error:, -writeToURL:ofType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead. - + // For applications targeted for Panther or earlier systems, you should use the deprecated API -dataRepresentationOfType:. In this case you can also choose to override -fileWrapperRepresentationOfType: or -writeToFile:ofType: instead. - + if ( outError != NULL ) { *outError = [NSError errorWithDomain:NSOSStatusErrorDomain code:unimpErr userInfo:NULL]; } @@ -77,26 +135,26 @@ - (NSData *)dataOfType:(NSString *)typeName error:(NSError **)outError - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSError **)outError -{ +{ // TODO: detect changes on document file (FSEvents) or alternatively make reload possible - + // load file NSString* filePath = [absoluteURL path]; - + // convert to std:string std::string fileString = [filePath stdString]; try { machO = cache->getFile(fileString, NULL); } catch (MachOException& exc) { NSString* msg = [NSString stringWithUTF8String:exc.getCause().c_str()]; - - + + // create and return custom domain error (the localized description key is overwritten by OS, so no point in setting it here) NSArray *objArray = [NSArray arrayWithObjects:@"", msg, @"Try again with another file", nil]; NSArray *keyArray = [NSArray arrayWithObjects:NSLocalizedDescriptionKey, NSLocalizedFailureReasonErrorKey, NSLocalizedRecoverySuggestionErrorKey, nil]; - + NSDictionary *eDict = [NSDictionary dictionaryWithObjects:objArray forKeys:keyArray]; - + // fill outError *outError = [NSError errorWithDomain:@"MachO" code:0 userInfo:eDict]; return NO; @@ -105,8 +163,8 @@ - (BOOL)readFromURL:(NSURL *)absoluteURL ofType:(NSString *)typeName error:(NSEr } -(void)awakeFromNib { - [[textFieldBottomBar cell] setBackgroundStyle:NSBackgroundStyleRaised]; - + [[textFieldBottomBar cell] setBackgroundStyle:NSBackgroundStyleRaised]; + [splitViewDelegate setPriority:1 forViewAtIndex:0]; [splitViewDelegate setPriority:0 forViewAtIndex:1]; [splitViewDelegate setMinimumLength:150 forViewAtIndex:0]; @@ -128,10 +186,10 @@ - (void)clearLog { } - (void)appendLogLine:(NSString *)line withModel:(MachOModel*)model state:(State)state { - + NSMutableAttributedString* newLog = [[NSMutableAttributedString alloc] init]; [newLog appendAttributedString:log]; - + NSString* prefix; switch (state) { case StateError: @@ -141,17 +199,22 @@ - (void)appendLogLine:(NSString *)line withModel:(MachOModel*)model state:(State prefix = NSLocalizedString(@"LOG_PREFIX_WARNING", nil); break; } - + NSString* newLine = [NSString stringWithFormat:@"%@%@\n\n", prefix, line]; - NSDictionary* attributes; - + + // A line that carries no colour of its own is drawn in black, whatever the + // background is, and the log has a dark background once the system is in + // dark mode. The dynamic colours are the ones that follow the appearance. + NSColor* color = model != nil ? [NSColor linkColor] : [NSColor textColor]; + NSMutableDictionary* attributes = [NSMutableDictionary dictionaryWithObject:color + forKey:NSForegroundColorAttributeName]; if (model) { - attributes = [NSDictionary dictionaryWithObjectsAndKeys:model, NSLinkAttributeName, [NSCursor pointingHandCursor], NSCursorAttributeName, NSLocalizedString(@"LOG_LINK_TOOLTIP", nil), NSToolTipAttributeName, nil]; - } else { - attributes = [NSDictionary dictionary]; + [attributes setObject:model forKey:NSLinkAttributeName]; + [attributes setObject:[NSCursor pointingHandCursor] forKey:NSCursorAttributeName]; + [attributes setObject:NSLocalizedString(@"LOG_LINK_TOOLTIP", nil) forKey:NSToolTipAttributeName]; } NSAttributedString* newLogLine = [[NSAttributedString alloc]initWithString:newLine attributes:attributes]; - + [newLog appendAttributedString:newLogLine]; [self setLog:newLog]; } @@ -173,10 +236,10 @@ - (NSString*) serializeIndexPath:(NSIndexPath*)indexPath { - (NSIndexPath*) deserializeIndexPath:(NSString*)link { NSIndexPath* indexPath = nil; - + // tokenize string NSArray* indices = [link componentsSeparatedByString:@";"]; - + // go through tokens NSEnumerator *enumerator = [indices objectEnumerator]; NSString* token = [enumerator nextObject]; @@ -191,10 +254,10 @@ - (NSIndexPath*) deserializeIndexPath:(NSString*)link { } -// delegate method +// delegate method - (BOOL)textView:(NSTextView *)aTextView clickedOnLink:(id)link atIndex:(NSUInteger)charIndex { [dependenciesController setSelectedObject: link]; - + // we need no further processing of the link return YES; } @@ -206,7 +269,7 @@ - (MachOCache*)cache { - (NSArray*) architectures { NSMutableArray* architectures = [NSMutableArray arrayWithCapacity:4]; - + if (machO) { MachOArchitecture* architecture = machO->getHostCompatibleArchitecture(); if (!architecture) { @@ -222,7 +285,7 @@ - (NSArray*) architectures { } else { [architectures addObject:currentArchitecture]; // insert at end } - + } } return architectures; @@ -230,7 +293,7 @@ - (NSArray*) architectures { - (IBAction)clickRevealInFinder:(id)sender { NSString* filename = [textFieldFilename stringValue]; - + [[NSWorkspace sharedWorkspace] selectFile: filename inFileViewerRootedAtPath: @""]; } @@ -257,4 +320,467 @@ - (SymbolTableController*)symbolTableController { return symbolTableController; } +#pragma mark - Objective-C class definitions + +- (ObjCMetadata*) objcMetadataForArchitecture:(MachOArchitecture*)architecture { + if (objcMetadata == 0) + objcMetadata = new std::map(); + + std::map::iterator existing = objcMetadata->find(architecture); + if (existing != objcMetadata->end()) + return existing->second; + + ObjCMetadata* metadata = new ObjCMetadata(*architecture); + (*objcMetadata)[architecture] = metadata; + return metadata; +} + +/** + * The symbol table view of the nib, found by walking the view hierarchy. + * + * Only used when the outlet is not connected -- the dependency tree is an + * NSOutlineView, which is also an NSTableView, so it is skipped explicitly. + */ +- (NSTableView*) findSymbolsTableView { + NSArray* controllers = [self windowControllers]; + if ([controllers count] == 0) + return nil; + NSView* root = [[[controllers objectAtIndex:0] window] contentView]; + if (root == nil) + return nil; + + NSMutableArray* pending = [NSMutableArray arrayWithObject:root]; + while ([pending count] > 0) { + NSView* view = [pending objectAtIndex:0]; + [pending removeObjectAtIndex:0]; + if ([view isKindOfClass:[NSOutlineView class]]) + continue; + if ([view isKindOfClass:[NSTableView class]]) + return (NSTableView*)view; + [pending addObjectsFromArray:[view subviews]]; + } + return nil; +} + +/** The dependency tree of the nib, found by walking the view hierarchy. */ +- (NSOutlineView*) findDependenciesOutlineView { + NSArray* controllers = [self windowControllers]; + if ([controllers count] == 0) + return nil; + NSView* root = [[[controllers objectAtIndex:0] window] contentView]; + if (root == nil) + return nil; + + NSMutableArray* pending = [NSMutableArray arrayWithObject:root]; + while ([pending count] > 0) { + NSView* view = [pending objectAtIndex:0]; + [pending removeObjectAtIndex:0]; + if ([view isKindOfClass:[NSOutlineView class]]) + return (NSOutlineView*)view; + [pending addObjectsFromArray:[view subviews]]; + } + return nil; +} + +- (void) definitionWindowWillClose:(NSNotification*)notification { + NSWindow* window = [notification object]; + [[NSNotificationCenter defaultCenter] removeObserver:self name:NSWindowWillCloseNotification object:window]; + + // Index based, because the array is modified here. + for (NSUInteger n = 0; n < [definitionWindows count]; n++) { + ObjCClassDefinitionWindowController* controller = [definitionWindows objectAtIndex:n]; + if ([controller window] == window) { + [definitionWindows removeObjectAtIndex:n]; + return; + } + } +} + +/** + * Gives the text view of the definition tab the behaviour that makes a class + * declaration readable: monospaced, read only but selectable, and scrolling + * sideways rather than wrapping the long protocol lists. It is the same setup + * the class definition window does, only here it is applied to a view that the + * nib owns. + */ +- (void) prepareDefinitionTextView { + if (definitionTextView == nil) + return; + + [definitionTextView setFont:[NSFont userFixedPitchFontOfSize:12.0]]; + [definitionTextView setEditable:NO]; + [definitionTextView setSelectable:YES]; + [definitionTextView setRichText:NO]; + [definitionTextView setUsesFindPanel:YES]; + [definitionTextView setVerticallyResizable:YES]; + [definitionTextView setHorizontallyResizable:YES]; + [definitionTextView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [[definitionTextView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [[definitionTextView textContainer] setWidthTracksTextView:NO]; +} + +- (SymbolTableEntryModel*) symbolModelAtRow:(NSInteger)row { + if (row < 0) + return nil; + NSArray* symbols = [symbolTableController arrangedObjects]; + if (row >= (NSInteger)[symbols count]) + return nil; + id object = [symbols objectAtIndex:row]; + if (![object isKindOfClass:[SymbolTableEntryModel class]]) + return nil; + return (SymbolTableEntryModel*)object; +} + +/** Recognises the symbols that stand for an Objective-C class. */ +- (BOOL) objcClassName:(NSString**)outClassName metaclass:(BOOL*)outMetaclass ofSymbol:(NSString*)symbol { + NSString* prefix = nil; + BOOL metaclass = NO; + if ([symbol hasPrefix:kObjCClassSymbolPrefix]) { + prefix = kObjCClassSymbolPrefix; + } else if ([symbol hasPrefix:kObjCMetaclassSymbolPrefix]) { + prefix = kObjCMetaclassSymbolPrefix; + metaclass = YES; + } else { + return NO; + } + if (outClassName) + *outClassName = [symbol substringFromIndex:[prefix length]]; + if (outMetaclass) + *outMetaclass = metaclass; + return YES; +} + +- (NSString*) definitionForClassName:(NSString*)className + metaclass:(BOOL)metaclass + architecture:(MachOArchitecture*)architecture { + if (architecture == 0) + return nil; + try { + std::string text = [self objcMetadataForArchitecture:architecture]->getDefinition([className UTF8String], metaclass); + if (!text.empty()) + return [NSString stringWithStdString:text]; + } catch (MachOException& exception) { + [self appendLogLine:[NSString stringWithUTF8String:exception.getCause().c_str()] withModel:nil state:StateError]; + } + return nil; +} + +/** + * Puts a text into the definition view of the bottom tab view. + * + * The tab is deliberately not selected here: a click in the symbol table + * updates what the tab holds and nothing else, so a user who is reading the + * warnings stays where they are. + */ +- (void) setDefinitionInBottomTab:(NSString*)definition { + if (bottomTabView == nil || definitionTextView == nil) + return; + [definitionTextView setString:definition]; + [definitionTextView scrollRangeToVisible:NSMakeRange(0, 0)]; +} + +- (void) showObjCClassDefinitionWindow:(NSString*)className + metaclass:(BOOL)metaclass + definition:(NSString*)definition { + NSString* titleFormat = metaclass ? NSLocalizedString(@"OBJC_METACLASS_WINDOW_TITLE", nil) + : NSLocalizedString(@"OBJC_CLASS_WINDOW_TITLE", nil); + ObjCClassDefinitionWindowController* controller = + [[ObjCClassDefinitionWindowController alloc] initWithTitle:[NSString stringWithFormat:titleFormat, className]]; + [controller setDefinition:definition]; + + // Keep the panel alive while it is open, and let go of it once it closes. + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(definitionWindowWillClose:) + name:NSWindowWillCloseNotification + object:[controller window]]; + [definitionWindows addObject:controller]; + [controller showWindow:self]; +} + +/** + * A single click in the symbol table is enough to put the declaration of the + * class the row stands for into the definition tab. Only what this image + * defines has one; an imported symbol belongs to another library, and there the + * double click is the one that has somewhere to go. + * + * A symbol that is not a class of this image at all -- a function, a string, a + * class of another library -- has no declaration either, and saying so is more + * useful than leaving the previous one standing. + */ +- (void) symbolSelectionDidChange:(NSNotification*)notification { + NSTableView* tableView = [notification object]; + SymbolTableEntryModel* model = [self symbolModelAtRow:[tableView selectedRow]]; + if (model == nil) + return; + + if ([model isImported]) + return; + + NSString* className = nil; + BOOL metaclass = NO; + if (![self objcClassName:&className metaclass:&metaclass ofSymbol:[model rawName]]) { + [self setDefinitionInBottomTab: + [NSString stringWithFormat:NSLocalizedString(@"OBJC_UNSUPPORTED_SYMBOL", nil), [model rawName]]]; + return; + } + + NSString* definition = [self definitionForClassName:className + metaclass:metaclass + architecture:[model architecture]]; + if ([definition length] == 0) + definition = [NSString stringWithFormat:NSLocalizedString(@"OBJC_NO_DEFINITION", nil), className]; + [self setDefinitionInBottomTab:definition]; +} + +- (IBAction)showObjCClassDefinition:(id)sender { + NSTableView* tableView = symbolsTableView ? symbolsTableView : [self findSymbolsTableView]; + if (tableView == nil) + return; + + NSInteger row = [tableView clickedRow]; + if (row < 0) + row = [tableView selectedRow]; + + SymbolTableEntryModel* model = [self symbolModelAtRow:row]; + if (model == nil) + return; + + // A symbol this image does not define is provided by one of its + // dependencies, so the double click goes there instead of showing a + // declaration that is not in this image to begin with. + if ([model isImported]) { + [self revealImportSymbol:model]; + return; + } + + NSString* className = nil; + BOOL metaclass = NO; + if (![self objcClassName:&className metaclass:&metaclass ofSymbol:[model rawName]]) + return; + + NSString* definition = [self definitionForClassName:className + metaclass:metaclass + architecture:[model architecture]]; + if ([definition length] == 0) { + NSBeep(); + [self appendLogLine:[NSString stringWithFormat:NSLocalizedString(@"OBJC_NO_DEFINITION", nil), className] + withModel:nil state:StateWarning]; + return; + } + + [self showObjCClassDefinitionWindow:className metaclass:metaclass definition:definition]; +} + +#pragma mark - Copying the symbol that is selected + +/** + * The row of the symbol table that is selected, and only while the table is + * what has the focus. + * + * The Edit menu's Copy item is the document's to answer, and the document is + * reached whenever nothing closer to the keyboard handles it -- so the symbol + * table is checked for the focus rather than assumed to have it, which is what + * keeps Copy from taking a symbol while the dependency tree is being used. + */ +- (NSInteger) focusedSymbolRow { + NSTableView* tableView = symbolsTableView ? symbolsTableView : [self findSymbolsTableView]; + if (tableView == nil) + return -1; + + NSArray* controllers = [self windowControllers]; + if ([controllers count] == 0) + return -1; + if ([[[controllers objectAtIndex:0] window] firstResponder] != tableView) + return -1; + + return [tableView selectedRow]; +} + +/** The text of a row, written the way the table displays it: type, then name. */ +- (NSString*) rowText:(NSInteger)row inTableView:(NSTableView*)tableView { + NSMutableArray* parts = [NSMutableArray array]; + for (NSInteger column = 0; column < [tableView numberOfColumns]; column++) { + // The cells carry the formatter of their column, so this is the text of + // the row as it stands on screen rather than a second way of writing it. + NSString* text = [[tableView preparedCellAtColumn:column row:row] stringValue]; + if ([text length] > 0) + [parts addObject:text]; + } + return [parts componentsJoinedByString:@" "]; +} + +- (IBAction)copy:(id)sender { + NSTableView* tableView = symbolsTableView ? symbolsTableView : [self findSymbolsTableView]; + NSInteger row = [self focusedSymbolRow]; + if (tableView == nil || row < 0) + return; + + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard declareTypes:[NSArray arrayWithObject:NSPasteboardTypeString] owner:nil]; + [pasteboard setString:[self rowText:row inTableView:tableView] forType:NSPasteboardTypeString]; +} + +/** The Copy item is only of use while the symbol table has something selected. */ +- (BOOL)validateUserInterfaceItem:(id)item { + if ([item action] == @selector(copy:)) + return [self focusedSymbolRow] >= 0; + return [super validateUserInterfaceItem:item]; +} + +#pragma mark - Following an imported symbol to the library that provides it + +/** The dependencies of the image whose symbols the table is listing. */ +- (NSArray*) librariesOfSelectedImage { + NSArray* selected = [dependenciesController selectedObjects]; + if ([selected count] == 0) + return nil; + id image = [selected objectAtIndex:0]; + if (![image isKindOfClass:[MachOModel class]]) + return nil; + return [(MachOModel*)image children]; +} + +/** + * The dependency that provides an imported symbol. + * + * A symbol table entry names the library it is bound to by ordinal, and those + * are the libraries of the load commands in order, which is the order the + * dependency tree lists them in as well. Not every import carries one -- the + * symbols a library only re-exports do not -- so those are looked up in this + * process instead, where the runtime has already resolved them. + */ +- (MachOModel*) providerOfImportedSymbol:(SymbolTableEntryModel*)model { + NSArray* libraries = [self librariesOfSelectedImage]; + unsigned int ordinal = [model libraryOrdinal]; + if (libraries != nil && ordinal >= 1 && ordinal <= [libraries count]) + return [libraries objectAtIndex:ordinal - 1]; + + return [self dependencyProvidingSymbolInProcess:[model rawName]]; +} + +/** + * The dependency that holds a symbol, as this process resolves it. Only a + * library that is loaded can be found this way, which is exactly the case for + * the libraries of a system image. + */ +- (MachOModel*) dependencyProvidingSymbolInProcess:(NSString*)symbol { + void* address = 0; + + NSString* className = nil; + BOOL metaclass = NO; + if ([self objcClassName:&className metaclass:&metaclass ofSymbol:symbol]) { + Class found = metaclass ? objc_getMetaClass([className UTF8String]) + : objc_getClass([className UTF8String]); + address = (__bridge void*)found; + } else if ([symbol hasPrefix:@"_"]) { + // The symbol table holds the linker name; dlsym wants the C one. + address = dlsym(RTLD_DEFAULT, [[symbol substringFromIndex:1] UTF8String]); + } + if (address == 0) + return nil; + + Dl_info info; + if (dladdr(address, &info) == 0 || info.dli_fname == 0) + return nil; + + return [self dependencyWithFileNamed:[NSString stringWithUTF8String:info.dli_fname]]; +} + +/** The dependency that was read from the given file. */ +- (MachOModel*) dependencyWithFileNamed:(NSString*)path { + NSArray* libraries = [self librariesOfSelectedImage]; + if (libraries == nil) + return nil; + + for (NSUInteger n = 0; n < [libraries count]; n++) { + MachOModel* library = [libraries objectAtIndex:n]; + if ([[library filename] isEqualToString:path]) + return library; + } + + // The same library can be named differently by the load command and by + // dyld; the file name is what both agree on. + NSString* name = [path lastPathComponent]; + for (NSUInteger n = 0; n < [libraries count]; n++) { + MachOModel* library = [libraries objectAtIndex:n]; + if ([[[library filename] lastPathComponent] isEqualToString:name]) + return library; + } + return nil; +} + +- (void) revealImportSymbol:(SymbolTableEntryModel*)model { + MachOModel* provider = [self providerOfImportedSymbol:model]; + if (provider == nil) { + NSBeep(); + [self appendLogLine:[NSString stringWithFormat:NSLocalizedString(@"OBJC_NO_PROVIDER", nil), [model rawName]] + withModel:nil state:StateWarning]; + return; + } + + NSOutlineView* outlineView = [self findDependenciesOutlineView]; + + // The library is a child of the image the symbol was imported by, so that + // image has to be open for the library to be reachable. + if (outlineView != nil) { + NSInteger row = [outlineView selectedRow]; + if (row >= 0) + [outlineView expandItem:[outlineView itemAtRow:row]]; + } + + if (![dependenciesController setSelectedObject:provider]) { + NSBeep(); + [self appendLogLine:[NSString stringWithFormat:NSLocalizedString(@"OBJC_NO_PROVIDER", nil), [model rawName]] + withModel:nil state:StateWarning]; + return; + } + + if (outlineView != nil) + [outlineView scrollRowToVisible:[outlineView selectedRow]]; + + // The symbol table follows the selection, but not until the array + // controllers have rearranged themselves, so the export is picked up on the + // next pass through the run loop. + [self performSelector:@selector(selectExportedSymbolNamed:) + withObject:[model rawName] + afterDelay:0.0]; +} + +- (NSInteger) rowOfExportedSymbolNamed:(NSString*)name { + NSArray* symbols = [symbolTableController arrangedObjects]; + for (NSUInteger n = 0; n < [symbols count]; n++) { + id object = [symbols objectAtIndex:n]; + if (![object isKindOfClass:[SymbolTableEntryModel class]]) + continue; + SymbolTableEntryModel* model = (SymbolTableEntryModel*)object; + if ([model isImported]) + continue; + if ([[model rawName] isEqualToString:name]) + return (NSInteger)n; + } + return -1; +} + +/** Selects the row of an export, once the symbol table shows the new library. */ +- (void) selectExportedSymbolNamed:(NSString*)name { + NSTableView* tableView = symbolsTableView ? symbolsTableView : [self findSymbolsTableView]; + if (tableView == nil) + return; + + // Only the exports of the library that was jumped to are of interest here, + // so the type filter is narrowed to them before the row is looked for. + [symbolTableController showExportedSymbols]; + + NSInteger row = [self rowOfExportedSymbolNamed:name]; + if (row < 0) { + NSBeep(); + [self appendLogLine:[NSString stringWithFormat:NSLocalizedString(@"OBJC_NO_EXPORT", nil), name] + withModel:nil state:StateWarning]; + return; + } + + [tableView selectRowIndexes:[NSIndexSet indexSetWithIndex:(NSUInteger)row] byExtendingSelection:NO]; + [tableView scrollRowToVisible:row]; +} + @end diff --git a/MacDependency/ObjCClassDefinitionWindowController.h b/MacDependency/ObjCClassDefinitionWindowController.h new file mode 100644 index 0000000..9ddc9e0 --- /dev/null +++ b/MacDependency/ObjCClassDefinitionWindowController.h @@ -0,0 +1,30 @@ +// +// ObjCClassDefinitionWindowController.h +// MacDependency +// + +#import + +/** + * A panel that shows the Objective-C declaration of one class. + * + * The text is read-only but selectable, so any part of the declaration can be + * copied out of it; a button copies the whole thing at once. + * + * The window is built in code rather than in a nib, because it has no layout + * of its own worth editing and that keeps it out of MyDocument.xib. + */ +@interface ObjCClassDefinitionWindowController : NSWindowController +{ + NSTextView* textView; +} + +/** Creates the panel with the given window title, showing no text yet. */ +- (id)initWithTitle:(NSString*)title; + +/** The declaration to show, as produced by ObjCMetadata::getDefinition(). */ +- (void)setDefinition:(NSString*)definition; + +- (IBAction)copyDefinition:(id)sender; + +@end diff --git a/MacDependency/ObjCClassDefinitionWindowController.m b/MacDependency/ObjCClassDefinitionWindowController.m new file mode 100644 index 0000000..7e71549 --- /dev/null +++ b/MacDependency/ObjCClassDefinitionWindowController.m @@ -0,0 +1,109 @@ +// +// ObjCClassDefinitionWindowController.m +// MacDependency +// + +#import "ObjCClassDefinitionWindowController.h" + +static const CGFloat kInitialWidth = 760.0; +static const CGFloat kInitialHeight = 580.0; +static const CGFloat kMinimumWidth = 360.0; +static const CGFloat kMinimumHeight = 240.0; +static const CGFloat kButtonBarHeight = 40.0; +static const CGFloat kButtonHeight = 24.0; +static const CGFloat kButtonMargin = 12.0; + +@implementation ObjCClassDefinitionWindowController + +- (id)initWithTitle:(NSString*)title { + NSRect frame = NSMakeRect(0.0, 0.0, kInitialWidth, kInitialHeight); + NSUInteger styleMask = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | + NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; + + NSWindow* window = [[NSWindow alloc] initWithContentRect:frame + styleMask:styleMask + backing:NSBackingStoreBuffered + defer:NO]; + [window setTitle:title]; + [window setMinSize:NSMakeSize(kMinimumWidth, kMinimumHeight)]; + // The controller owns the window, so the window must not also try to own + // itself: that would leave the controller with a dangling pointer. + [window setReleasedWhenClosed:NO]; + + self = [super initWithWindow:window]; + if (self) { + NSView* contentView = [window contentView]; + NSRect bounds = [contentView bounds]; + + NSScrollView* scrollView = [[NSScrollView alloc] initWithFrame: + NSMakeRect(0.0, kButtonBarHeight, bounds.size.width, bounds.size.height - kButtonBarHeight)]; + [scrollView setAutoresizingMask:NSViewWidthSizable | NSViewHeightSizable]; + [scrollView setHasVerticalScroller:YES]; + [scrollView setHasHorizontalScroller:YES]; + [scrollView setAutohidesScrollers:YES]; + [scrollView setBorderType:NSNoBorder]; + + NSSize documentSize = [scrollView contentSize]; + textView = [[NSTextView alloc] initWithFrame:NSMakeRect(0.0, 0.0, documentSize.width, documentSize.height)]; + [textView setMinSize:NSMakeSize(0.0, 0.0)]; + [textView setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [textView setVerticallyResizable:YES]; + [textView setHorizontallyResizable:YES]; + [textView setAutoresizingMask:NSViewWidthSizable]; + [textView setFont:[NSFont userFixedPitchFontOfSize:12.0]]; + // Read only, but still selectable -- that is the whole point of this panel. + [textView setEditable:NO]; + [textView setSelectable:YES]; + [textView setRichText:NO]; + [textView setUsesFindPanel:YES]; + [[textView textContainer] setContainerSize:NSMakeSize(FLT_MAX, FLT_MAX)]; + [[textView textContainer] setWidthTracksTextView:NO]; + [scrollView setDocumentView:textView]; + [contentView addSubview:scrollView]; + + NSButton* copyButton = [[NSButton alloc] initWithFrame: + NSMakeRect(kButtonMargin, kButtonMargin - 4.0, 0.0, kButtonHeight)]; + [copyButton setTitle:NSLocalizedString(@"OBJC_COPY_DEFINITION", nil)]; + [copyButton setBezelStyle:NSBezelStyleRounded]; + [copyButton setTarget:self]; + [copyButton setAction:@selector(copyDefinition:)]; + [copyButton setAutoresizingMask:NSViewMaxXMargin]; + [copyButton sizeToFit]; + [contentView addSubview:copyButton]; + + NSButton* closeButton = [[NSButton alloc] initWithFrame: + NSMakeRect(0.0, kButtonMargin - 4.0, 0.0, kButtonHeight)]; + [closeButton setTitle:NSLocalizedString(@"OBJC_CLOSE", nil)]; + [closeButton setBezelStyle:NSBezelStyleRounded]; + [closeButton setTarget:window]; + [closeButton setAction:@selector(performClose:)]; + [closeButton sizeToFit]; + NSRect closeFrame = [closeButton frame]; + closeFrame.origin.x = bounds.size.width - closeFrame.size.width - kButtonMargin; + closeFrame.origin.y = kButtonMargin - 4.0; + [closeButton setFrame:closeFrame]; + [closeButton setAutoresizingMask:NSViewMinXMargin]; + [contentView addSubview:closeButton]; + } + return self; +} + +- (void)showWindow:(id)sender { + [super showWindow:sender]; + + // So that the text can be selected (and copied) right away. + [[self window] makeFirstResponder:textView]; +} + +- (void)setDefinition:(NSString*)definition { + [textView setString:definition ? definition : @""]; + [textView scrollRangeToVisible:NSMakeRange(0, 0)]; +} + +- (IBAction)copyDefinition:(id)sender { + NSPasteboard* pasteboard = [NSPasteboard generalPasteboard]; + [pasteboard declareTypes:[NSArray arrayWithObject:NSPasteboardTypeString] owner:nil]; + [pasteboard setString:[textView string] forType:NSPasteboardTypeString]; +} + +@end diff --git a/MacDependency/SymbolTableController.h b/MacDependency/SymbolTableController.h index f271124..5858d05 100644 --- a/MacDependency/SymbolTableController.h +++ b/MacDependency/SymbolTableController.h @@ -8,19 +8,92 @@ @class MyDocument; +/** + * The symbols of one image, filtered by what is typed into the search field and + * by the two filters that hang off the headers of the table: the type of a + * symbol and its kind. Both are kept here rather than in a control of their + * own, because the header of a column is where they belong -- the column shows + * what the filter is about, and a row that is missing is one the column would + * have shown. + * + * The type has a second control, the Export and Import buttons above the table: + * a filter that is used on almost every visit to the window is worth a click + * that does not have to go through a menu first. The two are views of one + * filter, so whichever is used, the other follows. + */ @interface SymbolTableController : NSArrayController { NSPredicate* nameFilter; IBOutlet NSSegmentedControl* typeFilterControl; IBOutlet NSButton* demangleNamesControl; BOOL demangleNames; IBOutlet MyDocument* document; + + /** One bit per SymbolTableEntry::Type, the ones that are listed. */ + unsigned int typeFilterMask; + /** The kind that is listed, or a tag no kind has while all are. */ + NSInteger kindFilterTag; + /** The headers that show the filters, held weakly. */ + NSHashTable* filterHeaders; } - (void)setNameFilter:(NSPredicate*) nameFilter; - (NSPredicate*)nameFilter; +/** + * Reads the Export and Import buttons into the filter. Turning off the last one + * left on would leave a table with nothing in it, so it is put back: an empty + * table says less than one that still shows something. + */ - (IBAction)typeFilterChanged:(id)sender; +/** + * Narrows the table to the symbols of one type, or to all of them. The tag of + * the menu item is the type, and the one that filters nothing carries a tag no + * type has. + */ +- (IBAction)typeFilterChosen:(id)sender; + +/** + * Narrows the table to the symbols of one kind, or to all of them. The menu is + * filled from the kinds the model knows about, so it cannot fall out of step + * with SymbolTableEntry. + */ +- (IBAction)kindFilterChanged:(id)sender; + +/** + * Turns the type filter into "exports only": the exported symbols of the + * library that was jumped to are what is being looked for, and the imports of + * that library would only be in the way. The kind filter is left alone -- the + * export being looked for carries the same name as the import that was clicked, + * so it has the same kind and cannot be hidden by what is set. + */ +- (void)showExportedSymbols; + +/** + * Whether the header of this column carries a chevron that opens a filter. The + * type column and the kind column do, the name column does not. + */ +- (BOOL)hasFilterMenuForTableColumn:(NSTableColumn*)column; + +/** + * Whether the filter of this column is hiding rows right now, which is what + * makes its chevron stand out. + */ +- (BOOL)hasActiveFilterForTableColumn:(NSTableColumn*)column; + +/** + * What this column's filter offers, with what is in effect ticked. A new menu + * on every call, so what it shows is what the filter is at that moment. + */ +- (NSMenu*)filterMenuForTableColumn:(NSTableColumn*)column; + +/** + * The headers that draw the chevrons of those filters, so that they can be + * asked to draw again once a filter changes. They are held weakly: the table + * owns its header, and a controller has no business keeping one alive. + */ +- (void)addFilterHeaderView:(NSView*)headerView; + - (BOOL)demangleNames; - (BOOL*)demangleNamesPtr; - (void)setDemangleNames:(BOOL)demangle; diff --git a/MacDependency/SymbolTableController.mm b/MacDependency/SymbolTableController.mm index e023faf..82b7552 100644 --- a/MacDependency/SymbolTableController.mm +++ b/MacDependency/SymbolTableController.mm @@ -7,6 +7,8 @@ // #import "SymbolTableController.h" +#import "SymbolTableEntryTypeFormatter.h" +#import "SymbolTableEntryKindFormatter.h" #import "MyDocument.h" #include "MachO/symboltableentry.h" @@ -14,6 +16,13 @@ @interface SymbolTableController() -(void)setFilter; - (NSPredicate*) typeFilter; +- (NSPredicate*) kindFilter; +- (NSMenu*) typeFilterMenu; +- (NSMenu*) kindFilterMenu; +- (NSString*) kindOfColumn:(NSTableColumn*)column; +- (BOOL) listsAllTypes; +- (void) updateTypeFilterControl; +- (void) refreshFilterHeaders; @end @implementation SymbolTableController @@ -21,10 +30,32 @@ @implementation SymbolTableController const int TYPE[] = {SymbolTableEntry::TypeExported, SymbolTableEntry::TypeImported}; +/** The number of types a symbol can be filtered by. */ +static const unsigned int kNumTypes = sizeof(TYPE)/sizeof(*TYPE); + +/** The tag of the menu item that leaves every type of symbol listed. */ +static const NSInteger kAllTypesTag = -1; + +/** The tag of the menu item that leaves every kind of symbol listed. */ +static const NSInteger kAllKindsTag = -1; + +/** + * The columns whose header opens a filter, by the identifier the table columns + * carry. Everything else about the column -- where it sits, how wide it is -- + * is the xib's business and is not repeated here. + */ +static NSString* const kTypeColumnIdentifier = @"Type"; +static NSString* const kKindColumnIdentifier = @"Kind"; + - (id)initWithCoder:(NSCoder *)decoder { self = [super initWithCoder:decoder]; if (self) { demangleNames = true; + // The symbol table opens on what the image offers to others; the + // imports are a click away. + typeFilterMask = 1u << 0; + kindFilterTag = kAllKindsTag; + filterHeaders = [NSHashTable weakObjectsHashTable]; } return self; } @@ -34,17 +65,34 @@ - (void)awakeFromNib { [self setFilter]; } +/** Whether the table lists every type of symbol, i.e. nothing is hidden. */ +- (BOOL) listsAllTypes { + return typeFilterMask == (1u << kNumTypes) - 1; +} + +/** + * Shows the filter on the buttons above the table. They are the second view of + * it, so they follow whatever the menu -- or a jump -- set it to. + */ +- (void) updateTypeFilterControl { + for (unsigned int index=0; index < kNumTypes; index++) { + [typeFilterControl setSelected:(typeFilterMask & (1u << index)) != 0 + forSegment:(NSInteger)index]; + } +} + - (NSPredicate*) typeFilter { NSMutableString* typeFilter = [NSMutableString string]; - for (int segment=0; segment < sizeof(TYPE)/sizeof(*TYPE); segment++) { - if ([typeFilterControl isSelectedForSegment:segment]) { - NSString* condition = [NSString stringWithFormat:@"type=%d", TYPE[segment]]; - if ([typeFilter length] > 0) { - [typeFilter appendString:@" or "]; - } - [typeFilter appendString:condition]; + for (unsigned int index=0; index < kNumTypes; index++) { + if ((typeFilterMask & (1u << index)) == 0) { + continue; } + NSString* condition = [NSString stringWithFormat:@"type=%d", TYPE[index]]; + if ([typeFilter length] > 0) { + [typeFilter appendString:@" or "]; + } + [typeFilter appendString:condition]; } // select nothing if no filter set @@ -67,20 +115,180 @@ - (void)setNameFilter:(NSPredicate*) newNameFilter { [self setFilter]; } +/** + * The kind the table is narrowed to, or nil while every kind is listed. The + * item that filters nothing carries a tag no kind has. + */ +- (NSPredicate*) kindFilter { + if (kindFilterTag < 0) + return nil; + return [NSPredicate predicateWithFormat:@"kind=%d", (int)kindFilterTag]; +} + +/** + * The types a symbol can have, with the ones that are listed ticked. The names + * come from the formatter of the column that shows them. + */ +- (NSMenu*) typeFilterMenu { + NSMenu* menu = [[NSMenu alloc] init]; + + NSMenuItem* allTypes = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"SYMBOL_TYPE_ALL", @"All types") + action:@selector(typeFilterChosen:) + keyEquivalent:@""]; + [allTypes setTarget:self]; + [allTypes setTag:kAllTypesTag]; + [allTypes setState:[self listsAllTypes] ? NSControlStateValueOn : NSControlStateValueOff]; + [menu addItem:allTypes]; + + for (unsigned int index=0; index < kNumTypes; index++) { + NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:[SymbolTableEntryTypeFormatter labelForType:TYPE[index]] + action:@selector(typeFilterChosen:) + keyEquivalent:@""]; + [item setTarget:self]; + [item setTag:(NSInteger)index]; + [item setState:typeFilterMask == (1u << index) ? NSControlStateValueOn : NSControlStateValueOff]; + [menu addItem:item]; + } + + return menu; +} + +/** + * The kinds a symbol can have, behind the one that filters nothing. The kinds + * come from SymbolTableEntry, and their names from the formatter of the column + * that shows them, so a kind that is added there turns up here without anything + * else having to change. + */ +- (NSMenu*) kindFilterMenu { + NSMenu* menu = [[NSMenu alloc] init]; + + NSMenuItem* allKinds = [[NSMenuItem alloc] initWithTitle:NSLocalizedString(@"SYMBOL_KIND_ALL", @"All Kinds") + action:@selector(kindFilterChanged:) + keyEquivalent:@""]; + [allKinds setTarget:self]; + [allKinds setTag:kAllKindsTag]; + [allKinds setState:kindFilterTag < 0 ? NSControlStateValueOn : NSControlStateValueOff]; + [menu addItem:allKinds]; + + for (unsigned int kind=0; kind < SymbolTableEntry::NumKinds; kind++) { + NSMenuItem* item = [[NSMenuItem alloc] initWithTitle:[SymbolTableEntryKindFormatter labelForKind:kind] + action:@selector(kindFilterChanged:) + keyEquivalent:@""]; + [item setTarget:self]; + [item setTag:(NSInteger)kind]; + [item setState:kindFilterTag == (NSInteger)kind ? NSControlStateValueOn : NSControlStateValueOff]; + [menu addItem:item]; + } + + return menu; +} + +- (BOOL)hasFilterMenuForTableColumn:(NSTableColumn*)column { + return [self kindOfColumn:column] != nil; +} + +- (BOOL)hasActiveFilterForTableColumn:(NSTableColumn*)column { + NSString* identifier = [self kindOfColumn:column]; + if ([identifier isEqual:kTypeColumnIdentifier]) + return ![self listsAllTypes]; + if ([identifier isEqual:kKindColumnIdentifier]) + return kindFilterTag >= 0; + return NO; +} + +- (NSMenu*)filterMenuForTableColumn:(NSTableColumn*)column { + NSString* identifier = [self kindOfColumn:column]; + if ([identifier isEqual:kTypeColumnIdentifier]) + return [self typeFilterMenu]; + if ([identifier isEqual:kKindColumnIdentifier]) + return [self kindFilterMenu]; + return nil; +} + +/** + * Which filter a column carries, or nil for a column that carries none. The + * identifier is what says so, which is why the xib gives the two columns one. + */ +- (NSString*) kindOfColumn:(NSTableColumn*)column { + NSString* identifier = [column identifier]; + if ([identifier isEqual:kTypeColumnIdentifier] || [identifier isEqual:kKindColumnIdentifier]) + return identifier; + return nil; +} + +- (void)addFilterHeaderView:(NSView*)headerView { + if (headerView != nil) + [filterHeaders addObject:headerView]; +} + +- (void)refreshFilterHeaders { + for (NSView* headerView in filterHeaders) + [headerView setNeedsDisplay:YES]; +} + - (IBAction)typeFilterChanged:(id)sender { + unsigned int mask = 0; + for (unsigned int index=0; index < kNumTypes; index++) { + if ([typeFilterControl isSelectedForSegment:(NSInteger)index]) + mask |= 1u << index; + } + + // Unticking the last one would empty the table, which is a worse answer + // than the rows that are still there. The button is put back the way it was. + if (mask == 0) { + [self updateTypeFilterControl]; + return; + } + + typeFilterMask = mask; + [self setFilter]; +} + +- (IBAction)typeFilterChosen:(id)sender { + NSInteger tag = [sender tag]; + if (tag < 0) { + typeFilterMask = (1u << kNumTypes) - 1; + } else if ((unsigned int)tag < kNumTypes) { + typeFilterMask = 1u << tag; + } else { + return; + } + [self setFilter]; +} + +- (IBAction)kindFilterChanged:(id)sender { + kindFilterTag = [sender tag]; + [self setFilter]; +} + +- (void)showExportedSymbols { + // TYPE[0] is SymbolTableEntry::TypeExported. + if (typeFilterMask == (1u << 0)) + return; + + typeFilterMask = 1u << 0; [self setFilter]; } -(void)setFilter { - NSPredicate* typeFilter = [self typeFilter]; - NSPredicate* predicate; + NSMutableArray* predicates = [NSMutableArray array]; if (nameFilter) { - predicate = [NSCompoundPredicate andPredicateWithSubpredicates:[NSArray arrayWithObjects:nameFilter, typeFilter, nil]]; - } else { - predicate = typeFilter; + [predicates addObject:nameFilter]; } - //NSLog(@"%@", predicate); - [self setFilterPredicate:predicate]; + // The type filter is always part of it: with no type selected it is the + // predicate that matches nothing, which is what "no type chosen" means here. + [predicates addObject:[self typeFilter]]; + + NSPredicate* kindFilter = [self kindFilter]; + if (kindFilter) { + [predicates addObject:kindFilter]; + } + + //NSLog(@"%@", predicates); + [self setFilterPredicate:[NSCompoundPredicate andPredicateWithSubpredicates:predicates]]; + + [self updateTypeFilterControl]; + [self refreshFilterHeaders]; } - (BOOL)demangleNames { diff --git a/MacDependency/SymbolTableEntryKindFormatter.h b/MacDependency/SymbolTableEntryKindFormatter.h new file mode 100644 index 0000000..7e9b7b4 --- /dev/null +++ b/MacDependency/SymbolTableEntryKindFormatter.h @@ -0,0 +1,25 @@ +// +// SymbolTableEntryKindFormatter.h +// MacDependency +// +// Formatter for the kind of a symbol table entry -- C, C++, Swift, an +// Objective-C class and so on. The model carries the kind as the number of a +// SymbolTableEntry::Kind, and a formatter is what turns it into the text the +// column shows, the way SymbolTableEntryTypeFormatter does for the type. +// + +#import + + +@interface SymbolTableEntryKindFormatter : NSFormatter { + +} + +/** + * The text a kind is shown as. The menu that filters the symbol table by kind + * carries the same labels, so that the column and the filter cannot end up + * naming the same kind in two different ways. + */ ++ (NSString*) labelForKind:(unsigned int)kind; + +@end diff --git a/MacDependency/SymbolTableEntryKindFormatter.mm b/MacDependency/SymbolTableEntryKindFormatter.mm new file mode 100644 index 0000000..578682e --- /dev/null +++ b/MacDependency/SymbolTableEntryKindFormatter.mm @@ -0,0 +1,50 @@ +// +// SymbolTableEntryKindFormatter.mm +// MacDependency +// +// Created by Konrad Windszus on 18.07.09. +// Copyright 2009 Konrad Windszus. All rights reserved. +// + +#import "SymbolTableEntryKindFormatter.h" +#include "MachO/symboltableentry.h" + +@implementation SymbolTableEntryKindFormatter + ++ (NSString*) labelForKind:(unsigned int)kind { + switch (kind) { + case SymbolTableEntry::KindC: + return NSLocalizedString(@"SYMBOL_KIND_C", @"C"); + case SymbolTableEntry::KindCXX: + return NSLocalizedString(@"SYMBOL_KIND_CXX", @"C++"); + case SymbolTableEntry::KindSwift: + return NSLocalizedString(@"SYMBOL_KIND_SWIFT", @"Swift"); + case SymbolTableEntry::KindObjCClass: + return NSLocalizedString(@"SYMBOL_KIND_OBJC_CLASS", @"ObjC class"); + case SymbolTableEntry::KindObjCMetaclass: + return NSLocalizedString(@"SYMBOL_KIND_OBJC_METACLASS", @"ObjC metaclass"); + case SymbolTableEntry::KindObjCProtocol: + return NSLocalizedString(@"SYMBOL_KIND_OBJC_PROTOCOL", @"ObjC protocol"); + case SymbolTableEntry::KindObjCIvar: + return NSLocalizedString(@"SYMBOL_KIND_OBJC_IVAR", @"ObjC ivar"); + case SymbolTableEntry::KindObjCOther: + return NSLocalizedString(@"SYMBOL_KIND_OBJC_OTHER", @"ObjC"); + } + return NSLocalizedString(@"UNKNOWN", @"Unknown"); +} + +// conversion to string +- (NSString*) stringForObjectValue:(id)obj { + // must be a NSNumber + if (![obj isKindOfClass:[NSNumber class]]) { + return nil; + } + + return [SymbolTableEntryKindFormatter labelForKind:[obj unsignedIntValue]]; +} + +// conversion from string (not necessary) +- (BOOL) getObjectValue:(id*)obj forString:(NSString*)string errorDescription:(NSString**)errorString { + return NO; +} +@end diff --git a/MacDependency/SymbolTableEntryModel.h b/MacDependency/SymbolTableEntryModel.h index 51c4f75..150e5cc 100644 --- a/MacDependency/SymbolTableEntryModel.h +++ b/MacDependency/SymbolTableEntryModel.h @@ -8,6 +8,7 @@ #import #include "MachO/machofile.h" +#include "MachO/machoarchitecture.h" #include "MachO/symboltableentry.h" @class MyDocument; @@ -15,9 +16,38 @@ const SymbolTableEntry* entry; const BOOL* demangleNames; MyDocument* document; + MachOArchitecture* architecture; } -- (id) initWithEntry:(const SymbolTableEntry*)entry demangleNamesPtr:(BOOL*)demangleNames document:(MyDocument*)document; +- (id) initWithEntry:(const SymbolTableEntry*)entry demangleNamesPtr:(BOOL*)demangleNames document:(MyDocument*)document architecture:(MachOArchitecture*)architecture; - (NSString*) name; - (NSNumber*) type; + +/** + * What the symbol is -- C, C++, Swift, an Objective-C class and so on -- as + * the number of a SymbolTableEntry::Kind. The column that shows it carries a + * formatter, the way the one for the type does. + */ +- (NSNumber*) kind; + +/** + * The symbol name as the string table holds it, without demangling. + * + * Demangling turns a name into something to read, but the mangled form is what + * identifies the symbol -- the Objective-C class symbols are recognised by it. + */ +- (NSString*) rawName; + +/** YES for a symbol this image imports, i.e. one another image defines. */ +- (BOOL) isImported; + +/** + * The library an imported symbol is bound to, as an ordinal into the load + * commands of the image: 1 is the first library, 2 the second, and so on. + * Zero when the symbol is not bound to one library. + */ +- (unsigned int) libraryOrdinal; + +/** The image this symbol was read from. */ +- (MachOArchitecture*) architecture; @end diff --git a/MacDependency/SymbolTableEntryModel.mm b/MacDependency/SymbolTableEntryModel.mm index ecf9a7e..7337892 100644 --- a/MacDependency/SymbolTableEntryModel.mm +++ b/MacDependency/SymbolTableEntryModel.mm @@ -14,12 +14,13 @@ @implementation SymbolTableEntryModel -- (id) initWithEntry:(const SymbolTableEntry*)anEntry demangleNamesPtr:(BOOL*)demangle document:(MyDocument*)aDocument { +- (id) initWithEntry:(const SymbolTableEntry*)anEntry demangleNamesPtr:(BOOL*)demangle document:(MyDocument*)aDocument architecture:(MachOArchitecture*)anArchitecture { self = [super init]; if (self) { entry = anEntry; self->demangleNames = demangle; document = aDocument; + architecture = anArchitecture; } return self; } @@ -41,4 +42,24 @@ - (NSNumber*) type { return [NSNumber numberWithUnsignedInt:entry->getType()]; } +- (NSNumber*) kind { + return [NSNumber numberWithUnsignedInt:entry->getKind()]; +} + +- (NSString*) rawName { + return [NSString stringWithStdString:entry->getName(false)]; +} + +- (BOOL) isImported { + return entry->getType() == SymbolTableEntry::TypeImported; +} + +- (unsigned int) libraryOrdinal { + return entry->getLibraryOrdinal(); +} + +- (MachOArchitecture*) architecture { + return architecture; +} + @end diff --git a/MacDependency/SymbolTableEntryTypeFormatter.h b/MacDependency/SymbolTableEntryTypeFormatter.h index 81b9686..b4dcf19 100644 --- a/MacDependency/SymbolTableEntryTypeFormatter.h +++ b/MacDependency/SymbolTableEntryTypeFormatter.h @@ -13,4 +13,10 @@ } +/** + * What a type of symbol is called. The column that shows the type and the menu + * that filters by it ask the same place, so the two cannot drift apart. + */ ++ (NSString*) labelForType:(unsigned int)type; + @end diff --git a/MacDependency/SymbolTableEntryTypeFormatter.mm b/MacDependency/SymbolTableEntryTypeFormatter.mm index fa29321..bd5c3ed 100644 --- a/MacDependency/SymbolTableEntryTypeFormatter.mm +++ b/MacDependency/SymbolTableEntryTypeFormatter.mm @@ -10,28 +10,26 @@ #include "MachO/symboltableentry.h" @implementation SymbolTableEntryTypeFormatter + ++ (NSString*) labelForType:(unsigned int)type { + switch (type) { + case SymbolTableEntry::TypeExported: + return NSLocalizedString(@"SYMBOL_TYPE_EXPORT", @"Export"); + case SymbolTableEntry::TypeImported: + return NSLocalizedString(@"SYMBOL_TYPE_IMPORT", @"Import"); + } + return NSLocalizedString(@"UNKNOWN", @"Unknown"); +} + // conversion to string - (NSString*) stringForObjectValue:(id)obj { // must be a NSNumber if (![obj isKindOfClass:[NSNumber class]]) { return nil; } - - // NSNumber contains the version as unsigned int - unsigned int typeNumber = [obj unsignedIntValue]; - NSString* type; - switch(typeNumber) { - case SymbolTableEntry::TypeExported: - type = NSLocalizedString(@"SYMBOL_TYPE_EXPORT", @"Export"); - break; - case SymbolTableEntry::TypeImported: - type = NSLocalizedString(@"SYMBOL_TYPE_IMPORT", @"Import"); - break; - default: - type = NSLocalizedString(@"UNKNOWN", @"Unknown"); - - } - return type; + + // NSNumber contains the type as unsigned int + return [SymbolTableEntryTypeFormatter labelForType:[obj unsignedIntValue]]; } diff --git a/MacDependency/SymbolTableHeaderView.h b/MacDependency/SymbolTableHeaderView.h new file mode 100644 index 0000000..6f2f87b --- /dev/null +++ b/MacDependency/SymbolTableHeaderView.h @@ -0,0 +1,22 @@ +// +// SymbolTableHeaderView.h +// MacDependency +// +// The header of the symbol table. The headers of the columns that filter the +// table -- the type of a symbol and its kind -- carry a magnifying glass and +// open the list of what can be chosen when it is clicked. The rest of the +// header keeps the behaviour of a plain table header, sorting and resizing +// included; the magnifying glass stays clear of the sort indicator AppKit +// draws at the right of a sorted column. +// + +#import + +@class SymbolTableController; + +@interface SymbolTableHeaderView : NSTableHeaderView + +/** The controller that knows which columns filter and what they filter by. */ +@property (nonatomic, strong) IBOutlet SymbolTableController* filterController; + +@end diff --git a/MacDependency/SymbolTableHeaderView.m b/MacDependency/SymbolTableHeaderView.m new file mode 100644 index 0000000..51aded9 --- /dev/null +++ b/MacDependency/SymbolTableHeaderView.m @@ -0,0 +1,140 @@ +// +// SymbolTableHeaderView.m +// MacDependency +// +// Created by Konrad Windszus on 18.07.09. +// Copyright 2009 Konrad Windszus. All rights reserved. +// + +#import "SymbolTableHeaderView.h" +#import "SymbolTableController.h" + +/** + * How wide the clickable area a filter icon sits in is. The icon is centred in + * it, so the area is a little wider than the icon itself. + */ +static const CGFloat kFilterAreaWidth = 20.0; + +/** + * What the right edge of a header is left to AppKit, which draws the sort + * indicator of a sorted column there. Measured: the indicator covers the ten + * points before the last eight, so the last eighteen are enough to keep the two + * apart. The band is left free whether or not the column is sorted, so that the + * filter icon does not move out from under the pointer when the sort changes. + */ +static const CGFloat kSortIndicatorAreaWidth = 18.0; + +/** The square the magnifying glass of a filter is drawn in. */ +static const CGFloat kFilterIconSize = 9.0; + +/** How thick the strokes the magnifying glass is made of are. */ +static const CGFloat kFilterIconLineWidth = 1.5; + +@implementation SymbolTableHeaderView + +/** + * The controller is what knows what a filter is set to, and the controller is + * told about this header once the nib has connected the outlet. It holds the + * header weakly, so registering here does not tie the two lifetimes together. + */ +- (void) awakeFromNib { + [super awakeFromNib]; + [self.filterController addFilterHeaderView:self]; +} + +/** + * The rectangle the filter icon of a column is drawn in and clicked in, or + * NSZeroRect for a column that does not filter anything and for one that is too + * narrow to hold both the icon and the sort indicator beside it. + */ +- (NSRect) filterIconRectOfColumn:(NSInteger)column { + NSTableColumn* tableColumn = [[self tableView] tableColumns][(NSUInteger)column]; + if (![self.filterController hasFilterMenuForTableColumn:tableColumn]) + return NSZeroRect; + + NSRect header = [self headerRectOfColumn:column]; + if (NSWidth(header) < kFilterAreaWidth + kSortIndicatorAreaWidth) + return NSZeroRect; + + return NSMakeRect(NSMaxX(header) - kSortIndicatorAreaWidth - kFilterAreaWidth, NSMinY(header), + kFilterAreaWidth, NSHeight(header)); +} + +- (void) drawRect:(NSRect)dirtyRect { + [super drawRect:dirtyRect]; + + if (self.filterController == nil) + return; + + for (NSInteger column = 0; column < [[self tableView] numberOfColumns]; column++) { + NSRect area = [self filterIconRectOfColumn:column]; + if (NSEqualRects(area, NSZeroRect)) + continue; + + // A filter that is narrowing the table is worth noticing: the rows that + // are missing would otherwise look like rows that were never there. + BOOL active = [self.filterController hasActiveFilterForTableColumn: + [[self tableView] tableColumns][(NSUInteger)column]]; + [self drawFilterIconInRect:area active:active]; + } +} + +/** + * A magnifying glass: a lens with a handle running out of its lower right, + * which is the way round one is held. The view is flipped, so the lower right + * is where both coordinates grow. + */ +- (void) drawFilterIconInRect:(NSRect)area active:(BOOL)active { + NSRect icon = NSMakeRect(NSMidX(area) - kFilterIconSize / 2.0, + NSMidY(area) - kFilterIconSize / 2.0, + kFilterIconSize, kFilterIconSize); + + CGFloat diameter = kFilterIconSize * 0.7; + CGFloat radius = diameter / 2.0 - kFilterIconLineWidth / 2.0; + NSPoint centre = NSMakePoint(NSMinX(icon) + diameter / 2.0, NSMinY(icon) + diameter / 2.0); + CGFloat corner = radius * M_SQRT1_2; + + NSBezierPath* glass = [NSBezierPath bezierPath]; + [glass appendBezierPathWithOvalInRect:NSMakeRect(centre.x - radius, centre.y - radius, + radius * 2.0, radius * 2.0)]; + // The handle leaves the lens at forty five degrees and ends in the corner of + // the icon, which is what makes the glass read as one. + [glass moveToPoint:NSMakePoint(centre.x + corner, centre.y + corner)]; + [glass lineToPoint:NSMakePoint(NSMaxX(icon) - kFilterIconLineWidth / 2.0, + NSMaxY(icon) - kFilterIconLineWidth / 2.0)]; + [glass setLineWidth:kFilterIconLineWidth]; + [glass setLineCapStyle:NSLineCapStyleRound]; + + [(active ? [NSColor controlAccentColor] : [NSColor headerTextColor]) setStroke]; + [glass stroke]; +} + +- (void) mouseDown:(NSEvent*)event { + NSPoint point = [self convertPoint:[event locationInWindow] fromView:nil]; + NSInteger column = [self columnAtPoint:point]; + + NSRect area = column < 0 ? NSZeroRect : [self filterIconRectOfColumn:column]; + if (NSEqualRects(area, NSZeroRect) || !NSPointInRect(point, area)) { + // Not the icon: the header does what it always does, which is where the + // column is sorted from and resized from. + [super mouseDown:event]; + return; + } + + NSMenu* menu = [self.filterController filterMenuForTableColumn: + [[self tableView] tableColumns][(NSUInteger)column]]; + if (menu == nil) { + [super mouseDown:event]; + return; + } + + [menu popUpMenuPositioningItem:nil + atLocation:NSMakePoint(NSMinX(area), NSMinY(area)) + inView:self]; + + // The menu changed what the table lists, and the icon says whether a filter + // is on. + [self setNeedsDisplay:YES]; +} + +@end diff --git a/MacDependency/en.lproj/Localizable.strings b/MacDependency/en.lproj/Localizable.strings index 813857c..c1184e6 100644 Binary files a/MacDependency/en.lproj/Localizable.strings and b/MacDependency/en.lproj/Localizable.strings differ diff --git a/MachO/MachO.xcodeproj/project.pbxproj b/MachO/MachO.xcodeproj/project.pbxproj index c087291..cb22f8c 100644 --- a/MachO/MachO.xcodeproj/project.pbxproj +++ b/MachO/MachO.xcodeproj/project.pbxproj @@ -52,6 +52,22 @@ 8EA3E17B11B12B5A0093CD87 /* dylinkercommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 8EA3E17911B12B5A0093CD87 /* dylinkercommand.h */; }; 8ED32FDE100B99EC00EBF623 /* machocache.h in Headers */ = {isa = PBXBuildFile; fileRef = 8ED32FDC100B99EC00EBF623 /* machocache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8ED32FDF100B99EC00EBF623 /* machocache.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 8ED32FDD100B99EC00EBF623 /* machocache.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + A1C0DE0F1A2B3C4D5E0000A2 /* dyldcacheimage.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE0F1A2B3C4D5E0000A1 /* dyldcacheimage.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + A1C0DE0F1A2B3C4D5E0000A4 /* dyldcacheimage.h in Headers */ = {isa = PBXBuildFile; fileRef = A1C0DE0F1A2B3C4D5E0000A3 /* dyldcacheimage.h */; }; + A1C0DE0F1A2B3C4D5E0000B2 /* diskinternalfile.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE0F1A2B3C4D5E0000B1 /* diskinternalfile.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + A1C0DE0F1A2B3C4D5E0000B4 /* diskinternalfile.h in Headers */ = {isa = PBXBuildFile; fileRef = A1C0DE0F1A2B3C4D5E0000B3 /* diskinternalfile.h */; }; + A1C0DE0F1A2B3C4D5E0000C2 /* memoryinternalfile.cpp in Sources */ = {isa = PBXBuildFile; fileRef = A1C0DE0F1A2B3C4D5E0000C1 /* memoryinternalfile.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + A1C0DE0F1A2B3C4D5E0000C4 /* memoryinternalfile.h in Headers */ = {isa = PBXBuildFile; fileRef = A1C0DE0F1A2B3C4D5E0000C3 /* memoryinternalfile.h */; }; + B2C0DE0F1A2B3C4D5E000101 /* segmentcommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000001 /* segmentcommand.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + B2C0DE0F1A2B3C4D5E000102 /* segmentcommand.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000002 /* segmentcommand.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B2C0DE0F1A2B3C4D5E000103 /* linkeditdatacommand.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000003 /* linkeditdatacommand.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + B2C0DE0F1A2B3C4D5E000104 /* linkeditdatacommand.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000004 /* linkeditdatacommand.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B2C0DE0F1A2B3C4D5E000105 /* machofixups.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000005 /* machofixups.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + B2C0DE0F1A2B3C4D5E000106 /* machofixups.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000006 /* machofixups.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B2C0DE0F1A2B3C4D5E000107 /* objctypedecoder.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000007 /* objctypedecoder.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + B2C0DE0F1A2B3C4D5E000108 /* objctypedecoder.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000008 /* objctypedecoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; + B2C0DE0F1A2B3C4D5E000109 /* objcmetadata.cpp in Sources */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000009 /* objcmetadata.cpp */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; + B2C0DE0F1A2B3C4D5E000110 /* objcmetadata.h in Headers */ = {isa = PBXBuildFile; fileRef = B2C0DE0F1A2B3C4D5E000010 /* objcmetadata.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -106,7 +122,23 @@ 8EA3E17911B12B5A0093CD87 /* dylinkercommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dylinkercommand.h; sourceTree = ""; wrapsLines = 0; }; 8ED32FDC100B99EC00EBF623 /* machocache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = machocache.h; sourceTree = ""; wrapsLines = 0; }; 8ED32FDD100B99EC00EBF623 /* machocache.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = machocache.cpp; sourceTree = ""; wrapsLines = 0; }; + A1C0DE0F1A2B3C4D5E0000A1 /* dyldcacheimage.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = dyldcacheimage.cpp; sourceTree = ""; wrapsLines = 0; }; + A1C0DE0F1A2B3C4D5E0000A3 /* dyldcacheimage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = dyldcacheimage.h; sourceTree = ""; wrapsLines = 0; }; + A1C0DE0F1A2B3C4D5E0000B1 /* diskinternalfile.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = diskinternalfile.cpp; sourceTree = ""; wrapsLines = 0; }; + A1C0DE0F1A2B3C4D5E0000B3 /* diskinternalfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = diskinternalfile.h; sourceTree = ""; wrapsLines = 0; }; + A1C0DE0F1A2B3C4D5E0000C1 /* memoryinternalfile.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = memoryinternalfile.cpp; sourceTree = ""; wrapsLines = 0; }; + A1C0DE0F1A2B3C4D5E0000C3 /* memoryinternalfile.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = memoryinternalfile.h; sourceTree = ""; wrapsLines = 0; }; ADCDF193276D87BF002476ED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; + B2C0DE0F1A2B3C4D5E000001 /* segmentcommand.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = segmentcommand.cpp; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000002 /* segmentcommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = segmentcommand.h; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000003 /* linkeditdatacommand.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = linkeditdatacommand.cpp; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000004 /* linkeditdatacommand.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = linkeditdatacommand.h; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000005 /* machofixups.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = machofixups.cpp; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000006 /* machofixups.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = machofixups.h; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000007 /* objctypedecoder.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = objctypedecoder.cpp; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000008 /* objctypedecoder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = objctypedecoder.h; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000009 /* objcmetadata.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = objcmetadata.cpp; sourceTree = ""; wrapsLines = 0; }; + B2C0DE0F1A2B3C4D5E000010 /* objcmetadata.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = objcmetadata.h; sourceTree = ""; wrapsLines = 0; }; D2F7E79907B2D74100F64583 /* CoreData.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreData.framework; path = /System/Library/Frameworks/CoreData.framework; sourceTree = ""; }; /* End PBXFileReference section */ @@ -206,6 +238,22 @@ 8E131570100F7DB600367510 /* symboltableentry64.h */, 8ED32FDC100B99EC00EBF623 /* machocache.h */, 8ED32FDD100B99EC00EBF623 /* machocache.cpp */, + A1C0DE0F1A2B3C4D5E0000A1 /* dyldcacheimage.cpp */, + A1C0DE0F1A2B3C4D5E0000A3 /* dyldcacheimage.h */, + A1C0DE0F1A2B3C4D5E0000B1 /* diskinternalfile.cpp */, + A1C0DE0F1A2B3C4D5E0000B3 /* diskinternalfile.h */, + A1C0DE0F1A2B3C4D5E0000C1 /* memoryinternalfile.cpp */, + A1C0DE0F1A2B3C4D5E0000C3 /* memoryinternalfile.h */, + B2C0DE0F1A2B3C4D5E000001 /* segmentcommand.cpp */, + B2C0DE0F1A2B3C4D5E000002 /* segmentcommand.h */, + B2C0DE0F1A2B3C4D5E000003 /* linkeditdatacommand.cpp */, + B2C0DE0F1A2B3C4D5E000004 /* linkeditdatacommand.h */, + B2C0DE0F1A2B3C4D5E000005 /* machofixups.cpp */, + B2C0DE0F1A2B3C4D5E000006 /* machofixups.h */, + B2C0DE0F1A2B3C4D5E000007 /* objctypedecoder.cpp */, + B2C0DE0F1A2B3C4D5E000008 /* objctypedecoder.h */, + B2C0DE0F1A2B3C4D5E000009 /* objcmetadata.cpp */, + B2C0DE0F1A2B3C4D5E000010 /* objcmetadata.h */, ); name = Classes; sourceTree = ""; @@ -267,6 +315,14 @@ 8E212E93101237A50078924A /* machodemangleexception.h in Headers */, 8EA3DFF511AFD3790093CD87 /* uuidcommand.h in Headers */, 8EA3E17B11B12B5A0093CD87 /* dylinkercommand.h in Headers */, + A1C0DE0F1A2B3C4D5E0000A4 /* dyldcacheimage.h in Headers */, + A1C0DE0F1A2B3C4D5E0000B4 /* diskinternalfile.h in Headers */, + A1C0DE0F1A2B3C4D5E0000C4 /* memoryinternalfile.h in Headers */, + B2C0DE0F1A2B3C4D5E000102 /* segmentcommand.h in Headers */, + B2C0DE0F1A2B3C4D5E000104 /* linkeditdatacommand.h in Headers */, + B2C0DE0F1A2B3C4D5E000106 /* machofixups.h in Headers */, + B2C0DE0F1A2B3C4D5E000108 /* objctypedecoder.h in Headers */, + B2C0DE0F1A2B3C4D5E000110 /* objcmetadata.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -355,6 +411,14 @@ 8E212E94101237A50078924A /* machodemangleexception.cpp in Sources */, 8EA3DFF411AFD3790093CD87 /* uuidcommand.cpp in Sources */, 8EA3E17A11B12B5A0093CD87 /* dylinkercommand.cpp in Sources */, + A1C0DE0F1A2B3C4D5E0000A2 /* dyldcacheimage.cpp in Sources */, + A1C0DE0F1A2B3C4D5E0000B2 /* diskinternalfile.cpp in Sources */, + A1C0DE0F1A2B3C4D5E0000C2 /* memoryinternalfile.cpp in Sources */, + B2C0DE0F1A2B3C4D5E000101 /* segmentcommand.cpp in Sources */, + B2C0DE0F1A2B3C4D5E000103 /* linkeditdatacommand.cpp in Sources */, + B2C0DE0F1A2B3C4D5E000105 /* machofixups.cpp in Sources */, + B2C0DE0F1A2B3C4D5E000107 /* objctypedecoder.cpp in Sources */, + B2C0DE0F1A2B3C4D5E000109 /* objcmetadata.cpp in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -394,7 +458,7 @@ GCC_WARN_NON_VIRTUAL_DESTRUCTOR = YES; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "@executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 14.6; OTHER_LDFLAGS = "-Wl,-search_paths_first"; PRODUCT_BUNDLE_IDENTIFIER = "com.googlecode.${PRODUCT_NAME:identifier}"; PRODUCT_NAME = MachO; @@ -417,7 +481,7 @@ GCC_PREFIX_HEADER = MachO_Prefix.h; INFOPLIST_FILE = Info.plist; INSTALL_PATH = "@executable_path/../Frameworks"; - MACOSX_DEPLOYMENT_TARGET = 10.9; + MACOSX_DEPLOYMENT_TARGET = 14.6; OTHER_LDFLAGS = "-Wl,-search_paths_first"; PRODUCT_BUNDLE_IDENTIFIER = "com.googlecode.${PRODUCT_NAME:identifier}"; PRODUCT_NAME = MachO; diff --git a/MachO/MachO.xcodeproj/xcshareddata/xcschemes/MachO.xcscheme b/MachO/MachO.xcodeproj/xcshareddata/xcschemes/MachO.xcscheme index f2dcc74..596e580 100644 --- a/MachO/MachO.xcodeproj/xcshareddata/xcschemes/MachO.xcscheme +++ b/MachO/MachO.xcodeproj/xcshareddata/xcschemes/MachO.xcscheme @@ -1,6 +1,6 @@ + +DiskInternalFile::DiskInternalFile(const std::string& filename) : +InternalFile(filename), fileSize(0), lastWriteTime(0) +{ + file.open(filename, std::ios_base::in | std::ios_base::binary); + if (file.fail()) { + throw MachOException("Couldn't open file '" + filename + "'."); + } + + struct stat buffer; + if (stat(filename.c_str(), &buffer) >= 0) { + fileSize = buffer.st_size; + lastWriteTime = buffer.st_mtime; + } +} + +DiskInternalFile::~DiskInternalFile() +{ + if (file.is_open()) + file.close(); +} + +unsigned long long DiskInternalFile::getSize() const { + return fileSize; +} + +bool DiskInternalFile::seek(long long int position) { + file.seekg(position, std::ios_base::beg); + if (file.fail()) { + file.clear(); + return false; + } + return true; +} + +std::streamsize DiskInternalFile::read(char* buffer, std::streamsize size) { + file.read(buffer, size); + if (file.fail()) { + file.clear(); + return file.gcount(); + } + // TODO: handle badbit + return size; +} + +long long int DiskInternalFile::getPosition() { + return file.tellg(); +} + +time_t DiskInternalFile::getLastModificationTime() const { + return lastWriteTime; +} + +bool DiskInternalFile::isInMemory() const { + return false; +} diff --git a/MachO/diskinternalfile.h b/MachO/diskinternalfile.h new file mode 100644 index 0000000..4893b0f --- /dev/null +++ b/MachO/diskinternalfile.h @@ -0,0 +1,30 @@ +#ifndef DISKINTERNALFILE_H +#define DISKINTERNALFILE_H + +#include "internalfile.h" + +#include + +/** An InternalFile backed by a real file on disk. */ +class DiskInternalFile : public InternalFile +{ +public: + explicit DiskInternalFile(const std::string& filename); + virtual ~DiskInternalFile(); + + virtual unsigned long long getSize() const; + virtual bool seek(long long int position); + virtual std::streamsize read(char* buffer, std::streamsize size); + virtual long long int getPosition(); + virtual time_t getLastModificationTime() const; + + /** Always false: the bytes come from a real file on disk. */ + virtual bool isInMemory() const; + +private: + std::ifstream file; + unsigned long long fileSize; + time_t lastWriteTime; +}; + +#endif // DISKINTERNALFILE_H diff --git a/MachO/dyldcacheimage.cpp b/MachO/dyldcacheimage.cpp new file mode 100644 index 0000000..c72c501 --- /dev/null +++ b/MachO/dyldcacheimage.cpp @@ -0,0 +1,467 @@ +/* + * dyldcacheimage.cpp + * MachO + * + * Reads the symbol table of a library that has no file on disk, i.e. a library + * that only exists inside the dyld shared cache ("/usr/lib/libz.1.dylib" and + * friends since macOS 11). + * + * Those libraries cannot be opened with ifstream but they *can* be dlopen()ed, + * and once they are mapped the load commands and the LINKEDIT data are + * reachable in memory. This file copies them into a standalone Mach-O image. + */ + +#include "dyldcacheimage.h" +#include "machoexception.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include + +#ifndef LC_DYLD_EXPORTS_TRIE +#define LC_DYLD_EXPORTS_TRIE 0x80000033u +#endif + +namespace { + +const uint8_t kFlagReexport = 0x08; +const uint8_t kFlagStubAndResolver = 0x10; + +const uint32_t kMaxLoadCommands = 100000u; +const uint32_t kMaxSymbols = 20000000u; +const unsigned long long kMaxHeaderSize = 64ull * 1024ull * 1024ull; + +/** Reads a little endian base 128 encoded integer (dyld's uleb128). */ +uint64_t readUleb(const uint8_t** cursor, const uint8_t* end) +{ + uint64_t result = 0; + unsigned int shift = 0; + while (*cursor < end) { + uint8_t byte = *(*cursor)++; + result |= (uint64_t)(byte & 0x7fu) << shift; + if ((byte & 0x80u) == 0) + break; + shift += 7; + } + return result; +} + +/** Collects every symbol of a dyld export trie. */ +class ExportTrie +{ +public: + typedef std::pair Entry; + + ExportTrie(const uint8_t* start, uint32_t size) + : start_(start), end_(start != 0 ? start + size : 0) {} + + void walk() + { + if (start_ != 0 && end_ > start_) + visit(0, std::string(), 0); + } + + const std::vector& entries() const { return entries_; } + +private: + void visit(uint32_t offset, const std::string& prefix, int depth) + { + if (depth > 64) + return; + + const uint8_t* node = start_ + offset; + if (node >= end_) + return; + + uint64_t terminalSize = readUleb(&node, end_); + if (terminalSize > (uint64_t)(end_ - node)) + return; + const uint8_t* children = node + terminalSize; + + if (terminalSize > 0 && node < end_) { + const uint8_t* cursor = node; + uint64_t flags = readUleb(&cursor, end_); + uint64_t value = 0; + if ((flags & kFlagReexport) != 0) { + // reexport: import ordinal followed by the imported name + readUleb(&cursor, end_); + while (cursor < end_ && *cursor != 0) + cursor++; + cursor++; + } else { + value = readUleb(&cursor, end_); + } + if ((flags & kFlagStubAndResolver) != 0) + readUleb(&cursor, end_); + + if (!prefix.empty()) + entries_.push_back(Entry(prefix, value)); + } + + if (children >= end_) + return; + uint8_t childCount = *children++; + for (uint8_t i = 0; i < childCount; i++) { + if (children >= end_) + return; + const uint8_t* terminator = children; + while (terminator < end_ && *terminator != 0) + terminator++; + if (terminator >= end_) + return; + + std::string next(prefix); + next.append((const char*)children, (size_t)(terminator - children)); + + const uint8_t* cursor = terminator + 1; + uint64_t childOffset = readUleb(&cursor, end_); + visit((uint32_t)childOffset, next, depth + 1); + children = cursor; + } + } + + const uint8_t* start_; + const uint8_t* end_; + std::vector entries_; +}; + +/** "a/b/libSystem.dylib" -> "libSystem.dylib" */ +std::string baseNameOf(const std::string& path) +{ + size_t slash = path.rfind('/'); + return (slash == std::string::npos) ? path : path.substr(slash + 1); +} + +/** "libSystem.B.dylib" -> "libSystem" (drops the ABI / version letter) */ +std::string stemOf(const std::string& baseName) +{ + size_t dot = baseName.find('.'); + return (dot == std::string::npos) ? baseName : baseName.substr(0, dot); +} + +/** + * Locates the loaded image belonging to `path`. Returns -1 when not found. + * dyld resolves aliases such as /usr/lib/libSystem.dylib to the real cache + * image /usr/lib/libSystem.B.dylib, so the file name alone is not always + * enough to find it. + */ +int findImageIndex(const std::string& path) +{ + uint32_t count = _dyld_image_count(); + + for (uint32_t i = 0; i < count; i++) { + const char* name = _dyld_get_image_name(i); + if (name != 0 && path == name) + return (int)i; + } + + std::string baseName = baseNameOf(path); + if (baseName.empty()) + return -1; + std::string requestedStem = stemOf(baseName); + + int stemMatch = -1; + for (uint32_t i = 0; i < count; i++) { + const char* name = _dyld_get_image_name(i); + if (name == 0) + continue; + std::string imageBase = baseNameOf(name); + if (imageBase == baseName) + return (int)i; + if (stemMatch < 0 && stemOf(imageBase) == requestedStem) + stemMatch = (int)i; + } + return stemMatch; +} + +/** Appends `size` zero bytes until the buffer is aligned on `alignment`. */ +void align(std::vector& buffer, size_t alignment) +{ + while ((buffer.size() % alignment) != 0) + buffer.push_back(0); +} + +template +void append(std::vector& buffer, const T& value) +{ + const char* bytes = reinterpret_cast(&value); + buffer.insert(buffer.end(), bytes, bytes + sizeof(T)); +} + +} // namespace + +DyldCacheImage::DyldCacheImage() +{ +} + +DyldCacheImage::~DyldCacheImage() +{ +} + +void DyldCacheImage::load(const std::string& path) +{ + // RTLD_NOLOAD first: if dyld already has the image there is no need to run + // its initializers. Only when it is not loaded yet do we pay for that. + // + // Known residual risk: a handful of system libraries abort or crash inside + // their initializers when they are dlopen()ed by an unrelated process + // (ActionKit, ActionKitUI, WorkflowUI, libcrypto, libssl and the + // TextToSpeechKonaSupport voices -- 24 out of the 3463 images of this + // machine's cache). Accepted for now; if it ever matters, load() should be + // moved into a short lived helper process. + void* handle = dlopen(path.c_str(), RTLD_NOLOAD); + if (handle == 0) + handle = dlopen(path.c_str(), RTLD_LAZY); + if (handle == 0) + throw MachOException("Couldn't open file '" + path + "'."); + + // NOTE: the handle is deliberately never dlclose()d. Closing it would run + // the image's terminators and could unmap memory we still point at, while + // keeping it costs nothing for shared cache images (dyld keeps them mapped + // for the lifetime of the process anyway). + + int index = findImageIndex(path); + if (index < 0) + throw MachOException("Couldn't find loaded image '" + path + "'."); + + const struct mach_header_64* header = + (const struct mach_header_64*)_dyld_get_image_header((uint32_t)index); + intptr_t slide = _dyld_get_image_vmaddr_slide((uint32_t)index); + if (header == 0) + throw MachOException("Invalid image header for '" + path + "'."); + + // Remember where dyld put the image: everything outside the synthesized + // buffer (the Objective-C metadata for instance) is read from there. + mappedBase = (const uint8_t*)header; + mappedSlide = slide; + + if (header->magic != MH_MAGIC_64) + throw MachOException("Only 64 bit images can be read from the dyld shared cache ('" + path + "')."); + + if (header->ncmds == 0 || header->ncmds > kMaxLoadCommands || header->sizeofcmds == 0) + throw MachOException("Invalid load commands in '" + path + "'."); + + unsigned long long headerSize = + (unsigned long long)sizeof(struct mach_header_64) + header->sizeofcmds; + if (headerSize > kMaxHeaderSize) + throw MachOException("Load commands of '" + path + "' are too large."); + + buffer.clear(); + const char* mappedHeader = reinterpret_cast(header); + buffer.insert(buffer.end(), mappedHeader, mappedHeader + headerSize); + + // ---- scan the load commands ------------------------------------------- + // Only offsets are kept: `buffer` is grown later on, which would invalidate + // any pointer into it. + size_t symtabCommandOffset = 0; + uint32_t symbolOffset = 0; + uint32_t numberOfSymbols = 0; + uint32_t stringOffset = 0; + uint32_t stringTableSize = 0; + uint64_t linkeditVmAddress = 0; + uint64_t linkeditVmSize = 0; + uint64_t linkeditFileOffset = 0; + uint32_t exportsTrieOffset = 0; + uint32_t exportsTrieSize = 0; + + size_t offset = sizeof(struct mach_header_64); + for (uint32_t i = 0; i < header->ncmds; i++) { + if (offset + sizeof(struct load_command) > (size_t)headerSize) + throw MachOException("Truncated load commands in '" + path + "'."); + + const struct load_command* command = + reinterpret_cast(&buffer[offset]); + uint32_t cmd = command->cmd; + uint32_t cmdSize = command->cmdsize; + + if (cmdSize < sizeof(struct load_command) || offset + cmdSize > (size_t)headerSize) + throw MachOException("Invalid load command size in '" + path + "'."); + + if (cmd == LC_SEGMENT_64) { + const struct segment_command_64* segment = + reinterpret_cast(command); + if (cmdSize >= sizeof(struct segment_command_64) && + strncmp(segment->segname, "__LINKEDIT", sizeof(segment->segname)) == 0) { + linkeditVmAddress = segment->vmaddr; + linkeditVmSize = segment->vmsize; + linkeditFileOffset = segment->fileoff; + } + } else if (cmd == LC_SYMTAB) { + const struct symtab_command* command2 = + reinterpret_cast(command); + symtabCommandOffset = offset; + symbolOffset = command2->symoff; + numberOfSymbols = command2->nsyms; + stringOffset = command2->stroff; + stringTableSize = command2->strsize; + } else if (cmd == LC_DYLD_EXPORTS_TRIE) { + const struct linkedit_data_command* command2 = + reinterpret_cast(command); + exportsTrieOffset = command2->dataoff; + exportsTrieSize = command2->datasize; + } + + offset += cmdSize; + } + + if (symtabCommandOffset == 0) + throw MachOException("Image '" + path + "' has no symbol table."); + if (linkeditVmSize == 0) + throw MachOException("Image '" + path + "' has no __LINKEDIT segment."); + + // Offsets of a mapped image are file offsets relative to the beginning of + // the file the image was loaded from -- for shared cache images that is the + // cache itself, hence __LINKEDIT is shared by all images and only symoff / + // stroff differ. The same formula works for ordinary mapped dylibs. + const uint8_t* linkeditBase = reinterpret_cast(linkeditVmAddress + slide); + const uint8_t* linkeditEnd = linkeditBase + linkeditVmSize; + + // An empty symbol table is not an error: some cached images (several + // libswift* dylibs for instance) keep LC_SYMTAB only as a placeholder and + // carry their exports exclusively in the export trie. + if (numberOfSymbols > kMaxSymbols) + throw MachOException("Image '" + path + "' has a symbol table of implausible size."); + if (symbolOffset < linkeditFileOffset || stringOffset < linkeditFileOffset) + throw MachOException("Symbol table of '" + path + "' lies outside __LINKEDIT."); + + const uint8_t* symbolTable = linkeditBase + (symbolOffset - linkeditFileOffset); + const uint8_t* stringTable = linkeditBase + (stringOffset - linkeditFileOffset); + if (symbolTable + (uint64_t)numberOfSymbols * sizeof(struct nlist_64) > linkeditEnd || + stringTable >= linkeditEnd) + throw MachOException("Symbol table of '" + path + "' lies outside __LINKEDIT."); + + const struct nlist_64* symbols = reinterpret_cast(symbolTable); + + // ---- build a compact string table -------------------------------------- + // The shared cache uses one gigantic string table for all of its images + // (hundreds of megabytes), so only the strings this image really references + // are copied and every n_strx is rewritten. + std::vector strings; + strings.push_back('\0'); // index 0 stays the empty string + std::vector newStringIndex(numberOfSymbols, 0); + std::unordered_map definedSymbols; + + for (uint32_t i = 0; i < numberOfSymbols; i++) { + uint32_t stringIndex = symbols[i].n_un.n_strx; + if (stringIndex == 0 || stringIndex >= stringTableSize) { + newStringIndex[i] = 0; + continue; + } + + if (stringTable + stringIndex >= linkeditEnd) { + newStringIndex[i] = 0; + continue; + } + + const char* name = reinterpret_cast(stringTable) + stringIndex; + size_t maxLength = (size_t)(stringTableSize - stringIndex); + size_t remaining = (size_t)(linkeditEnd - (stringTable + stringIndex)); + if (remaining < maxLength) + maxLength = remaining; + + size_t length = 0; + while (length < maxLength && name[length] != '\0') + length++; + + uint32_t offsetInNewTable = (uint32_t)strings.size(); + strings.insert(strings.end(), name, name + length + 1); + newStringIndex[i] = offsetInNewTable; + + uint8_t type = symbols[i].n_type; + if ((type & N_STAB) == 0 && (type & N_TYPE) != N_UNDF) { + definedSymbols.insert(std::make_pair(std::string(name, length), (size_t)i)); + } + } + + // ---- exports ------------------------------------------------------------ + // The export trie is authoritative. Symbols it exports but which the symbol + // table does not mark as external are upgraded in place; symbols missing + // from the symbol table altogether are appended. + struct AdditionalSymbol { + uint32_t stringIndex; + uint64_t value; + }; + std::vector additionalSymbols; + std::vector upgradedTypes(numberOfSymbols, 0); + bool hasUpgrades = false; + + if (exportsTrieSize > 0 && exportsTrieOffset >= linkeditFileOffset && + (uint64_t)exportsTrieOffset - linkeditFileOffset + exportsTrieSize <= linkeditVmSize) { + const uint8_t* trie = linkeditBase + (exportsTrieOffset - linkeditFileOffset); + if (trie + exportsTrieSize <= linkeditEnd) { + ExportTrie walker(trie, exportsTrieSize); + walker.walk(); + + uint64_t imageBase = (uint64_t)header - (uint64_t)slide; + + const std::vector& exports = walker.entries(); + for (size_t i = 0; i < exports.size(); i++) { + const std::string& name = exports[i].first; + + std::unordered_map::const_iterator found = + definedSymbols.find(name); + if (found != definedSymbols.end()) { + uint8_t& type = upgradedTypes[found->second]; + type = symbols[found->second].n_type; + type |= N_EXT; + type &= ~N_PEXT; + hasUpgrades = true; + continue; + } + + uint32_t stringOffset = (uint32_t)strings.size(); + strings.insert(strings.end(), name.begin(), name.end()); + strings.push_back('\0'); + + AdditionalSymbol symbol; + symbol.stringIndex = stringOffset; + symbol.value = exports[i].second != 0 ? imageBase + exports[i].second : 0; + additionalSymbols.push_back(symbol); + } + } + } + + // ---- assemble ---------------------------------------------------------- + align(buffer, 8); + uint32_t newSymbolOffset = (uint32_t)buffer.size(); + + for (uint32_t i = 0; i < numberOfSymbols; i++) { + struct nlist_64 entry = symbols[i]; + entry.n_un.n_strx = newStringIndex[i]; + if (hasUpgrades && upgradedTypes[i] != 0) + entry.n_type = upgradedTypes[i]; + append(buffer, entry); + } + + for (size_t i = 0; i < additionalSymbols.size(); i++) { + struct nlist_64 entry; + memset(&entry, 0, sizeof(entry)); + entry.n_un.n_strx = additionalSymbols[i].stringIndex; + entry.n_type = N_SECT | N_EXT; + entry.n_sect = 1; + entry.n_desc = 0; + entry.n_value = additionalSymbols[i].value; + append(buffer, entry); + } + + uint32_t newStringOffset = (uint32_t)buffer.size(); + buffer.insert(buffer.end(), strings.begin(), strings.end()); + + // ---- patch the copied LC_SYMTAB ---------------------------------------- + struct symtab_command* patched = + reinterpret_cast(&buffer[symtabCommandOffset]); + patched->symoff = newSymbolOffset; + patched->nsyms = numberOfSymbols + (uint32_t)additionalSymbols.size(); + patched->stroff = newStringOffset; + patched->strsize = (uint32_t)strings.size(); +} diff --git a/MachO/dyldcacheimage.h b/MachO/dyldcacheimage.h new file mode 100644 index 0000000..49cdd38 --- /dev/null +++ b/MachO/dyldcacheimage.h @@ -0,0 +1,58 @@ +#ifndef DYLD_CACHE_IMAGE_H +#define DYLD_CACHE_IMAGE_H + +#include "macho_global.h" + +#include +#include + +/** + * A library that has no representation on disk (it only lives inside the dyld + * shared cache) can still be inspected: dyld maps it into this process as soon + * as it is dlopen()ed, and the mapped image carries the very same load commands + * and LINKEDIT data that an on-disk Mach-O file would have. + * + * This class dlopen()s such a library, then copies the pieces MacDependency + * needs -- header, load commands, symbol table and string table -- into one + * contiguous buffer and rewrites the LINKEDIT offsets so that the result is a + * self contained Mach-O file image which can be parsed by the regular + * MachOFile / MachOArchitecture code. + * + * Exports are taken from LC_DYLD_EXPORTS_TRIE (which is authoritative -- the + * symbol table of some cached images, AppKit for instance, marks far fewer + * symbols as external than the image actually exports). Imports are the + * undefined entries of LC_SYMTAB. + */ +class EXPORT DyldCacheImage +{ +public: + DyldCacheImage(); + ~DyldCacheImage(); + + /** Maps `path` and builds the in-memory file image. Throws MachOException. */ + void load(const std::string& path); + + /** Owning buffer with the synthesized Mach-O file image. */ + const char* getData() const { return buffer.empty() ? 0 : &buffer[0]; } + unsigned long long getSize() const { return (unsigned long long)buffer.size(); } + + /** + * The image as dyld mapped it into this process: the address of its mach + * header and the slide that was applied to it. The buffer above only holds + * the header, the load commands and __LINKEDIT, so anything else the image + * contains -- the Objective-C metadata for instance -- has to be read from + * the mapped memory through these. + * + * The mapping stays valid for the lifetime of the process (the image is + * never dlclose()d), so the pointers can be used after this object is gone. + */ + const uint8_t* getMappedBase() const { return mappedBase; } + intptr_t getMappedSlide() const { return mappedSlide; } + +private: + std::vector buffer; + const uint8_t* mappedBase = 0; + intptr_t mappedSlide = 0; +}; + +#endif // DYLD_CACHE_IMAGE_H diff --git a/MachO/internalfile.cpp b/MachO/internalfile.cpp index c138410..de40d1f 100644 --- a/MachO/internalfile.cpp +++ b/MachO/internalfile.cpp @@ -1,6 +1,12 @@ #include "internalfile.h" +#include "diskinternalfile.h" +#include "memoryinternalfile.h" +#include "dyldcacheimage.h" #include "machoexception.h" +#include +#include + // use reference counting to reuse files for all used architectures InternalFile* InternalFile::create(InternalFile* file) { file->counter++; @@ -8,7 +14,18 @@ InternalFile* InternalFile::create(InternalFile* file) { } InternalFile* InternalFile::create(const std::string& filename) { - return new InternalFile(filename); + try { + return new DiskInternalFile(filename); + } catch (MachOException&) { + // no readable file on disk -- maybe the library only exists inside the + // dyld shared cache + } + + // Throws a MachOException when the library cannot be mapped either. + DyldCacheImage image; + image.load(filename); + return new MemoryInternalFile(filename, image.getData(), image.getSize(), + image.getMappedBase(), image.getMappedSlide()); } void InternalFile::release() { @@ -19,26 +36,12 @@ void InternalFile::release() { } InternalFile::InternalFile(const std::string& filename) : -filename(filename), counter(1) +counter(1), filename(filename) { - // open file handle - file.open(this->filename, std::ios_base::in | std::ios_base::binary); - if (file.fail()) { - std::ostringstream error; - error << "Couldn't open file '" << filename << "'."; - throw MachOException(error.str()); - } - - struct stat buffer; - if (stat(filename.c_str(), &buffer) >= 0) { - _fileSize = buffer.st_size; - _lastWriteTime = buffer.st_mtime; - } } -// destructor is private since we use reference counting mechanism -InternalFile::~InternalFile() { - file.close(); +InternalFile::~InternalFile() +{ } /* returns whole filename (including path)*/ @@ -55,34 +58,3 @@ std::string InternalFile::getName() const { std::string InternalFile::getTitle() const { return filename; } - -unsigned long long InternalFile::getSize() const { - return _fileSize; -} - -bool InternalFile::seek(long long int position) { - file.seekg(position, std::ios_base::beg); - if (file.fail()) { - file.clear(); - return false; - } - return true; -} - -std::streamsize InternalFile::read(char* buffer, std::streamsize size) { - file.read(buffer, size); - if (file.fail()) { - file.clear(); - return file.gcount(); - } - // TODO: handle badbit - return size; -} - -long long int InternalFile::getPosition() { - return file.tellg(); -} - -time_t InternalFile::getLastModificationTime() const { - return _lastWriteTime; -} diff --git a/MachO/internalfile.h b/MachO/internalfile.h index f70ac51..cc4d71e 100644 --- a/MachO/internalfile.h +++ b/MachO/internalfile.h @@ -3,6 +3,17 @@ #include "macho_global.h" +#include +#include + +/** + * Abstract source of raw bytes for the Mach-O parsers. + * + * Normally the bytes come from a file on disk (DiskInternalFile), but libraries + * that only exist inside the dyld shared cache have no file to open. For those + * MemoryInternalFile serves a Mach-O image that DyldCacheImage rebuilt from the + * mapped -- already loaded -- image. + */ class InternalFile { @@ -13,23 +24,38 @@ class InternalFile static InternalFile* create(const std::string& filename); void release(); - std::string getFolder() const; std::string getName() const; std::string getTitle() const; - unsigned long long getSize() const; - bool seek(long long int position); - std::streamsize read(char* buffer, std::streamsize size); - long long int getPosition(); - time_t getLastModificationTime() const; + + virtual unsigned long long getSize() const = 0; + virtual bool seek(long long int position) = 0; + virtual std::streamsize read(char* buffer, std::streamsize size) = 0; + virtual long long int getPosition() = 0; + virtual time_t getLastModificationTime() const = 0; + + /** + * True when the bytes are not backed by a file on disk. This is the case for + * libraries that only exist inside the dyld shared cache: their image is + * rebuilt in memory by DyldCacheImage. The UI marks those entries specially, + * because there is nothing on disk to inspect or reveal in the Finder. + */ + virtual bool isInMemory() const = 0; + + /** + * When the bytes were taken from an image that dyld mapped into this + * process, the address of that image and the slide applied to it. Zero + * otherwise. The mapped image holds everything the synthesized file image + * does not, so parts of the image that are not in the file can still be read. + */ + virtual const uint8_t* getMappedBase() const { return 0; } + virtual intptr_t getMappedSlide() const { return 0; } + +protected: + explicit InternalFile(const std::string& filename); private: unsigned int counter; - - InternalFile(const std::string& filename); - std::ifstream file; std::string filename; - size_t _fileSize; - time_t _lastWriteTime; }; #endif // INTERNALFILE_H diff --git a/MachO/linkeditdatacommand.cpp b/MachO/linkeditdatacommand.cpp new file mode 100644 index 0000000..3120637 --- /dev/null +++ b/MachO/linkeditdatacommand.cpp @@ -0,0 +1,33 @@ +#include "linkeditdatacommand.h" +#include "machoheader.h" +#include "machofile.h" + +#include + +LinkeditDataCommand::LinkeditDataCommand(MachOHeader* header) : + LoadCommand(header), command(0), dataOffset(0), dataSize(0), bindOffset(0), bindSize(0), + lazyBindOffset(0), lazyBindSize(0), commandSize(0) +{ + uint32_t cmd = file.readUint32(); + command = cmd; + file.seek(offset); + + if (cmd == LC_DYLD_INFO || cmd == LC_DYLD_INFO_ONLY) { + struct dyld_info_command command; + file.readBytes((char*)&command, sizeof(command)); + commandSize = file.getUint32(command.cmdsize); + // The generic payload of a dyld_info_command is its rebase stream. + dataOffset = file.getUint32(command.rebase_off); + dataSize = file.getUint32(command.rebase_size); + bindOffset = file.getUint32(command.bind_off); + bindSize = file.getUint32(command.bind_size); + lazyBindOffset = file.getUint32(command.lazy_bind_off); + lazyBindSize = file.getUint32(command.lazy_bind_size); + } else { + struct linkedit_data_command command; + file.readBytes((char*)&command, sizeof(command)); + commandSize = file.getUint32(command.cmdsize); + dataOffset = file.getUint32(command.dataoff); + dataSize = file.getUint32(command.datasize); + } +} diff --git a/MachO/linkeditdatacommand.h b/MachO/linkeditdatacommand.h new file mode 100644 index 0000000..7ddd44e --- /dev/null +++ b/MachO/linkeditdatacommand.h @@ -0,0 +1,46 @@ +#ifndef LINKEDITDATACOMMAND_H +#define LINKEDITDATACOMMAND_H + +#include "loadcommand.h" + +/** + * Load commands that only point at a blob in __LINKEDIT, i.e. everything that + * is laid out as a struct linkedit_data_command: LC_DYLD_CHAINED_FIXUPS, + * LC_DYLD_EXPORTS_TRIE, LC_CODE_SIGNATURE and friends. + * + * LC_DYLD_INFO(_ONLY) shares the first two fields but carries the offsets of + * the classic rebase / bind opcode streams behind them, so those are exposed + * as well. + */ +class EXPORT LinkeditDataCommand : public LoadCommand +{ +public: + LinkeditDataCommand(MachOHeader* header); + + virtual unsigned int getSize() const { return commandSize; } + + /** The LC_* value this command was created for. */ + uint32_t getCommand() const { return command; } + + /** Offset of the payload relative to the beginning of the Mach-O image. */ + uint32_t getDataOffset() const { return dataOffset; } + uint32_t getDataSize() const { return dataSize; } + + /** LC_DYLD_INFO(_ONLY): the classic bind opcode streams. */ + uint32_t getBindOffset() const { return bindOffset; } + uint32_t getBindSize() const { return bindSize; } + uint32_t getLazyBindOffset() const { return lazyBindOffset; } + uint32_t getLazyBindSize() const { return lazyBindSize; } + +private: + uint32_t command; + uint32_t dataOffset; + uint32_t dataSize; + uint32_t bindOffset; + uint32_t bindSize; + uint32_t lazyBindOffset; + uint32_t lazyBindSize; + unsigned int commandSize; +}; + +#endif // LINKEDITDATACOMMAND_H diff --git a/MachO/loadcommand.cpp b/MachO/loadcommand.cpp index c35f017..77b058b 100644 --- a/MachO/loadcommand.cpp +++ b/MachO/loadcommand.cpp @@ -1,6 +1,8 @@ #include "loadcommand.h" #include "dylibcommand.h" #include "genericcommand.h" +#include "segmentcommand.h" +#include "linkeditdatacommand.h" #include "symboltablecommand.h" #include "rpathcommand.h" #include "uuidcommand.h" @@ -8,11 +10,28 @@ #include "machoexception.h" #include "machoheader.h" +#ifndef LC_DYLD_CHAINED_FIXUPS +#define LC_DYLD_CHAINED_FIXUPS 0x80000034 +#endif +#ifndef LC_DYLD_EXPORTS_TRIE +#define LC_DYLD_EXPORTS_TRIE 0x80000033 +#endif + LoadCommand* LoadCommand::getLoadCommand(unsigned int cmd, MachOHeader* header) { LoadCommand* loadCommand; switch(cmd) { + case LC_SEGMENT: + case LC_SEGMENT_64: + loadCommand = new SegmentCommand(header); + break; + case LC_DYLD_INFO: + case LC_DYLD_INFO_ONLY: + case LC_DYLD_CHAINED_FIXUPS: + case LC_DYLD_EXPORTS_TRIE: + loadCommand = new LinkeditDataCommand(header); + break; case LC_LOAD_DYLINKER: loadCommand = new DylinkerCommand(header); break; diff --git a/MachO/macho.cpp b/MachO/macho.cpp index 86c7541..358a7e6 100644 --- a/MachO/macho.cpp +++ b/MachO/macho.cpp @@ -149,6 +149,12 @@ time_t MachO::getLastModificationTime() const { return file->getLastModificationTime(); } +// True when this Mach-O has no file on disk, i.e. it was served out of the dyld +// shared cache. +bool MachO::isInMemory() const { + return file != 0 && file->isInMemory(); +} + // return bundle version if available, otherwise NULL string std::string MachO::getVersion() const { std::string version; diff --git a/MachO/macho.h b/MachO/macho.h index b921d48..6320eb5 100644 --- a/MachO/macho.h +++ b/MachO/macho.h @@ -27,6 +27,7 @@ class EXPORT MachO { time_t getLastModificationTime() const; std::string getVersion() const; std::string getName() const; + bool isInMemory() const; const MachO* getParent() { return parent;} static DynamicLoader* dynamicLoader; static int referenceCounter; diff --git a/MachO/machoarchitecture.cpp b/MachO/machoarchitecture.cpp index 6a02c03..1934932 100644 --- a/MachO/machoarchitecture.cpp +++ b/MachO/machoarchitecture.cpp @@ -2,6 +2,7 @@ #include "machofile.h" #include "machoheader.h" #include "loadcommand.h" +#include "segmentcommand.h" #include "dylibcommand.h" #include "machoexception.h" #include "rpathcommand.h" @@ -10,6 +11,9 @@ #include "macho.h" #include "dynamicloader.h" +#include +#include + MachOArchitecture::MachOArchitecture(MachOFile& file, uint32_t magic, unsigned int size) : header(MachOHeader::getHeader(file, magic)), file(header->getFile()), size(size), hasReadLoadCommands(false), parent(0), dynamicLibIdCommand(0), uuid(0) @@ -113,5 +117,107 @@ const uint8_t* MachOArchitecture::getUuid() const { return uuid; } +std::vector MachOArchitecture::getSegments() const { + std::vector segments; + for (LoadCommandsConstIterator it = getLoadCommandsBegin(); it != getLoadCommandsEnd(); ++it) { + SegmentCommand* segment = dynamic_cast(*it); + if (segment != 0) + segments.push_back(segment); + } + return segments; +} + +uint64_t MachOArchitecture::getImageBase() const { + for (LoadCommandsConstIterator it = getLoadCommandsBegin(); it != getLoadCommandsEnd(); ++it) { + SegmentCommand* segment = dynamic_cast(*it); + if (segment != 0 && segment->getName() == "__TEXT") + return segment->getVMAddress(); + } + return 0; +} + +uint64_t MachOArchitecture::getMappedSize() const { + uint64_t imageBase = getImageBase(); + uint64_t end = imageBase; + std::vector segments = getSegments(); + for (unsigned int n = 0; n < segments.size(); n++) { + uint64_t segmentEnd = segments[n]->getVMAddress() + segments[n]->getVMSize(); + if (segmentEnd > end) + end = segmentEnd; + } + return end - imageBase; +} + +long long MachOArchitecture::getFileOffset(uint64_t vmAddress) const { + std::vector segments = getSegments(); + for (unsigned int n = 0; n < segments.size(); n++) { + long long offset = segments[n]->getFileOffsetForAddress(vmAddress); + if (offset >= 0) + return offset; + } + return -1; +} + +bool MachOArchitecture::readAtFileOffset(uint64_t offset, void* buffer, size_t size) const { + unsigned long long absoluteOffset = (unsigned long long)header->getOffset() + offset; + if (absoluteOffset + size > file.getSize()) + return false; + + long long savedPosition = file.getPosition(); + bool success = false; + try { + file.seek((long long)absoluteOffset); + file.readBytes((char*)buffer, size); + success = true; + } catch (MachOException&) { + success = false; + } + // The caller may be in the middle of walking the load commands. + file.seek(savedPosition); + return success; +} + +bool MachOArchitecture::readFromProcess(uint64_t address, void* buffer, size_t size) const { + if (size == 0) + return true; + if (address == 0) + return false; + + // The address is not necessarily backed by anything, so the copy is done + // by the kernel, which reports an error instead of taking the process down. + vm_size_t read = 0; + kern_return_t result = vm_read_overwrite(mach_task_self(), (vm_address_t)address, size, + (vm_address_t)buffer, &read); + return result == KERN_SUCCESS && read == size; +} + +bool MachOArchitecture::readAtAddress(uint64_t vmAddress, void* buffer, size_t size) const { + const uint8_t* mappedBase = file.getMappedBase(); + if (mappedBase != 0) { + // The image was mapped into this process by dyld. The synthesized file + // image only holds the header, the load commands and __LINKEDIT, but + // everything else -- the Objective-C metadata for instance -- can be + // read straight out of the mapping. dyld laid the segments out relative + // to the image base, so the file offsets do not apply here. + uint64_t imageBase = getImageBase(); + if (vmAddress < imageBase) + return false; + uint64_t offset = vmAddress - imageBase; + uint64_t processAddress = (uint64_t)(uintptr_t)mappedBase + offset; + if (offset + size <= getMappedSize()) { + memcpy(buffer, mappedBase + offset, size); + return true; + } + // Beyond the image: class name strings of a shared cache image live in + // a pool that belongs to no single image, so this is still worth a try. + return readFromProcess(processAddress, buffer, size); + } + + long long offset = getFileOffset(vmAddress); + if (offset < 0) + return false; + return readAtFileOffset((uint64_t)offset, buffer, size); +} + diff --git a/MachO/machoarchitecture.h b/MachO/machoarchitecture.h index 5944a93..70d0ceb 100644 --- a/MachO/machoarchitecture.h +++ b/MachO/machoarchitecture.h @@ -3,10 +3,14 @@ #include "macho_global.h" +#include +#include + class MachOFile; class MachOHeader; class LoadCommand; class DylibCommand; +class SegmentCommand; class EXPORT MachOArchitecture { @@ -19,7 +23,7 @@ class EXPORT MachOArchitecture MachOArchitecture(MachOFile& file, uint32_t magic, unsigned int size); ~MachOArchitecture(); - const MachOHeader* getHeader() { return header; } + const MachOHeader* getHeader() const { return header; } LoadCommandsConstIterator getLoadCommandsBegin() const { if(!hasReadLoadCommands) { readLoadCommands(); } return loadCommands.begin(); } LoadCommandsConstIterator getLoadCommandsEnd() const { if(!hasReadLoadCommands) { readLoadCommands(); } return loadCommands.end(); } DylibCommand* getDynamicLibIdCommand() const { if(!hasReadLoadCommands) { readLoadCommands(); } return dynamicLibIdCommand; } @@ -31,6 +35,39 @@ class EXPORT MachOArchitecture std::string getResolvedName(const std::string& name, const std::string& workingPath) const; const uint8_t* getUuid() const; + /** All LC_SEGMENT / LC_SEGMENT_64 commands of this architecture. */ + std::vector getSegments() const; + + /** Address the image itself is mapped to, i.e. the address of __TEXT. */ + uint64_t getImageBase() const; + + /** Size of the image as dyld mapped it. Only meaningful for a mapped image. */ + uint64_t getMappedSize() const; + + /** + * Offset (relative to the beginning of the Mach-O image) of the bytes that + * are mapped at `vmAddress`, or -1 when the address is not backed by the file. + */ + long long getFileOffset(uint64_t vmAddress) const; + + /** Reads `size` bytes at an offset relative to the beginning of the image. */ + bool readAtFileOffset(uint64_t offset, void* buffer, size_t size) const; + + /** Reads `size` bytes from `vmAddress`. Returns false when they are not there. */ + bool readAtAddress(uint64_t vmAddress, void* buffer, size_t size) const; + + /** + * Reads `size` bytes from an address in the address space of this process. + * + * readAtAddress translates the address through the image, which is what the + * load commands use; pointers that were taken out of a mapped image however + * are process addresses already, and some of them -- the class data the + * Objective-C runtime builds for a realized class, for instance -- do not + * belong to the image at all. Those are read here. Returns false when the + * memory is not readable instead of crashing. + */ + bool readFromProcess(uint64_t address, void* buffer, size_t size) const; + private: MachOHeader* header; MachOFile& file; diff --git a/MachO/machoexception.cpp b/MachO/machoexception.cpp index 66ba42c..8496298 100644 --- a/MachO/machoexception.cpp +++ b/MachO/machoexception.cpp @@ -1,6 +1,9 @@ #include "machoexception.h" -MachOException::MachOException(const std::string& cause) : cause(cause) +MachOException::MachOException(const std::string& cause) : cause(cause), warning(false) { } +MachOException::MachOException(const std::string& cause, bool warning) : cause(cause), warning(warning) +{ +} diff --git a/MachO/machoexception.h b/MachO/machoexception.h index a6b3c08..e6580bb 100644 --- a/MachO/machoexception.h +++ b/MachO/machoexception.h @@ -7,9 +7,12 @@ class EXPORT MachOException { public: MachOException(const std::string&); + MachOException(const std::string&, bool warning); const std::string& getCause() { return cause; } + const bool isWarning() { return warning; } private: const std::string cause; + bool warning; }; #endif // MACHOEXCEPTION_H diff --git a/MachO/machofile.cpp b/MachO/machofile.cpp index 276f1d0..0911bc1 100644 --- a/MachO/machofile.cpp +++ b/MachO/machofile.cpp @@ -34,6 +34,9 @@ std::string MachOFile::getDirectory() const { std::string MachOFile::getTitle() const { return file->getTitle(); } unsigned long long MachOFile::getSize() const { return file->getSize(); } time_t MachOFile::getLastModificationTime() const { return file->getLastModificationTime(); } +bool MachOFile::isInMemory() const { return file->isInMemory(); } +const uint8_t* MachOFile::getMappedBase() const { return file->getMappedBase(); } +intptr_t MachOFile::getMappedSlide() const { return file->getMappedSlide(); } uint32_t MachOFile::readUint32() { unsigned int temp; @@ -53,6 +56,26 @@ uint32_t MachOFile::readUint32BE() { return getUint32BE(temp); } +uint64_t MachOFile::readUint64() { + uint64_t temp; + readBytes((char*)&temp, sizeof(temp)); + return getUint64(temp); +} + +// getUint32 only handles 32 bit values, so a 64 bit value is split into its two +// halves first: the byte order of each half is reversed individually and the +// halves are then swapped. +uint64_t MachOFile::getUint64(uint64_t data) const { + if (!reversedByteOrder) + return data; + const char* source = (const char*)&data; + uint64_t result; + char* destination = (char*)&result; + for (unsigned int n = 0; n < sizeof(data); n++) + destination[n] = source[sizeof(data) - 1 - n]; + return result; +} + uint32_t MachOFile::getUint32BE(uint32_t data) { return convertByteOrder((char*)&data, true, sizeof(data)); } diff --git a/MachO/machofile.h b/MachO/machofile.h index b019dd5..f6bc0b1 100644 --- a/MachO/machofile.h +++ b/MachO/machofile.h @@ -14,10 +14,13 @@ class MachOFile uint32_t readUint32(); uint32_t readUint32LE(); uint32_t readUint32BE(); + uint64_t readUint64(); void readBytes(char* result, size_t size); uint32_t getUint32(unsigned int data) const {return (reversedByteOrder?reverseByteOrder(data):data);} + uint16_t getUint16(uint16_t data) const {return (reversedByteOrder?(uint16_t)((data >> 8) | (data << 8)):data);} + uint64_t getUint64(uint64_t data) const; static uint32_t getUint32LE(uint32_t data); static uint32_t getUint32BE(uint32_t data); std::string getDirectory() const; @@ -29,6 +32,16 @@ class MachOFile const std::string& getExecutablePath() const { return executablePath; } time_t getLastModificationTime() const; + /** True when the underlying bytes come from the dyld shared cache, not disk. */ + bool isInMemory() const; + + /** + * The image as dyld mapped it into this process, when it came from the dyld + * shared cache. Zero for a file on disk. + */ + const uint8_t* getMappedBase() const; + intptr_t getMappedSlide() const; + private: static unsigned int convertByteOrder(char* data, bool isBigEndian, unsigned int numberOfBytes); static unsigned int reverseByteOrder(unsigned int data); diff --git a/MachO/machofixups.cpp b/MachO/machofixups.cpp new file mode 100644 index 0000000..82d618a --- /dev/null +++ b/MachO/machofixups.cpp @@ -0,0 +1,507 @@ +#include "machofixups.h" + +#include "machoarchitecture.h" +#include "machoheader.h" +#include "loadcommand.h" +#include "linkeditdatacommand.h" +#include "segmentcommand.h" + +#include + +#ifndef LC_DYLD_INFO +#define LC_DYLD_INFO 0x22 +#endif +#ifndef LC_DYLD_INFO_ONLY +#define LC_DYLD_INFO_ONLY 0x80000022 +#endif +#ifndef LC_DYLD_CHAINED_FIXUPS +#define LC_DYLD_CHAINED_FIXUPS 0x80000034 +#endif + +namespace { + +// dyld_chained_fixups pointer formats +const uint16_t kPtrArm64e = 1; +const uint16_t kPtr64 = 2; +const uint16_t kPtr64Offset = 6; +const uint16_t kPtrArm64eUserland = 9; +const uint16_t kPtrArm64eUserland24 = 12; + +// dyld_chained_fixups import formats +const uint32_t kImport = 1; +const uint32_t kImportAddend = 2; +const uint32_t kImportAddend64 = 3; + +// dyld_chained_starts_in_segment page markers +const uint16_t kPageStartNone = 0xFFFF; +const uint16_t kPageStartMulti = 0x8000; + +// Sizes beyond these are not something a real image would contain. +const uint32_t kMaxFixupsBlobSize = 64u * 1024u * 1024u; +const size_t kMaxFixups = 8u * 1024u * 1024u; +const size_t kMaxChainLength = 1000000u; +const size_t kMaxImports = 4u * 1024u * 1024u; + +// LC_DYLD_INFO bind opcodes +const uint8_t kBindOpcodeMask = 0xF0; +const uint8_t kBindImmediateMask = 0x0F; +const uint8_t kBindOpcodeDone = 0x00; +const uint8_t kBindOpcodeSetDylibOrdinalImm = 0x10; +const uint8_t kBindOpcodeSetDylibOrdinalUleb = 0x20; +const uint8_t kBindOpcodeSetDylibSpecialImm = 0x30; +const uint8_t kBindOpcodeSetSymbolTrailingFlagsImm = 0x40; +const uint8_t kBindOpcodeSetTypeImm = 0x50; +const uint8_t kBindOpcodeSetAddendSleb = 0x60; +const uint8_t kBindOpcodeSetSegmentAndOffsetUleb = 0x70; +const uint8_t kBindOpcodeAddAddrUleb = 0x80; +const uint8_t kBindOpcodeDoBind = 0x90; +const uint8_t kBindOpcodeDoBindAddAddrUleb = 0xA0; +const uint8_t kBindOpcodeDoBindAddAddrImmScaled = 0xB0; +const uint8_t kBindOpcodeDoBindUlebTimesSkippingUleb = 0xC0; + +uint16_t readUint16(const uint8_t* data) { + uint16_t value; + memcpy(&value, data, sizeof(value)); + return value; +} + +uint32_t readUint32(const uint8_t* data) { + uint32_t value; + memcpy(&value, data, sizeof(value)); + return value; +} + +uint64_t readUint64(const uint8_t* data) { + uint64_t value; + memcpy(&value, data, sizeof(value)); + return value; +} + +uint64_t readUleb128(const uint8_t* data, size_t size, size_t& position) { + uint64_t result = 0; + unsigned int shift = 0; + while (position < size) { + uint8_t byte = data[position++]; + result |= (uint64_t)(byte & 0x7Fu) << shift; + if ((byte & 0x80u) == 0) + break; + shift += 7; + if (shift > 63) + break; + } + return result; +} + +int64_t readSleb128(const uint8_t* data, size_t size, size_t& position) { + int64_t result = 0; + unsigned int shift = 0; + uint8_t byte = 0; + while (position < size) { + byte = data[position++]; + result |= (int64_t)(byte & 0x7Fu) << shift; + shift += 7; + if ((byte & 0x80u) == 0) + break; + if (shift > 63) + break; + } + if (shift < 64 && (byte & 0x40u) != 0) + result |= -((int64_t)1 << shift); + return result; +} + +} // namespace + +MachOFixups::MachOFixups(const MachOArchitecture& anArchitecture) : + architecture(anArchitecture), supported(true), hasChainedFixups(false), + mappedBase(0), mappedSlide(0), mappedSize(0) +{ + mappedBase = architecture.getFile()->getMappedBase(); + mappedSlide = architecture.getFile()->getMappedSlide(); + if (mappedBase != 0) { + // dyld has already written every pointer of this image, so the fixup + // descriptions stored on disk no longer describe what is in memory. + // Reading the pointers as they are is all that is needed. + mappedSize = architecture.getMappedSize(); + return; + } + + for (MachOArchitecture::LoadCommandsConstIterator it = architecture.getLoadCommandsBegin(); + it != architecture.getLoadCommandsEnd(); + ++it) + { + LinkeditDataCommand* linkedit = dynamic_cast(*it); + if (linkedit == 0) + continue; + + switch (linkedit->getCommand()) { + case LC_DYLD_CHAINED_FIXUPS: + hasChainedFixups = true; + readChainedFixups(linkedit->getDataOffset(), linkedit->getDataSize()); + break; + case LC_DYLD_INFO: + case LC_DYLD_INFO_ONLY: + readDyldInfoBinds(linkedit->getBindOffset(), linkedit->getBindSize()); + readDyldInfoBinds(linkedit->getLazyBindOffset(), linkedit->getLazyBindSize()); + break; + default: + break; + } + } +} + +uint64_t MachOFixups::readPointer(uint64_t vmAddress) const { + uint64_t value = 0; + if (!architecture.readAtAddress(vmAddress, &value, sizeof(value))) + return 0; + return value; +} + +bool MachOFixups::readFileBlob(uint64_t offset, uint32_t size, std::vector& blob) const { + blob.clear(); + if (size == 0 || size > kMaxFixupsBlobSize) + return false; + blob.resize(size); + if (!architecture.readAtFileOffset(offset, &blob[0], size)) { + blob.clear(); + return false; + } + return true; +} + +bool MachOFixups::isAddressInImage(uint64_t address) const { + return mappedBase != 0 && + address >= (uint64_t)(uintptr_t)mappedBase && + address < (uint64_t)(uintptr_t)mappedBase + mappedSize; +} + +uint64_t MachOFixups::resolve(uint64_t vmAddress) const { + if (!supported) + return 0; + + if (mappedBase != 0) { + // A pointer of a mapped image is an address in this process already. + // Bring it back into the unslid space the load commands are in; an + // address outside the image cannot be resolved from the file. + uint64_t value = readPointer(vmAddress); + if (!isAddressInImage(value)) + return 0; + return (uint64_t)((int64_t)value - (int64_t)mappedSlide); + } + + std::map::const_iterator found = fixups.find(vmAddress); + if (found == fixups.end()) + return readPointer(vmAddress); + if (found->second.isImport) + return 0; + return found->second.value; +} + +bool MachOFixups::isImport(uint64_t vmAddress) const { + if (!supported) + return false; + if (mappedBase != 0) + return false; + std::map::const_iterator found = fixups.find(vmAddress); + return found != fixups.end() && found->second.isImport; +} + +std::string MachOFixups::getImportName(uint64_t vmAddress) const { + if (!supported || mappedBase != 0) + return std::string(); + std::map::const_iterator found = fixups.find(vmAddress); + if (found == fixups.end() || !found->second.isImport) + return std::string(); + if (found->second.value >= imports.size()) + return std::string(); + return imports[(size_t)found->second.value]; +} + +bool MachOFixups::isExternal(uint64_t vmAddress) const { + if (!supported || mappedBase == 0) + return false; + uint64_t value = readPointer(vmAddress); + return value != 0 && !isAddressInImage(value); +} + +uint64_t MachOFixups::getExternalAddress(uint64_t vmAddress) const { + if (!supported || mappedBase == 0) + return 0; + uint64_t value = readPointer(vmAddress); + if (value == 0 || isAddressInImage(value)) + return 0; + return value; +} + +// --------------------------------------------------------------------------- +// Chained fixups +// --------------------------------------------------------------------------- + +void MachOFixups::readChainedFixups(uint32_t offset, uint32_t size) { + std::vector blob; + if (!readFileBlob(offset, size, blob)) + return; + if (blob.size() < 28) { + supported = false; + return; + } + + uint32_t startsOffset = readUint32(&blob[4]); + uint32_t importsOffset = readUint32(&blob[8]); + uint32_t symbolsOffset = readUint32(&blob[12]); + uint32_t importsCount = readUint32(&blob[16]); + uint32_t importsFormat = readUint32(&blob[20]); + uint32_t symbolsFormat = readUint32(&blob[24]); + + readImports(blob, importsOffset, symbolsOffset, importsCount, importsFormat, symbolsFormat); + + if (startsOffset + 4 > blob.size()) { + supported = false; + return; + } + + uint32_t segmentCount = readUint32(&blob[startsOffset]); + std::vector segments = architecture.getSegments(); + + for (uint32_t index = 0; index < segmentCount; index++) { + size_t entryPosition = (size_t)startsOffset + 4 + (size_t)index * 4; + if (entryPosition + 4 > blob.size()) + break; + uint32_t segmentInfoOffset = readUint32(&blob[entryPosition]); + if (segmentInfoOffset == 0) + continue; + if (index >= segments.size()) + continue; + + size_t infoPosition = (size_t)startsOffset + segmentInfoOffset; + if (infoPosition + 22 > blob.size()) + continue; + + uint16_t pageSize = readUint16(&blob[infoPosition + 4]); + uint16_t pointerFormat = readUint16(&blob[infoPosition + 6]); + uint64_t segmentAddress = readUint64(&blob[infoPosition + 8]); + uint16_t pageCount = readUint16(&blob[infoPosition + 20]); + if (pageSize == 0) + continue; + + uint64_t segmentSize = segments[index]->getVMSize(); + if (pageCount == 0 && segmentSize > 0) + pageCount = (uint16_t)((segmentSize + pageSize - 1) / pageSize); + + for (uint16_t page = 0; page < pageCount; page++) { + size_t pageEntry = infoPosition + 22 + (size_t)page * 2; + if (pageEntry + 2 > blob.size()) + break; + uint16_t pageStart = readUint16(&blob[pageEntry]); + if (pageStart == kPageStartNone || (pageStart & kPageStartMulti) != 0) + continue; + walkChain(segmentAddress, segmentSize, pointerFormat, + (uint64_t)page * pageSize + pageStart); + } + } +} + +void MachOFixups::readImports(const std::vector& blob, uint32_t importsOffset, uint32_t symbolsOffset, + uint32_t importsCount, uint32_t importsFormat, uint32_t symbolsFormat) { + if (importsCount > kMaxImports) { + supported = false; + return; + } + + size_t importSize = 0; + switch (importsFormat) { + case kImport: importSize = 4; break; + case kImportAddend: importSize = 8; break; + case kImportAddend64: importSize = 16; break; + default: + supported = false; + return; + } + + if ((size_t)importsOffset + (size_t)importsCount * importSize > blob.size()) { + supported = false; + return; + } + + // Compressed symbol tables (symbolsFormat == 1) are rare; the bindings + // themselves stay usable, only their names are missing. + bool namesUsable = (symbolsFormat == 0) && (symbolsOffset < blob.size()); + + imports.reserve(importsCount); + for (uint32_t index = 0; index < importsCount; index++) { + const uint8_t* entry = &blob[(size_t)importsOffset + (size_t)index * importSize]; + uint32_t nameOffset = 0; + if (importsFormat == kImport || importsFormat == kImportAddend) + nameOffset = readUint32(entry) >> 9; + else + nameOffset = (uint32_t)(readUint64(entry) >> 32); + + std::string name; + if (namesUsable) { + size_t position = (size_t)symbolsOffset + nameOffset; + while (position < blob.size() && blob[position] != 0) + name.push_back((char)blob[position++]); + } + imports.push_back(name); + } +} + +void MachOFixups::walkChain(uint64_t segmentAddress, uint64_t segmentSize, + uint16_t pointerFormat, uint64_t offsetInSegment) { + bool format64 = (pointerFormat == kPtr64 || pointerFormat == kPtr64Offset); + bool formatArm64e = (pointerFormat == kPtrArm64e || pointerFormat == kPtrArm64eUserland || + pointerFormat == kPtrArm64eUserland24); + if (!format64 && !formatArm64e) { + // 32 bit pointer formats and the kernel cache formats are not decoded. + supported = false; + return; + } + + uint64_t imageBase = architecture.getImageBase(); + uint64_t vmAddress = segmentAddress + offsetInSegment; + uint64_t segmentEnd = segmentAddress + segmentSize; + + for (size_t step = 0; step < kMaxChainLength; step++) { + if (vmAddress < segmentAddress || vmAddress + 8 > segmentEnd) + return; + + // The pointer slots themselves live in the segments, not in the fixups + // blob; only the description of the chain is in the blob. + uint64_t raw = readPointer(vmAddress); + uint64_t next = 0; + + Fixup fixup; + if (format64) { + bool isBind = (raw >> 63) != 0; + next = (raw >> 51) & 0xFFF; + if (isBind) { + fixup.isImport = true; + fixup.value = raw & 0xFFFFFF; + } else { + uint64_t target = raw & 0xFFFFFFFFF; + uint64_t high8 = (raw >> 36) & 0xFF; + if (pointerFormat == kPtr64Offset) + target += imageBase; + fixup.isImport = false; + fixup.value = target | (high8 << 56); + } + } else { + bool isAuth = (raw >> 63) != 0; + bool isBind = ((raw >> 62) & 1) != 0; + next = (raw >> 51) & 0x7FF; + if (isBind) { + fixup.isImport = true; + fixup.value = (pointerFormat == kPtrArm64eUserland24) ? (raw & 0xFFFFFF) : (raw & 0xFFFF); + } else { + // An authenticated pointer carries a 32 bit offset, an + // unauthenticated one a 43 bit target. Both are relative to the + // image base. + uint64_t target = isAuth ? (raw & 0xFFFFFFFF) : (raw & 0x7FFFFFFFFFF); + fixup.isImport = false; + fixup.value = target + imageBase; + } + } + fixups[vmAddress] = fixup; + + if (fixups.size() > kMaxFixups) { + supported = false; + return; + } + + if (next == 0) + return; + vmAddress += next * 4; + } +} + +// --------------------------------------------------------------------------- +// Classic dyld info binds +// --------------------------------------------------------------------------- + +void MachOFixups::readDyldInfoBinds(uint32_t offset, uint32_t size) { + std::vector blob; + if (!readFileBlob(offset, size, blob)) + return; + readBindStream(&blob[0], blob.size()); +} + +void MachOFixups::readBindStream(const uint8_t* data, size_t size) { + std::vector segments = architecture.getSegments(); + + size_t position = 0; + uint64_t segmentOffset = 0; + std::string symbol; + size_t segmentIndex = 0; + bool haveSegment = false; + + while (position < size) { + uint8_t byte = data[position++]; + uint8_t opcode = byte & kBindOpcodeMask; + uint8_t immediate = byte & kBindImmediateMask; + + switch (opcode) { + case kBindOpcodeDone: + // A lazy bind stream is a sequence of independently decodable + // chunks, so "done" only resets the current symbol. + symbol.clear(); + haveSegment = false; + break; + case kBindOpcodeSetDylibOrdinalImm: + case kBindOpcodeSetDylibSpecialImm: + case kBindOpcodeSetTypeImm: + break; + case kBindOpcodeSetDylibOrdinalUleb: + readUleb128(data, size, position); + break; + case kBindOpcodeSetSymbolTrailingFlagsImm: { + symbol.clear(); + while (position < size && data[position] != 0) + symbol.push_back((char)data[position++]); + if (position < size) + position++; + break; + } + case kBindOpcodeSetAddendSleb: + readSleb128(data, size, position); + break; + case kBindOpcodeSetSegmentAndOffsetUleb: { + segmentIndex = immediate; + segmentOffset = readUleb128(data, size, position); + haveSegment = segmentIndex < segments.size(); + break; + } + case kBindOpcodeAddAddrUleb: + segmentOffset += readUleb128(data, size, position); + break; + case kBindOpcodeDoBind: + case kBindOpcodeDoBindAddAddrUleb: + case kBindOpcodeDoBindAddAddrImmScaled: + case kBindOpcodeDoBindUlebTimesSkippingUleb: { + uint64_t times = 1; + uint64_t skip = 0; + if (opcode == kBindOpcodeDoBindUlebTimesSkippingUleb) { + times = readUleb128(data, size, position); + skip = readUleb128(data, size, position); + } + for (uint64_t n = 0; n < times; n++) { + if (haveSegment && !symbol.empty() && imports.size() < kMaxImports) { + Fixup fixup; + fixup.isImport = true; + fixup.value = imports.size(); + imports.push_back(symbol); + fixups[segments[segmentIndex]->getVMAddress() + segmentOffset] = fixup; + } + segmentOffset += 8 + skip; + } + if (opcode == kBindOpcodeDoBindAddAddrUleb) + segmentOffset += readUleb128(data, size, position); + else if (opcode == kBindOpcodeDoBindAddAddrImmScaled) + segmentOffset += (uint64_t)immediate * 8; + break; + } + default: + // Unknown opcode: the rest of the stream cannot be decoded. + return; + } + } +} diff --git a/MachO/machofixups.h b/MachO/machofixups.h new file mode 100644 index 0000000..4d2b0d9 --- /dev/null +++ b/MachO/machofixups.h @@ -0,0 +1,105 @@ +#ifndef MACHOFIXUPS_H +#define MACHOFIXUPS_H + +#include "macho_global.h" + +#include +#include +#include +#include + +#include + +class MachOArchitecture; + +/** + * Pointer slots inside __DATA / __DATA_CONST are not filled in on disk: the + * linker leaves a placeholder and dyld rewrites it when the image is loaded. + * Reading a class list, an ivar list or a method list therefore means resolving + * those placeholders first. + * + * Two encodings are in use: + * + * - chained fixups (LC_DYLD_CHAINED_FIXUPS, macOS 12 / iOS 13.4 and later). + * Each slot carries a small structure that either holds a target address + * relative to the image base (a rebase) or an index into the import table + * (a bind), plus the distance to the next slot of the chain. + * - the classic rebase/bind opcode streams of LC_DYLD_INFO(_ONLY), which only + * list the slots that need to be written; a slot holding a bind is zero on + * disk. + * + * Both are decoded here. Images without either (object files, static archives) + * simply have plain pointers. + */ +class EXPORT MachOFixups +{ +public: + MachOFixups(const MachOArchitecture& architecture); + + /** + * False when the image uses a pointer encoding this class does not know. + * The values returned by the accessors are then not trustworthy. + */ + bool isSupported() const { return supported; } + + /** True when the image has pointer fixups at all. */ + bool hasFixups() const { return !fixups.empty(); } + + /** + * Address that the pointer slot at `vmAddress` holds. A slot that is an + * import has no address and yields 0. + */ + uint64_t resolve(uint64_t vmAddress) const; + + /** True when the slot at `vmAddress` is an import. */ + bool isImport(uint64_t vmAddress) const; + + /** Symbol name the slot at `vmAddress` is bound to, or an empty string. */ + std::string getImportName(uint64_t vmAddress) const; + + /** + * True when the slot at `vmAddress` points outside this image. Only ever + * the case for an image that was mapped into this process, where the + * pointer is an address of another loaded image. + */ + bool isExternal(uint64_t vmAddress) const; + + /** That outside address, or 0. See isExternal(). */ + uint64_t getExternalAddress(uint64_t vmAddress) const; + +private: + struct Fixup { + Fixup() : isImport(false), value(0) {} + bool isImport; + // rebase: the target address. import: an index into `imports`. + uint64_t value; + }; + + uint64_t readPointer(uint64_t vmAddress) const; + bool readFileBlob(uint64_t offset, uint32_t size, std::vector& blob) const; + bool isAddressInImage(uint64_t address) const; + + void readChainedFixups(uint32_t offset, uint32_t size); + void walkChain(uint64_t segmentAddress, uint64_t segmentSize, + uint16_t pointerFormat, uint64_t offsetInSegment); + void readImports(const std::vector& blob, uint32_t importsOffset, uint32_t symbolsOffset, + uint32_t importsCount, uint32_t importsFormat, uint32_t symbolsFormat); + + void readDyldInfoBinds(uint32_t offset, uint32_t size); + void readBindStream(const uint8_t* data, size_t size); + + const MachOArchitecture& architecture; + std::vector imports; + std::map fixups; + bool supported; + bool hasChainedFixups; + + // Set when the image was mapped into this process by dyld. Its pointers are + // already relocated, so there is nothing to decode -- they only have to be + // translated back into the unslid address space the load commands use. + const uint8_t* mappedBase; + intptr_t mappedSlide; + uint64_t mappedSize; +}; + +#endif // MACHOFIXUPS_H diff --git a/MachO/memoryinternalfile.cpp b/MachO/memoryinternalfile.cpp new file mode 100644 index 0000000..e4972a1 --- /dev/null +++ b/MachO/memoryinternalfile.cpp @@ -0,0 +1,46 @@ +#include "memoryinternalfile.h" + +#include + +MemoryInternalFile::MemoryInternalFile(const std::string& filename, const char* data, size_t size, + const uint8_t* mappedBase, intptr_t mappedSlide) : +InternalFile(filename), data(data, data + size), position(0), mappedBase(mappedBase), mappedSlide(mappedSlide) +{ +} + +unsigned long long MemoryInternalFile::getSize() const { + return (unsigned long long)data.size(); +} + +bool MemoryInternalFile::seek(long long int position) { + if (position < 0 || (unsigned long long)position > data.size()) { + return false; + } + this->position = position; + return true; +} + +std::streamsize MemoryInternalFile::read(char* buffer, std::streamsize size) { + std::streamsize available = (std::streamsize)data.size() - position; + if (available <= 0 || size <= 0) + return 0; + + std::streamsize count = (size < available) ? size : available; + if (!data.empty()) + memcpy(buffer, &data[(size_t)position], (size_t)count); + position += count; + return count; +} + +long long int MemoryInternalFile::getPosition() { + return position; +} + +// A cached library has no file on disk, so there is no modification time. +time_t MemoryInternalFile::getLastModificationTime() const { + return 0; +} + +bool MemoryInternalFile::isInMemory() const { + return true; +} diff --git a/MachO/memoryinternalfile.h b/MachO/memoryinternalfile.h new file mode 100644 index 0000000..d785cb5 --- /dev/null +++ b/MachO/memoryinternalfile.h @@ -0,0 +1,40 @@ +#ifndef MEMORYINTERNALFILE_H +#define MEMORYINTERNALFILE_H + +#include "internalfile.h" + +#include +#include + +/** + * An InternalFile backed by a buffer in memory. Used for libraries of the dyld + * shared cache, which have no file on disk but whose image can be copied out of + * the running process. + */ +class MemoryInternalFile : public InternalFile +{ +public: + MemoryInternalFile(const std::string& filename, const char* data, size_t size, + const uint8_t* mappedBase = 0, intptr_t mappedSlide = 0); + + virtual unsigned long long getSize() const; + virtual bool seek(long long int position); + virtual std::streamsize read(char* buffer, std::streamsize size); + virtual long long int getPosition(); + virtual time_t getLastModificationTime() const; + + /** Always true: a cached library has no file on disk. */ + virtual bool isInMemory() const; + + /** The image as dyld mapped it, so the parts missing from the buffer stay readable. */ + virtual const uint8_t* getMappedBase() const { return mappedBase; } + virtual intptr_t getMappedSlide() const { return mappedSlide; } + +private: + std::vector data; + long long int position; + const uint8_t* mappedBase; + intptr_t mappedSlide; +}; + +#endif // MEMORYINTERNALFILE_H diff --git a/MachO/objcmetadata.cpp b/MachO/objcmetadata.cpp new file mode 100644 index 0000000..fb88ab0 --- /dev/null +++ b/MachO/objcmetadata.cpp @@ -0,0 +1,1284 @@ +#include "objcmetadata.h" + +#include "machoarchitecture.h" +#include "machoheader.h" +#include "segmentcommand.h" +#include "objctypedecoder.h" + +#include +#include +#include +#include +#include + +namespace { + +// class_ro_t flags +const uint32_t kClassFlagMeta = 1 << 0; +const uint32_t kClassFlagRoot = 1 << 1; + +// Relative method lists mark themselves with this bit in entsizeAndFlags. +const uint32_t kMethodListRelativeFlag = 0x80000000u; +const uint32_t kEntsizeMask = 0x0000FFFCu; + +// class_data_bits_t: the pointer is stored above these bits, which hold the +// runtime's own flags instead. +const uint64_t kFastDataMask = 0x00007ffffffffff8ull; + +// class_rw_t::flags +const uint32_t kRwRealized = 1u << 31; + +// class_rw_t::ro_or_rw_ext carries this bit when it points at a class_rw_ext_t +// (which the runtime builds for a class that has grown beyond its ro) rather +// than at the class_ro_t itself. +const uint64_t kRwExtFlag = 1; + +const size_t kMaxStringLength = 8192; +const size_t kMaxListEntries = 500000; +const size_t kMaxClasses = 500000; + +// protocol_t::_classProperties was added to the struct after everything else, +// and a protocol_t only carries the field when its own size field covers it: +// the size sits at +64 and is the size of the struct, so the class properties +// are there exactly when it is at least the offset of the field plus a +// pointer. Measured on the protocols of this machine: the field is at +88 and +// the size of a protocol_t that has it is 96. +const uint32_t kProtocolClassPropertiesOffset = 88; +const uint32_t kProtocolSizeWithClassProperties = kProtocolClassPropertiesOffset + 8; + +uint32_t readUint32(const uint8_t* data) { + uint32_t value; + memcpy(&value, data, sizeof(value)); + return value; +} + +uint64_t readUint64(const uint8_t* data) { + uint64_t value; + memcpy(&value, data, sizeof(value)); + return value; +} + +int32_t readInt32(const uint8_t* data) { + int32_t value; + memcpy(&value, data, sizeof(value)); + return value; +} + +/** "_OBJC_CLASS_$_NSString" -> "NSString" */ +std::string stripClassSymbolPrefix(const std::string& symbol) { + static const char* prefixes[] = { + "_OBJC_CLASS_$_", "_OBJC_METACLASS_$_", "_OBJC_PROTOCOL_$_" + }; + for (unsigned int n = 0; n < sizeof(prefixes) / sizeof(*prefixes); n++) { + size_t length = strlen(prefixes[n]); + if (symbol.compare(0, length, prefixes[n]) == 0) + return symbol.substr(length); + } + return symbol; +} + +/** True when the symbol is the one that stands for a class object. */ +bool isClassSymbol(const std::string& symbol) { + static const char* prefixes[] = { "_OBJC_CLASS_$_", "_OBJC_METACLASS_$_" }; + for (unsigned int n = 0; n < sizeof(prefixes) / sizeof(*prefixes); n++) { + if (symbol.compare(0, strlen(prefixes[n]), prefixes[n]) == 0) + return true; + } + return false; +} + +/** + * The runtime hands out the module qualified name of a Swift protocol + * ("AppKit._NSAssertion"), while the image and the other dump tools use the + * mangled symbol ("_TtP6AppKit12_NSAssertion_"). Rebuilding it keeps the two + * spellings -- and the two ways of reading an image -- consistent. + */ +std::string swiftProtocolMangledName(const std::string& name) { + size_t dot = name.find('.'); + if (dot == std::string::npos || dot == 0 || dot + 1 >= name.size()) + return name; + + std::string module = name.substr(0, dot); + std::string type = name.substr(dot + 1); + + char length[24]; + std::string result = "_TtP"; + snprintf(length, sizeof(length), "%u", (unsigned int)module.size()); + result += length; + result += module; + snprintf(length, sizeof(length), "%u", (unsigned int)type.size()); + result += length; + result += type; + result += "_"; + return result; +} + +bool contains(const std::vector& values, const std::string& value) { + for (unsigned int n = 0; n < values.size(); n++) { + if (values[n] == value) + return true; + } + return false; +} + +/** + * The compiler emits a .cxx_destruct / .cxx_construct method for every class + * with a C++ ivar. They are bookkeeping, not part of the API, and the other + * class dump tools leave them out as well. + */ +bool isCompilerGeneratedMethod(const std::string& selector) { + return selector == ".cxx_destruct" || selector == ".cxx_construct"; +} + +/** + * Names the exported symbol that lives at `address`. + * + * This is only used for an image that dyld mapped into this process, where a + * pointer to another image is an address in the running process -- asking the + * dynamic linker is then the only way to find out what it points at. An image + * read from disk gets the name from its own bind information instead. + */ +std::string symbolNameAtAddress(uint64_t address) { + if (address == 0) + return std::string(); + + // An arm64e pointer may carry pointer authentication bits in its high bits. + const uint64_t candidates[] = { address, address & 0x0000000FFFFFFFFFull }; + for (unsigned int n = 0; n < sizeof(candidates) / sizeof(*candidates); n++) { + if (candidates[n] == 0) + continue; + Dl_info info; + if (dladdr((const void*)(uintptr_t)candidates[n], &info) == 0 || info.dli_sname == 0) + continue; + // Only accept an exact match: otherwise the address sits inside some + // unrelated symbol and its name would be misleading. + if ((uint64_t)(uintptr_t)info.dli_saddr == candidates[n]) + return std::string(info.dli_sname); + } + return std::string(); +} + +/** + * The class a mapped image's classref points at, or an empty string when the + * address is not a class object. Only class symbols are accepted, so a pointer + * that happens to land inside an unrelated function cannot name a class. + */ +std::string classSymbolNameAtAddress(uint64_t address) { + std::string symbol = symbolNameAtAddress(address); + if (symbol.empty() || !isClassSymbol(symbol)) + return std::string(); + return stripClassSymbolPrefix(symbol); +} + +void appendJoined(std::string& target, const std::vector& values, const char* separator) { + for (unsigned int n = 0; n < values.size(); n++) { + if (n > 0) + target += separator; + target += values[n]; + } +} + +/** "1 instance method" / "34 instance methods" */ +std::string describeCount(uint32_t count, const char* singular) { + char buffer[64]; + snprintf(buffer, sizeof(buffer), "%u %s%s", count, singular, count == 1 ? "" : "s"); + return std::string(buffer); +} + +} // namespace + +struct ObjCMetadata::Method { + std::string name; + std::string types; +}; + +struct ObjCMetadata::Ivar { + std::string name; + std::string type; +}; + +struct ObjCMetadata::Property { + std::string name; + std::string attributes; +}; + +struct ObjCMetadata::Class { + Class() : superclassKnown(false), isRoot(false) {} + + std::string name; + std::string superclass; + bool superclassKnown; + bool isRoot; + std::vector protocols; + std::vector ivars; + std::vector properties; + std::vector instanceMethods; + std::vector classMethods; +}; + +struct ObjCMetadata::Category { + Category() : instanceMethodCount(0), classMethodCount(0) {} + + std::string name; // the part in parentheses + std::string className; + std::vector protocols; + std::vector properties; + std::vector instanceMethods; + std::vector classMethods; + + // What the method lists claim to hold. For a shared cache image the lists + // themselves cannot be decoded, but their headers can, so at least the + // number of methods is known. + uint32_t instanceMethodCount; + uint32_t classMethodCount; +}; + +struct ObjCMetadata::Protocol { + std::string name; + std::vector protocols; + std::vector properties; + // A protocol can declare properties of the class as a whole; they are a + // list of their own and are always required (there is no field for + // optional ones, and no image has ever turned out to have one). + std::vector classProperties; + std::vector requiredInstanceMethods; + std::vector optionalInstanceMethods; + std::vector requiredClassMethods; + std::vector optionalClassMethods; +}; + +ObjCMetadata::ObjCMetadata(const MachOArchitecture& anArchitecture) : + architecture(anArchitecture), fixups(anArchitecture), parsed(false) +{ +} + +ObjCMetadata::~ObjCMetadata() { + for (unsigned int n = 0; n < classes.size(); n++) + delete classes[n]; + for (unsigned int n = 0; n < categories.size(); n++) + delete categories[n]; + for (unsigned int n = 0; n < protocols.size(); n++) + delete protocols[n]; +} + +const uint8_t* ObjCMetadata::mappedBase() const { + return architecture.getFile()->getMappedBase(); +} + +void ObjCMetadata::parse() const { + if (parsed) + return; + parsed = true; + + // The class_t layout that is decoded here is the 64 bit one. + if (!architecture.getHeader()->is64Bit()) + return; + + // __objc_classlist lives in __DATA_CONST on modern images and in __DATA on + // older ones, so all segments are searched. + const Section* classList = 0; + const Section* categoryList = 0; + const Section* protocolList = 0; + std::vector segments = architecture.getSegments(); + for (unsigned int n = 0; n < segments.size(); n++) { + if (classList == 0) + classList = segments[n]->findSection("__objc_classlist"); + if (categoryList == 0) + categoryList = segments[n]->findSection("__objc_catlist"); + if (protocolList == 0) + protocolList = segments[n]->findSection("__objc_protolist"); + } + + if (classList != 0) { + size_t count = (size_t)(classList->getSize() / 8); + if (count > kMaxClasses) + count = kMaxClasses; + + for (size_t n = 0; n < count; n++) { + uint64_t classAddress = fixups.resolve(classList->getAddress() + n * 8); + if (classAddress == 0) + continue; + Class* info = new Class(); + if (readClass(classAddress, *info)) + classes.push_back(info); + else + delete info; + } + } + + if (categoryList != 0) { + size_t count = (size_t)(categoryList->getSize() / 8); + if (count > kMaxClasses) + count = kMaxClasses; + + for (size_t n = 0; n < count; n++) { + uint64_t categoryAddress = fixups.resolve(categoryList->getAddress() + n * 8); + if (categoryAddress == 0) + continue; + Category* info = new Category(); + if (readCategory(categoryAddress, *info)) + categories.push_back(info); + else + delete info; + } + } + + if (protocolList != 0) { + size_t count = (size_t)(protocolList->getSize() / 8); + if (count > kMaxClasses) + count = kMaxClasses; + + for (size_t n = 0; n < count; n++) { + uint64_t protocolAddress = fixups.resolve(protocolList->getAddress() + n * 8); + if (protocolAddress == 0) + continue; + Protocol* info = new Protocol(); + // Some images list a protocol more than once; declaring it twice + // would be wrong, so the first one wins. + if (readProtocol(protocolAddress, *info) && !hasProtocolNamed(info->name)) + protocols.push_back(info); + else + delete info; + } + } +} + +bool ObjCMetadata::hasProtocolNamed(const std::string& name) const { + for (unsigned int n = 0; n < protocols.size(); n++) { + if (protocols[n]->name == name) + return true; + } + return false; +} + +std::string ObjCMetadata::readString(uint64_t address) const { + std::string result; + if (address == 0) + return result; + + char chunk[64]; + while (result.size() < kMaxStringLength) { + size_t remaining = kMaxStringLength - result.size(); + size_t request = remaining < sizeof(chunk) ? remaining : sizeof(chunk); + size_t got = request; + // The last chunk may reach past the end of the segment; fall back to a + // single byte so that a string ending right at the boundary still works. + if (!architecture.readAtAddress(address + result.size(), chunk, request)) { + got = 1; + if (!architecture.readAtAddress(address + result.size(), chunk, 1)) + break; + } + for (size_t n = 0; n < got; n++) { + if (chunk[n] == 0) + return result; + result.push_back(chunk[n]); + } + } + return result; +} + +uint64_t ObjCMetadata::decodeMappedClassData(uint64_t bits) const { + // An image dyld mapped into this process does not hold the class_ro_t in + // class_data_bits_t::bits any more: the runtime realizes its classes and + // rewrites the field to point at the class_rw_t it built, which is a + // per-process structure that belongs to no image. + uint64_t data = bits & kFastDataMask; + + uint32_t flags = 0; + if (!architecture.readFromProcess(data, &flags, sizeof(flags)) || (flags & kRwRealized) == 0) { + // Not realized after all, so the field still holds the ro. + return data & ~(uint64_t)7; + } + + uint64_t roOrRwExt = 0; + if (!architecture.readFromProcess(data + 8, &roOrRwExt, sizeof(roOrRwExt))) + return data & ~(uint64_t)7; + + if ((roOrRwExt & kRwExtFlag) != 0) { + // class_rw_ext_t, whose first member is the class_ro_t. + uint64_t extended = roOrRwExt & ~(uint64_t)7; + uint64_t ro = 0; + if (!architecture.readFromProcess(extended, &ro, sizeof(ro))) + return data & ~(uint64_t)7; + return ro & ~(uint64_t)7; + } + + return roOrRwExt & ~(uint64_t)7; +} + +uint64_t ObjCMetadata::classRoAddress(uint64_t classAddress) const { + const uint8_t* mappedBase = architecture.getFile()->getMappedBase(); + if (mappedBase == 0) { + // On disk the field holds the address of the ro itself, behind a fixup. + return fixups.resolve(classAddress + 32) & ~(uint64_t)7; + } + + uint64_t bits = 0; + if (!architecture.readAtAddress(classAddress + 32, &bits, sizeof(bits))) + return 0; + + // decodeMappedClassData works with process addresses, everything else here + // with the unslid ones the load commands are in. + uint64_t ro = decodeMappedClassData(bits); + if (ro == 0) + return 0; + return ro - (uint64_t)architecture.getFile()->getMappedSlide(); +} + +std::string ObjCMetadata::readClassName(uint64_t classAddress) const { + uint64_t roAddress = classRoAddress(classAddress); + if (roAddress == 0) + return std::string(); + return readString(fixups.resolve(roAddress + 24)); +} + +uint32_t ObjCMetadata::readListCount(uint64_t listAddress) const { + if (listAddress == 0) + return 0; + uint8_t header[8]; + if (!architecture.readAtAddress(listAddress, header, sizeof(header))) + return 0; + uint32_t count = readUint32(header + 4); + return count > kMaxListEntries ? 0 : count; +} + +std::string ObjCMetadata::readClassReferenceName(uint64_t slotAddress) const { + if (fixups.isImport(slotAddress)) { + // The class lives in another image and the bind names it. + return stripClassSymbolPrefix(fixups.getImportName(slotAddress)); + } + + if (mappedBase() != 0) { + // A mapped image holds a real address here, which may belong to any + // other loaded image; the dynamic linker knows what lives there. + uint64_t raw = 0; + if (architecture.readAtAddress(slotAddress, &raw, sizeof(raw))) { + std::string name = classSymbolNameAtAddress(raw); + if (!name.empty()) + return name; + } + } + + uint64_t address = fixups.resolve(slotAddress); + if (address == 0) + return std::string(); + return readClassName(address); +} + +void ObjCMetadata::readMethods(uint64_t listAddress, std::vector& result) const { + if (listAddress == 0) + return; + + uint8_t header[8]; + if (!architecture.readAtAddress(listAddress, header, sizeof(header))) + return; + uint32_t entsizeAndFlags = readUint32(header); + uint32_t count = readUint32(header + 4); + if (count > kMaxListEntries) + return; + + uint32_t entrySize = entsizeAndFlags & kEntsizeMask; + bool isRelative = (entsizeAndFlags & kMethodListRelativeFlag) != 0 || entrySize == 12; + if (entrySize == 0) + entrySize = isRelative ? 12 : 24; + if (entrySize < 12 || entrySize > 64) + return; + + for (uint32_t n = 0; n < count; n++) { + uint64_t entryAddress = listAddress + 8 + (uint64_t)n * entrySize; + Method method; + if (isRelative) { + uint8_t entry[12]; + if (!architecture.readAtAddress(entryAddress, entry, sizeof(entry))) + break; + int32_t nameOffset = readInt32(entry); + int32_t typesOffset = readInt32(entry + 4); + // The name offset points at a selector reference (one more + // indirection), the type offset straight at the type string. Both + // are relative to the field they are stored in. + uint64_t selectorSlot = (uint64_t)((int64_t)entryAddress + nameOffset); + method.name = readString(fixups.resolve(selectorSlot)); + method.types = readString((uint64_t)((int64_t)entryAddress + 4 + typesOffset)); + } else { + uint8_t entry[24]; + if (!architecture.readAtAddress(entryAddress, entry, sizeof(entry))) + break; + method.name = readString(fixups.resolve(entryAddress)); + method.types = readString(fixups.resolve(entryAddress + 8)); + } + if (!method.name.empty() && !isCompilerGeneratedMethod(method.name)) + result.push_back(method); + } +} + +void ObjCMetadata::readIvars(uint64_t listAddress, std::vector& result) const { + if (listAddress == 0) + return; + + uint8_t header[8]; + if (!architecture.readAtAddress(listAddress, header, sizeof(header))) + return; + uint32_t entrySize = readUint32(header) & kEntsizeMask; + uint32_t count = readUint32(header + 4); + if (entrySize == 0) + entrySize = 32; + if (entrySize < 32 || count > kMaxListEntries) + return; + + for (uint32_t n = 0; n < count; n++) { + // ivar_t: offset, name, type, alignment, size + uint64_t entryAddress = listAddress + 8 + (uint64_t)n * entrySize; + uint8_t entry[32]; + if (!architecture.readAtAddress(entryAddress, entry, sizeof(entry))) + break; + Ivar ivar; + ivar.name = readString(fixups.resolve(entryAddress + 8)); + ivar.type = readString(fixups.resolve(entryAddress + 16)); + if (!ivar.name.empty()) + result.push_back(ivar); + } +} + +void ObjCMetadata::readProperties(uint64_t listAddress, std::vector& result) const { + if (listAddress == 0) + return; + + uint8_t header[8]; + if (!architecture.readAtAddress(listAddress, header, sizeof(header))) + return; + uint32_t entrySize = readUint32(header) & kEntsizeMask; + uint32_t count = readUint32(header + 4); + if (entrySize == 0) + entrySize = 16; + if (entrySize < 16 || count > kMaxListEntries) + return; + + for (uint32_t n = 0; n < count; n++) { + // property_t: name, attributes + uint64_t entryAddress = listAddress + 8 + (uint64_t)n * entrySize; + uint8_t entry[16]; + if (!architecture.readAtAddress(entryAddress, entry, sizeof(entry))) + break; + Property property; + property.name = readString(fixups.resolve(entryAddress)); + property.attributes = readString(fixups.resolve(entryAddress + 8)); + if (!property.name.empty()) + result.push_back(property); + } +} + +bool ObjCMetadata::readMethodsFromRuntime(uint64_t processClassAddress, bool metaclass, + std::vector& result) const { + if (processClassAddress == 0) + return false; + + // `Class` and `Method` name the nested structs here, so the runtime's own + // types have to be spelled out. + ::Class cls = (::Class)(uintptr_t)processClassAddress; + ::Class target = cls; + if (metaclass) { + // The metaclass holds the class methods. It is looked up by name + // because object_getClass takes an instance, and a class_t is not one. + const char* className = class_getName(cls); + if (className == 0) + return false; + target = objc_getMetaClass(className); + } + if (target == nil) + return false; + + unsigned int count = 0; + ::Method* methods = class_copyMethodList(target, &count); + if (methods == 0) { + // A class without methods is not an error, the runtime just has + // nothing to return. + return count == 0; + } + + for (unsigned int n = 0; n < count; n++) { + const char* name = sel_getName(method_getName(methods[n])); + if (name == 0 || isCompilerGeneratedMethod(name)) + continue; + const char* types = method_getTypeEncoding(methods[n]); + Method method; + method.name = name; + if (types != 0) + method.types = types; + result.push_back(method); + } + + free(methods); + return true; +} + +bool ObjCMetadata::readProtocolsFromRuntime(uint64_t processClassAddress, + std::vector& result) const { + if (processClassAddress == 0) + return false; + + ::Class cls = (::Class)(uintptr_t)processClassAddress; + unsigned int count = 0; + ::Protocol* const* protocols = class_copyProtocolList(cls, &count); + if (protocols == 0) + return count == 0; + + for (unsigned int n = 0; n < count; n++) { + const char* name = protocol_getName(protocols[n]); + if (name == 0) + continue; + std::string normalized = swiftProtocolMangledName(name); + if (!contains(result, normalized)) + result.push_back(normalized); + } + + free((void*)protocols); + return true; +} + +bool ObjCMetadata::readProtocolFromRuntime(const std::string& name, Protocol& result) const { + // The runtime keys protocols by their mangled name, which is what the image + // holds as well. + ::Protocol* protocol = objc_getProtocol(name.c_str()); + if (protocol == nil) + return false; + + result.name = name; + + unsigned int count = 0; + ::Protocol* __unsafe_unretained* parents = protocol_copyProtocolList(protocol, &count); + if (parents != 0) { + for (unsigned int n = 0; n < count; n++) { + const char* parentName = protocol_getName(parents[n]); + if (parentName != 0) + result.protocols.push_back(swiftProtocolMangledName(parentName)); + } + free(parents); + } + + // The instance properties and the class properties of a protocol are two + // lists of their own, and protocol_copyPropertyList() only hands out the + // first of them. + objc_property_t* propertyLists[2]; + std::vector* propertyTargets[2] = { &result.properties, &result.classProperties }; + unsigned int propertyCounts[2] = { 0, 0 }; + propertyLists[0] = protocol_copyPropertyList(protocol, &propertyCounts[0]); + propertyLists[1] = protocol_copyPropertyList2(protocol, &propertyCounts[1], YES, NO); + + for (unsigned int list = 0; list < 2; list++) { + if (propertyLists[list] == 0) + continue; + for (unsigned int n = 0; n < propertyCounts[list]; n++) { + Property property; + const char* propertyName = property_getName(propertyLists[list][n]); + const char* attributes = property_getAttributes(propertyLists[list][n]); + if (propertyName != 0) + property.name = propertyName; + if (attributes != 0) + property.attributes = attributes; + if (!property.name.empty()) + propertyTargets[list]->push_back(property); + } + free(propertyLists[list]); + } + + struct { + std::vector* target; + BOOL required; + BOOL instance; + } groups[] = { + { &result.requiredInstanceMethods, YES, YES }, + { &result.optionalInstanceMethods, NO, YES }, + { &result.requiredClassMethods, YES, NO }, + { &result.optionalClassMethods, NO, NO }, + }; + + for (unsigned int g = 0; g < sizeof(groups) / sizeof(*groups); g++) { + struct objc_method_description* descriptions = + protocol_copyMethodDescriptionList(protocol, groups[g].required, groups[g].instance, &count); + if (descriptions == 0) + continue; + for (unsigned int n = 0; n < count; n++) { + const char* selector = descriptions[n].name != 0 ? sel_getName(descriptions[n].name) : 0; + if (selector == 0 || isCompilerGeneratedMethod(selector)) + continue; + Method method; + method.name = selector; + if (descriptions[n].types != 0) + method.types = descriptions[n].types; + groups[g].target->push_back(method); + } + free(descriptions); + } + + return true; +} + +void ObjCMetadata::readProtocols(uint64_t listAddress, std::vector& result) const { + if (listAddress == 0) + return; + + uint64_t count = fixups.resolve(listAddress); + if (count == 0 || count > kMaxListEntries) + return; + + for (uint64_t n = 0; n < count; n++) { + uint64_t slot = listAddress + 8 + n * 8; + uint64_t protocolAddress = fixups.resolve(slot); + if (protocolAddress == 0) { + // A protocol defined in another image; only a mapped image can say + // which one it is. + std::string external = stripClassSymbolPrefix(symbolNameAtAddress(fixups.getExternalAddress(slot))); + if (!external.empty() && !contains(result, external)) + result.push_back(external); + continue; + } + // protocol_t: isa, mangledName, ... + std::string name = readString(fixups.resolve(protocolAddress + 8)); + if (!name.empty() && !contains(result, name)) + result.push_back(name); + } +} + +bool ObjCMetadata::readClass(uint64_t address, Class& result) const { + // class_t: isa, superclass, cache (two words), bits + uint8_t classData[40]; + if (!architecture.readAtAddress(address, classData, sizeof(classData))) + return false; + + // The low bits of `bits` are flags, not part of the address. + uint64_t roAddress = classRoAddress(address); + if (roAddress == 0) + return false; + + // class_ro_t: flags, instanceStart, instanceSize, reserved, ivarLayout, + // name, baseMethods, baseProtocols, ivars, weakIvarLayout, + // baseProperties + uint8_t ro[72]; + if (!architecture.readAtAddress(roAddress, ro, sizeof(ro))) + return false; + + uint32_t flags = readUint32(ro); + result.isRoot = (flags & kClassFlagRoot) != 0; + result.name = readString(fixups.resolve(roAddress + 24)); + if (result.name.empty()) + return false; + + if (fixups.isImport(address + 8)) { + // The superclass lives in another image; the fixup names it. + result.superclass = stripClassSymbolPrefix(fixups.getImportName(address + 8)); + result.superclassKnown = !result.superclass.empty(); + } else if (fixups.isExternal(address + 8)) { + // Same situation, but for an image dyld mapped into this process: the + // pointer is an address of the other image, so ask the dynamic linker. + result.superclass = stripClassSymbolPrefix(symbolNameAtAddress(fixups.getExternalAddress(address + 8))); + result.superclassKnown = !result.superclass.empty(); + } else { + uint64_t superclassAddress = fixups.resolve(address + 8); + if (superclassAddress != 0) { + result.superclass = readClassName(superclassAddress); + result.superclassKnown = !result.superclass.empty(); + } else { + result.superclassKnown = result.isRoot; + } + } + + // Ivars and properties read fine out of the image, mapped or not. + readIvars(fixups.resolve(roAddress + 48), result.ivars); + readProperties(fixups.resolve(roAddress + 64), result.properties); + + // The method and protocol lists of a shared cache image cannot be read out + // of the image: dyld stores them in __TEXT.__objc_methlist as eight byte + // entries in an encoding of its own, with the selector strings moved out of + // the image entirely. The runtime has already decoded all of that, so ask + // it -- but only after checking that it really is this very class, or the + // answer would describe some other image's class of the same name. + uint64_t processClassAddress = 0; + if (architecture.getFile()->getMappedBase() != 0) { + uint64_t candidate = address + (uint64_t)architecture.getFile()->getMappedSlide(); + ::Class runtimeClass = objc_getClass(result.name.c_str()); + if (runtimeClass != nil && (uint64_t)(uintptr_t)runtimeClass == candidate) + processClassAddress = candidate; + } + + if (processClassAddress != 0) { + if (!readProtocolsFromRuntime(processClassAddress, result.protocols)) + readProtocols(fixups.resolve(roAddress + 40), result.protocols); + + std::vector methods; + if (readMethodsFromRuntime(processClassAddress, false, methods) && !methods.empty()) + result.instanceMethods.swap(methods); + else + readMethods(fixups.resolve(roAddress + 32), result.instanceMethods); + + methods.clear(); + if (readMethodsFromRuntime(processClassAddress, true, methods) && !methods.empty()) + result.classMethods.swap(methods); + else + readMetaclassMethods(address, result.classMethods); + + return true; + } + + readProtocols(fixups.resolve(roAddress + 40), result.protocols); + readMethods(fixups.resolve(roAddress + 32), result.instanceMethods); + readMetaclassMethods(address, result.classMethods); + + return true; +} + +void ObjCMetadata::readMetaclassMethods(uint64_t classAddress, std::vector& result) const { + // Class methods are the instance methods of the metaclass. + uint64_t metaclassAddress = fixups.resolve(classAddress); + if (metaclassAddress == 0) + return; + + uint64_t metaclassRo = classRoAddress(metaclassAddress); + uint8_t metaclassData[72]; + if (metaclassRo != 0 && architecture.readAtAddress(metaclassRo, metaclassData, sizeof(metaclassData)) && + (readUint32(metaclassData) & kClassFlagMeta) != 0) { + readMethods(fixups.resolve(metaclassRo + 32), result); + } +} + +bool ObjCMetadata::readCategory(uint64_t address, Category& result) const { + // category_t: name, cls, instanceMethods, classMethods, protocols, + // instanceProperties, classProperties + uint8_t category[56]; + if (!architecture.readAtAddress(address, category, sizeof(category))) + return false; + + // The name of a category_t is the one in the parentheses; the class it + // extends is the classref next to it. + result.name = readString(fixups.resolve(address)); + if (result.name.empty()) + return false; + + result.className = readClassReferenceName(address + 8); + + readProtocols(fixups.resolve(address + 32), result.protocols); + readProperties(fixups.resolve(address + 40), result.properties); + + uint64_t instanceMethods = fixups.resolve(address + 16); + uint64_t classMethods = fixups.resolve(address + 24); + result.instanceMethodCount = readListCount(instanceMethods); + result.classMethodCount = readListCount(classMethods); + + // An image dyld mapped into this process keeps its method lists in an + // encoding of its own whose offsets point into a string pool that belongs + // to no single image, so they cannot be decoded from here (see the progress + // notes for the measurements). The methods are not lost by that: the + // runtime merges every category into its class, and the listing of that + // class already contains them. + if (mappedBase() == 0) { + readMethods(instanceMethods, result.instanceMethods); + readMethods(classMethods, result.classMethods); + } + + return true; +} + +bool ObjCMetadata::readProtocol(uint64_t address, Protocol& result) const { + // protocol_t: isa, mangledName, protocols, instanceMethods, classMethods, + // optionalInstanceMethods, optionalClassMethods, + // instanceProperties, size, flags, _extendedMethodTypes, + // _demangledName, _classProperties + uint8_t protocol[96]; + if (!architecture.readAtAddress(address, protocol, sizeof(protocol))) + return false; + + result.name = readString(fixups.resolve(address + 8)); + if (result.name.empty()) + return false; + + // The same applies to the method lists of a protocol, and the runtime has + // an API that hands out a protocol by name -- so ask it. + if (mappedBase() != 0) { + Protocol fromRuntime; + if (readProtocolFromRuntime(result.name, fromRuntime)) { + result = fromRuntime; + return true; + } + } + + readProtocols(fixups.resolve(address + 16), result.protocols); + readProperties(fixups.resolve(address + 56), result.properties); + readMethods(fixups.resolve(address + 24), result.requiredInstanceMethods); + readMethods(fixups.resolve(address + 32), result.requiredClassMethods); + readMethods(fixups.resolve(address + 40), result.optionalInstanceMethods); + readMethods(fixups.resolve(address + 48), result.optionalClassMethods); + if (readUint32(protocol + 64) >= kProtocolSizeWithClassProperties) + readProperties(fixups.resolve(address + kProtocolClassPropertiesOffset), result.classProperties); + + return true; +} + +#pragma mark - Rendering + +namespace { + +/** + * "Ti,N,V_baseProperty" -> type "i", attributes "(nonatomic)". The declared + * property name comes from the property itself, the ivar name from V. + * + * The codes are the ones the runtime documents: R is a read-only property and + * & is a strong one, C a copy, W a weak one and N a non-atomic one. The + * spelling that is written out is the current one, so & becomes strong. + */ +void parsePropertyAttributes(const std::string& attributes, bool classProperty, + std::string& type, std::string& rendered) { + std::vector flags; + std::string getter; + std::string setter; + bool nonatomic = false; + bool readonly = false; + bool isWeak = false; + std::string memory; + + size_t position = 0; + while (position <= attributes.size()) { + size_t end = attributes.find(',', position); + if (end == std::string::npos) + end = attributes.size(); + std::string token = attributes.substr(position, end - position); + if (!token.empty()) { + char code = token[0]; + if (code == 'T') { + type = token.substr(1); + } else if (code == 'V') { + // The backing ivar; not shown, the property name is enough. + } else if (code == 'C') { + memory = "copy"; + } else if (code == '&') { + memory = "strong"; + } else if (code == 'R') { + readonly = true; + } else if (code == 'W') { + isWeak = true; + } else if (code == 'N') { + nonatomic = true; + } else if (code == 'G') { + getter = token.substr(1); + } else if (code == 'S') { + setter = token.substr(1); + } + } + if (end >= attributes.size()) + break; + position = end + 1; + } + + if (classProperty) + flags.push_back("class"); + if (nonatomic) + flags.push_back("nonatomic"); + if (readonly) + flags.push_back("readonly"); + if (isWeak) + flags.push_back("weak"); + else if (!memory.empty()) + flags.push_back(memory); + if (!getter.empty()) + flags.push_back("getter=" + getter); + if (!setter.empty()) + flags.push_back("setter=" + setter); + + rendered = "("; + for (unsigned int n = 0; n < flags.size(); n++) { + if (n > 0) + rendered += ", "; + rendered += flags[n]; + } + rendered += ")"; +} + +} // namespace + +void ObjCMetadata::appendProperties(std::string& result, const std::vector& properties, + bool classProperty) const { + for (unsigned int n = 0; n < properties.size(); n++) { + std::string typeEncoding; + std::string attributes; + parsePropertyAttributes(properties[n].attributes, classProperty, typeEncoding, attributes); + + std::string declaration; + size_t position = 0; + ObjCType type = ObjCTypeDecoder::decodeType(typeEncoding, position); + if (type.isEmpty()) + declaration = "id " + properties[n].name; + else + declaration = type.declaration(properties[n].name); + + result += "@property "; + result += attributes; + result += " "; + result += declaration; + result += ";\n"; + } +} + +void ObjCMetadata::appendMethods(std::string& result, const std::vector& methods, + bool classMethod) const { + for (unsigned int n = 0; n < methods.size(); n++) { + std::string declaration = ObjCTypeDecoder::decodeMethod(methods[n].name, methods[n].types, classMethod); + if (declaration.empty()) + continue; + result += declaration; + result += ";\n"; + } +} + +std::string ObjCMetadata::render(const Class& info, bool metaclass) const { + std::string result; + + if (metaclass) { + result += "// Class methods of "; + result += info.name; + result += " (_OBJC_METACLASS_$_"; + result += info.name; + result += ")\n"; + } + + result += "@interface "; + result += info.name; + if (!metaclass) { + if (info.superclassKnown && !info.superclass.empty()) + result += " : " + info.superclass; + else if (!info.isRoot) + result += " /* superclass is imported and could not be named */"; + } + if (!metaclass && !info.protocols.empty()) { + result += " <"; + appendJoined(result, info.protocols, ", "); + result += ">"; + } + result += "\n"; + + if (!metaclass && !info.ivars.empty()) { + result += "{\n"; + for (unsigned int n = 0; n < info.ivars.size(); n++) { + result += " "; + result += ObjCTypeDecoder::decodeIvar(info.ivars[n].type, info.ivars[n].name); + result += ";\n"; + } + result += "}\n"; + } + + if (!metaclass) + appendProperties(result, info.properties, false); + + if (metaclass) { + appendMethods(result, info.classMethods, true); + } else { + // A class declaration lists both, class methods first. + if (!info.classMethods.empty()) { + result += "\n/* class methods */\n"; + appendMethods(result, info.classMethods, true); + } + if (!info.instanceMethods.empty()) { + result += "\n/* instance methods */\n"; + appendMethods(result, info.instanceMethods, false); + } + } + + result += "@end\n"; + return result; +} + +std::string ObjCMetadata::renderCategory(const Category& info) const { + std::string result = "@interface "; + if (info.className.empty()) + result += "/* unknown class */"; + else + result += info.className; + result += " ("; + result += info.name; + result += ")"; + if (!info.protocols.empty()) { + result += " <"; + appendJoined(result, info.protocols, ", "); + result += ">"; + } + result += "\n"; + + appendProperties(result, info.properties, false); + + if (!info.classMethods.empty()) { + result += "\n/* class methods */\n"; + appendMethods(result, info.classMethods, true); + } else if (info.classMethodCount > 0) { + result += "\n/* "; + result += describeCount(info.classMethodCount, "class method"); + result += " that the runtime has merged into "; + result += info.className.empty() ? "the class" : info.className; + result += " */\n"; + } + + if (!info.instanceMethods.empty()) { + result += "\n/* instance methods */\n"; + appendMethods(result, info.instanceMethods, false); + } else if (info.instanceMethodCount > 0) { + result += "\n/* "; + result += describeCount(info.instanceMethodCount, "instance method"); + result += " that the runtime has merged into "; + result += info.className.empty() ? "the class" : info.className; + result += " */\n"; + } + + result += "@end\n"; + return result; +} + +std::string ObjCMetadata::renderProtocol(const Protocol& info) const { + std::string result = "@protocol "; + result += info.name; + if (!info.protocols.empty()) { + result += " <"; + appendJoined(result, info.protocols, ", "); + result += ">"; + } + result += "\n"; + + if (!info.requiredInstanceMethods.empty() || !info.requiredClassMethods.empty() || + !info.properties.empty() || !info.classProperties.empty()) { + result += "\n@required\n"; + appendProperties(result, info.properties, false); + if (!info.classProperties.empty()) { + // The properties of the protocol itself, rather than of whatever + // conforms to it, are a group of their own. + result += "\n"; + appendProperties(result, info.classProperties, true); + } + appendMethods(result, info.requiredClassMethods, true); + appendMethods(result, info.requiredInstanceMethods, false); + } + + if (!info.optionalInstanceMethods.empty() || !info.optionalClassMethods.empty()) { + result += "\n@optional\n"; + appendMethods(result, info.optionalClassMethods, true); + appendMethods(result, info.optionalInstanceMethods, false); + } + + result += "\n@end\n"; + return result; +} + +bool ObjCMetadata::hasClasses() const { + parse(); + return !classes.empty(); +} + +std::vector ObjCMetadata::getClassNames() const { + parse(); + std::vector names; + names.reserve(classes.size()); + for (unsigned int n = 0; n < classes.size(); n++) + names.push_back(classes[n]->name); + return names; +} + +bool ObjCMetadata::hasClass(const std::string& name) const { + parse(); + for (unsigned int n = 0; n < classes.size(); n++) { + if (classes[n]->name == name) + return true; + } + return false; +} + +std::vector ObjCMetadata::getCategoryNames() const { + parse(); + std::vector names; + names.reserve(categories.size()); + for (unsigned int n = 0; n < categories.size(); n++) { + std::string name = categories[n]->className; + name += "("; + name += categories[n]->name; + name += ")"; + names.push_back(name); + } + return names; +} + +std::string ObjCMetadata::getCategoryDefinition(const std::string& className, + const std::string& category) const { + parse(); + for (unsigned int n = 0; n < categories.size(); n++) { + if (categories[n]->name == category && categories[n]->className == className) + return renderCategory(*categories[n]); + } + return std::string(); +} + +std::vector ObjCMetadata::getProtocolNames() const { + parse(); + std::vector names; + names.reserve(protocols.size()); + for (unsigned int n = 0; n < protocols.size(); n++) + names.push_back(protocols[n]->name); + return names; +} + +bool ObjCMetadata::hasProtocol(const std::string& name) const { + parse(); + for (unsigned int n = 0; n < protocols.size(); n++) { + if (protocols[n]->name == name) + return true; + } + return false; +} + +std::string ObjCMetadata::getProtocolDefinition(const std::string& name) const { + parse(); + for (unsigned int n = 0; n < protocols.size(); n++) { + if (protocols[n]->name == name) + return renderProtocol(*protocols[n]); + } + return std::string(); +} + +std::string ObjCMetadata::getDefinition(const std::string& name, bool metaclass) const { + parse(); + for (unsigned int n = 0; n < classes.size(); n++) { + if (classes[n]->name == name) + return render(*classes[n], metaclass); + } + + // A "_OBJC_PROTOCOL_$_*" symbol names a protocol rather than a class, and + // the two share the name space of the symbol table. + if (!metaclass) { + std::string protocol = getProtocolDefinition(name); + if (!protocol.empty()) + return protocol; + } + return std::string(); +} + +std::string ObjCMetadata::getDefinitions() const { + parse(); + std::string result; + for (unsigned int n = 0; n < classes.size(); n++) { + if (n > 0) + result += "\n"; + result += render(*classes[n], false); + } + return result; +} + +std::string ObjCMetadata::getAllDefinitions() const { + parse(); + std::string result = getDefinitions(); + + for (unsigned int n = 0; n < categories.size(); n++) { + if (!result.empty()) + result += "\n"; + result += renderCategory(*categories[n]); + } + + for (unsigned int n = 0; n < protocols.size(); n++) { + if (!result.empty()) + result += "\n"; + result += renderProtocol(*protocols[n]); + } + + return result; +} diff --git a/MachO/objcmetadata.h b/MachO/objcmetadata.h new file mode 100644 index 0000000..e38d20e --- /dev/null +++ b/MachO/objcmetadata.h @@ -0,0 +1,165 @@ +#ifndef OBJCMETADATA_H +#define OBJCMETADATA_H + +#include "macho_global.h" +#include "machofixups.h" + +#include +#include +#include + +class MachOArchitecture; + +/** + * Reads the Objective-C metadata of a Mach-O image -- __objc_classlist and + * everything it points at -- and renders it as source declarations. + * + * Everything comes out of the image itself: no external tool is run, so it + * works for any file on disk, including one built for another platform. An + * image that only exists inside the dyld shared cache is read out of the + * mapping dyld made of it, which is also the only way to get at a system + * library that has no file of its own any more. + */ +class EXPORT ObjCMetadata +{ +public: + explicit ObjCMetadata(const MachOArchitecture& architecture); + ~ObjCMetadata(); + + /** True when the image has an Objective-C class list at all. */ + bool hasClasses() const; + + /** The names of all classes, in the order of the class list. */ + std::vector getClassNames() const; + + /** True when the image defines a class with that name. */ + bool hasClass(const std::string& name) const; + + /** + * The "@interface ... @end" declaration of one class, or an empty string + * when the image has no such class. With `metaclass` the class methods are + * listed instead of the instance methods. + */ + std::string getDefinition(const std::string& name, bool metaclass) const; + + /** The declarations of every class, separated by a blank line. */ + std::string getDefinitions() const; + + /** The names of the categories of the image, as "Class(Category)". */ + std::vector getCategoryNames() const; + + /** The "@interface Class (Category)" declaration of one category. */ + std::string getCategoryDefinition(const std::string& className, + const std::string& category) const; + + /** The names of the protocols the image declares itself. */ + std::vector getProtocolNames() const; + + /** True when the image declares a protocol with that name. */ + bool hasProtocol(const std::string& name) const; + + /** The "@protocol ... @end" declaration of one protocol. */ + std::string getProtocolDefinition(const std::string& name) const; + + /** + * Every declaration of the image: classes, then categories, then the + * protocols it declares itself, separated by a blank line. + */ + std::string getAllDefinitions() const; + +private: + struct Class; + struct Category; + struct Protocol; + struct Method; + struct Ivar; + struct Property; + + // Not copyable: the parsed classes are owned. + ObjCMetadata(const ObjCMetadata&); + ObjCMetadata& operator=(const ObjCMetadata&); + + void parse() const; + bool readClass(uint64_t address, Class& result) const; + bool readCategory(uint64_t address, Category& result) const; + bool readProtocol(uint64_t address, Protocol& result) const; + void readMethods(uint64_t listAddress, std::vector& result) const; + + /** Number of entries a method / property list header claims, or 0. */ + uint32_t readListCount(uint64_t listAddress) const; + + /** The class methods, i.e. the instance methods of the metaclass. */ + void readMetaclassMethods(uint64_t classAddress, std::vector& result) const; + void readIvars(uint64_t listAddress, std::vector& result) const; + void readProperties(uint64_t listAddress, std::vector& result) const; + void readProtocols(uint64_t listAddress, std::vector& result) const; + std::string readString(uint64_t address) const; + std::string readClassName(uint64_t classAddress) const; + + /** + * Name of the class a classref points at, whatever way the pointer is + * encoded: a bind, a pointer into a mapped image, or a plain address. + */ + std::string readClassReferenceName(uint64_t slotAddress) const; + + std::string render(const Class& info, bool metaclass) const; + std::string renderCategory(const Category& info) const; + std::string renderProtocol(const Protocol& info) const; + + /** + * "@property (nonatomic) NSString *name;" lines, one per property. With + * `classProperty` the class keyword is added, as in + * "@property (class, readonly) NSString *name;". + */ + void appendProperties(std::string& result, const std::vector& properties, + bool classProperty) const; + + /** "- (void)foo;" / "+ (id)bar;" lines, one per method. */ + void appendMethods(std::string& result, const std::vector& methods, + bool classMethod) const; + + /** + * Address of the class_ro_t of the class at `classAddress`, in the unslid + * address space of the image. + */ + uint64_t classRoAddress(uint64_t classAddress) const; + + /** + * Unslid address of the class_ro_t behind a class_data_bits_t::bits value + * of an image that dyld mapped into this process. + */ + uint64_t decodeMappedClassData(uint64_t bits) const; + + /** + * Method lists of an image that dyld mapped into this process, taken from + * the runtime rather than from the image. Returns false when the runtime + * does not know the class. + */ + bool readMethodsFromRuntime(uint64_t processClassAddress, bool metaclass, + std::vector& result) const; + + /** Protocol names of such a class, taken from the runtime. */ + bool readProtocolsFromRuntime(uint64_t processClassAddress, + std::vector& result) const; + + /** + * Method list of one protocol, taken from the runtime. Returns false when + * the runtime does not know the protocol. + */ + bool readProtocolFromRuntime(const std::string& name, Protocol& result) const; + + /** True when a protocol of that name was already read. */ + bool hasProtocolNamed(const std::string& name) const; + + /** The image as dyld mapped it, or 0 when it was read from disk. */ + const uint8_t* mappedBase() const; + + const MachOArchitecture& architecture; + MachOFixups fixups; + mutable bool parsed; + mutable std::vector classes; + mutable std::vector categories; + mutable std::vector protocols; +}; + +#endif // OBJCMETADATA_H diff --git a/MachO/objctypedecoder.cpp b/MachO/objctypedecoder.cpp new file mode 100644 index 0000000..34795d3 --- /dev/null +++ b/MachO/objctypedecoder.cpp @@ -0,0 +1,279 @@ +#include "objctypedecoder.h" + +#include +#include + +namespace { + +bool isDigit(char character) { + return character >= '0' && character <= '9'; +} + +// r = const, n = in, N = out, o = inout, O = bycopy, R = byref, V = oneway. +// None of those is a type code itself. +bool isQualifier(char character) { + return character == 'r' || character == 'n' || character == 'N' || character == 'o' || + character == 'O' || character == 'R' || character == 'V'; +} + +/** The frame size / offset digits that may follow a type. */ +void skipDigits(const std::string& encoding, size_t& position) { + while (position < encoding.size() && isDigit(encoding[position])) + position++; +} + +/** + * Skips the body of a struct or union whose opening brace was already + * consumed, leaving `position` behind the matching closing brace. + */ +void skipBody(const std::string& encoding, size_t& position, char open, char close) { + int depth = 0; + while (position < encoding.size()) { + char character = encoding[position]; + if (character == open) { + depth++; + } else if (character == close) { + if (depth == 0) { + position++; + return; + } + depth--; + } + position++; + } +} + +/** Everything up to the field list ('=') or the closing brace. */ +std::string readName(const std::string& encoding, size_t& position, char close) { + std::string name; + while (position < encoding.size() && encoding[position] != '=' && encoding[position] != close) { + name.push_back(encoding[position]); + position++; + } + return name; +} + +ObjCType applyQualifiers(const ObjCType& type, const std::string& qualifiers) { + if (qualifiers.empty()) + return type; + return ObjCType(qualifiers + type.prefix, type.suffix); +} + +std::string argumentName(size_t index) { + char buffer[32]; + snprintf(buffer, sizeof(buffer), "arg%lu", (unsigned long)index); + return std::string(buffer); +} + +} // namespace + +std::string ObjCType::declaration(const std::string& name) const { + std::string result = prefix + name + suffix; + // Without a name the type would otherwise end in the space of the prefix. + while (name.empty() && !result.empty() && result[result.size() - 1] == ' ') + result.erase(result.size() - 1); + return result; +} + +ObjCType ObjCTypeDecoder::decodeType(const std::string& encoding, size_t& position) { + std::string qualifiers; + while (position < encoding.size() && isQualifier(encoding[position])) { + if (encoding[position] == 'r') + qualifiers = "const "; + position++; + } + if (position >= encoding.size()) + return ObjCType(); + + char code = encoding[position]; + + switch (code) { + case 'c': position++; return applyQualifiers(ObjCType("char ", ""), qualifiers); + case 'C': position++; return applyQualifiers(ObjCType("unsigned char ", ""), qualifiers); + case 's': position++; return applyQualifiers(ObjCType("short ", ""), qualifiers); + case 'S': position++; return applyQualifiers(ObjCType("unsigned short ", ""), qualifiers); + case 'i': position++; return applyQualifiers(ObjCType("int ", ""), qualifiers); + case 'I': position++; return applyQualifiers(ObjCType("unsigned int ", ""), qualifiers); + case 'l': position++; return applyQualifiers(ObjCType("long ", ""), qualifiers); + case 'L': position++; return applyQualifiers(ObjCType("unsigned long ", ""), qualifiers); + case 'q': position++; return applyQualifiers(ObjCType("long long ", ""), qualifiers); + case 'Q': position++; return applyQualifiers(ObjCType("unsigned long long ", ""), qualifiers); + case 'f': position++; return applyQualifiers(ObjCType("float ", ""), qualifiers); + case 'd': position++; return applyQualifiers(ObjCType("double ", ""), qualifiers); + case 'D': position++; return applyQualifiers(ObjCType("long double ", ""), qualifiers); + case 'B': position++; return applyQualifiers(ObjCType("bool ", ""), qualifiers); + case 'v': position++; return applyQualifiers(ObjCType("void ", ""), qualifiers); + case '*': position++; return applyQualifiers(ObjCType("char *", ""), qualifiers); + case ':': position++; return applyQualifiers(ObjCType("SEL ", ""), qualifiers); + case '#': position++; return applyQualifiers(ObjCType("Class ", ""), qualifiers); + case '?': position++; return applyQualifiers(ObjCType("id ", ""), qualifiers); + + case '@': { + position++; + if (position < encoding.size() && encoding[position] == '?') { + position++; + // The signature of a block is not part of the encoding. + return applyQualifiers(ObjCType("id /* block */ ", ""), qualifiers); + } + if (position < encoding.size() && encoding[position] == '"') { + position++; + std::string name; + while (position < encoding.size() && encoding[position] != '"') + name.push_back(encoding[position++]); + if (position < encoding.size()) + position++; + if (name.empty() || name == "?") + return applyQualifiers(ObjCType("id ", ""), qualifiers); + return applyQualifiers(ObjCType(name + " *", ""), qualifiers); + } + return applyQualifiers(ObjCType("id ", ""), qualifiers); + } + + case '^': { + position++; + if (position < encoding.size() && encoding[position] == '?') { + position++; + return applyQualifiers(ObjCType("void *", ""), qualifiers); + } + ObjCType pointee = decodeType(encoding, position); + if (pointee.isEmpty()) + return applyQualifiers(ObjCType("void *", ""), qualifiers); + return applyQualifiers(ObjCType(pointee.prefix + "*", pointee.suffix), qualifiers); + } + + case 'b': { + position++; + skipDigits(encoding, position); + return applyQualifiers(ObjCType("unsigned int ", ""), qualifiers); + } + + case 'j': { + // _Complex: the encoding names the underlying type afterwards. + position++; + ObjCType component = decodeType(encoding, position); + return applyQualifiers(component, qualifiers); + } + + case '{': + case '(': { + char close = (code == '{') ? '}' : ')'; + std::string keyword = (code == '{') ? "struct" : "union"; + position++; + std::string name = readName(encoding, position, close); + if (!name.empty() && name != "?") { + skipBody(encoding, position, code, close); + return applyQualifiers(ObjCType(name + " ", ""), qualifiers); + } + // A struct or union without a name of its own (the encoding uses + // '?') is written out inline so that the declaration stays valid. + std::string body = decodeAnonymousBody(encoding, position, close); + if (body.empty()) + return applyQualifiers(ObjCType(keyword + " ", ""), qualifiers); + return applyQualifiers(ObjCType(keyword + " { " + body + " } ", ""), qualifiers); + } + + case '[': { + position++; + std::string count; + while (position < encoding.size() && isDigit(encoding[position])) + count.push_back(encoding[position++]); + ObjCType element = decodeType(encoding, position); + if (position < encoding.size() && encoding[position] == ']') + position++; + if (element.isEmpty()) + return applyQualifiers(ObjCType("", "[" + count + "]"), qualifiers); + return applyQualifiers(ObjCType(element.prefix, "[" + count + "]" + element.suffix), qualifiers); + } + + default: + position++; + return applyQualifiers(ObjCType("id ", ""), qualifiers); + } +} + +std::string ObjCTypeDecoder::decodeAnonymousBody(const std::string& encoding, size_t& position, char close) { + std::string body; + if (position < encoding.size() && encoding[position] == '=') + position++; + + while (position < encoding.size() && encoding[position] != close) { + std::string fieldName; + if (encoding[position] == '"') { + position++; + while (position < encoding.size() && encoding[position] != '"') + fieldName.push_back(encoding[position++]); + if (position < encoding.size()) + position++; + } + ObjCType field = decodeType(encoding, position); + if (field.isEmpty()) + break; + if (!body.empty()) + body += " "; + body += field.declaration(fieldName); + body += ";"; + } + if (position < encoding.size() && encoding[position] == close) + position++; + return body; +} + +std::string ObjCTypeDecoder::decodeIvar(const std::string& encoding, const std::string& name) { + size_t position = 0; + ObjCType type = decodeType(encoding, position); + if (type.isEmpty()) { + // A few ivars carry no type encoding at all; "id" is the safe guess. + if (encoding.empty()) + return "id " + name; + return encoding + " " + name; + } + return type.declaration(name); +} + +std::string ObjCTypeDecoder::decodeMethod(const std::string& selector, const std::string& encoding, bool isClassMethod) { + if (encoding.empty()) + return std::string(); + + size_t position = 0; + ObjCType returnType = decodeType(encoding, position); + // An unreadable return type is no reason to hide the method. + if (returnType.isEmpty()) + returnType = ObjCType("id ", ""); + + skipDigits(encoding, position); + // The first two arguments are always self and _cmd. + decodeType(encoding, position); + skipDigits(encoding, position); + decodeType(encoding, position); + skipDigits(encoding, position); + std::vector parts; + std::string current; + for (size_t n = 0; n < selector.size(); n++) { + if (selector[n] == ':') { + parts.push_back(current); + current.clear(); + } else { + current.push_back(selector[n]); + } + } + + std::string result = isClassMethod ? "+ (" : "- ("; + result += returnType.declaration(""); + result += ")"; + + if (parts.empty()) { + result += selector; + return result; + } + + for (size_t n = 0; n < parts.size(); n++) { + ObjCType argument = decodeType(encoding, position); + skipDigits(encoding, position); + if (argument.isEmpty()) + argument = ObjCType("id ", ""); + if (n > 0) + result += " "; + result += parts[n] + ":(" + argument.declaration(argumentName(n + 1)) + ")"; + } + return result; +} diff --git a/MachO/objctypedecoder.h b/MachO/objctypedecoder.h new file mode 100644 index 0000000..f653296 --- /dev/null +++ b/MachO/objctypedecoder.h @@ -0,0 +1,62 @@ +#ifndef OBJCTYPEDECODER_H +#define OBJCTYPEDECODER_H + +#include "macho_global.h" + +#include +#include + +/** + * Turns Objective-C type encodings into something readable. + * + * A type is decoded into a prefix and a suffix so that a declaration can be + * built as `prefix + name + suffix`. That is what makes `char _name[32]` + * possible, which a plain string concatenation could not express: + * + * "i" -> prefix "int ", suffix "" + * "^i" -> prefix "int *", suffix "" + * "[32c]" -> prefix "char ", suffix "[32]" + * "@\"NSString\"" -> prefix "NSString *", suffix "" + */ +class EXPORT ObjCType +{ +public: + ObjCType() {} + ObjCType(const std::string& aPrefix, const std::string& aSuffix) : prefix(aPrefix), suffix(aSuffix) {} + + /** Builds the declaration, e.g. "char _name[32]". */ + std::string declaration(const std::string& name) const; + + bool isEmpty() const { return prefix.empty() && suffix.empty(); } + + std::string prefix; + std::string suffix; +}; + +class EXPORT ObjCTypeDecoder +{ +public: + /** + * Decodes the type starting at `position`, which is advanced past it. + * An empty result means the encoding could not be understood. + */ + static ObjCType decodeType(const std::string& encoding, size_t& position); + + /** + * Decodes the field list of a struct or union that has no name of its own + * ("{?={_NSRange=QQ}QQ}"). Such a type is rendered inline, which keeps the + * declaration valid C. + */ + static std::string decodeAnonymousBody(const std::string& encoding, size_t& position, char close); + + /** Decodes an ivar type encoding ("[32c]") into "char _name[32]". */ + static std::string decodeIvar(const std::string& encoding, const std::string& name); + + /** + * Decodes a method type encoding such as "v32@0:8q16@24" into a readable + * declaration such as "- (void)doThing:(long long)arg1 with:(id)arg2". + */ + static std::string decodeMethod(const std::string& selector, const std::string& encoding, bool isClassMethod); +}; + +#endif // OBJCTYPEDECODER_H diff --git a/MachO/segmentcommand.cpp b/MachO/segmentcommand.cpp new file mode 100644 index 0000000..6904978 --- /dev/null +++ b/MachO/segmentcommand.cpp @@ -0,0 +1,118 @@ +#include "segmentcommand.h" +#include "machoheader.h" +#include "machofile.h" + +#include + +#include + +namespace { + +/** Segment and section names are fixed size and not necessarily terminated. */ +std::string fixedString(const char* data, size_t size) { + size_t length = 0; + while (length < size && data[length] != '\0') + length++; + return std::string(data, length); +} + +} // namespace + +Section::Section() : address(0), size(0), offset(0) { +} + +Section::Section(const char* segmentName, const char* sectionName, uint64_t address, uint64_t size, uint32_t offset) : + segmentName(fixedString(segmentName, 16)), name(fixedString(sectionName, 16)), + address(address), size(size), offset(offset) +{ +} + +SegmentCommand::SegmentCommand(MachOHeader* header) : + LoadCommand(header), vmAddress(0), vmSize(0), fileOffset(0), fileSize(0), commandSize(0) +{ + // The file is positioned on the command itself, so the whole structure can + // be read in one go. + uint32_t cmd = file.readUint32(); + if (cmd == LC_SEGMENT_64) { + struct segment_command_64 segment; + file.seek(offset); + file.readBytes((char*)&segment, sizeof(segment)); + name = fixedString(segment.segname, sizeof(segment.segname)); + vmAddress = file.getUint64(segment.vmaddr); + vmSize = file.getUint64(segment.vmsize); + fileOffset = file.getUint64(segment.fileoff); + fileSize = file.getUint64(segment.filesize); + commandSize = file.getUint32(segment.cmdsize); + readSections(file.getUint32(segment.nsects), true); + } else { + struct segment_command segment; + file.seek(offset); + file.readBytes((char*)&segment, sizeof(segment)); + name = fixedString(segment.segname, sizeof(segment.segname)); + vmAddress = file.getUint32(segment.vmaddr); + vmSize = file.getUint32(segment.vmsize); + fileOffset = file.getUint32(segment.fileoff); + fileSize = file.getUint32(segment.filesize); + commandSize = file.getUint32(segment.cmdsize); + readSections(file.getUint32(segment.nsects), false); + } +} + +void SegmentCommand::readSections(unsigned int numberOfSections, bool is64Bit) { + unsigned int structureSize = is64Bit ? sizeof(struct segment_command_64) : sizeof(struct segment_command); + unsigned int sectionSize = is64Bit ? sizeof(struct section_64) : sizeof(struct section); + + // A malformed command could claim an absurd number of sections; the + // remaining bytes of the command are the only ones that can hold them. + if (commandSize > structureSize) { + unsigned int available = (commandSize - structureSize) / sectionSize; + if (numberOfSections > available) + numberOfSections = available; + } else { + numberOfSections = 0; + } + + for (unsigned int n = 0; n < numberOfSections; n++) { + if (is64Bit) { + struct section_64 section; + file.readBytes((char*)§ion, sizeof(section)); + sections.push_back(Section(section.segname, section.sectname, + file.getUint64(section.addr), file.getUint64(section.size), + file.getUint32(section.offset))); + } else { + struct section section; + file.readBytes((char*)§ion, sizeof(section)); + sections.push_back(Section(section.segname, section.sectname, + file.getUint32(section.addr), file.getUint32(section.size), + file.getUint32(section.offset))); + } + } +} + +bool SegmentCommand::containsAddress(uint64_t address) const { + // Only the part that is backed by file bytes can be read; a segment may be + // larger in memory than in the file (__PAGEZERO, zero filled tails). + return fileSize > 0 && address >= vmAddress && address < vmAddress + fileSize; +} + +long long SegmentCommand::getFileOffsetForAddress(uint64_t address) const { + if (!containsAddress(address)) + return -1; + return (long long)(fileOffset + (address - vmAddress)); +} + +const Section* SegmentCommand::findSection(const std::string& segmentName, const std::string& sectionName) const { + for (unsigned int n = 0; n < sections.size(); n++) { + if (sections[n].getName() == sectionName && sections[n].getSegmentName() == segmentName) + return §ions[n]; + } + return 0; +} + +const Section* SegmentCommand::findSection(const std::string& sectionName) const { + for (unsigned int n = 0; n < sections.size(); n++) { + if (sections[n].getName() == sectionName) + return §ions[n]; + } + return 0; +} diff --git a/MachO/segmentcommand.h b/MachO/segmentcommand.h new file mode 100644 index 0000000..974d1db --- /dev/null +++ b/MachO/segmentcommand.h @@ -0,0 +1,79 @@ +#ifndef SEGMENTCOMMAND_H +#define SEGMENTCOMMAND_H + +#include "loadcommand.h" + +#include +#include + +/** + * One section of a segment: where it lives in the address space and where the + * very same bytes live in the file. + */ +class Section +{ +public: + Section(); + Section(const char* segmentName, const char* sectionName, uint64_t address, uint64_t size, uint32_t offset); + + const std::string& getSegmentName() const { return segmentName; } + const std::string& getName() const { return name; } + uint64_t getAddress() const { return address; } + uint64_t getSize() const { return size; } + /** Offset relative to the beginning of the Mach-O image. */ + uint32_t getOffset() const { return offset; } + bool contains(uint64_t vmAddress) const { return vmAddress >= address && vmAddress < address + size; } + +private: + std::string segmentName; + std::string name; + uint64_t address; + uint64_t size; + uint32_t offset; +}; + +/** + * LC_SEGMENT / LC_SEGMENT_64. Keeps the segment range plus the sections it + * contains, which is everything needed to translate an address into a file + * offset (and the other way round). + */ +class EXPORT SegmentCommand : public LoadCommand +{ +public: + SegmentCommand(MachOHeader* header); + + virtual unsigned int getSize() const { return commandSize; } + + const std::string& getName() const { return name; } + uint64_t getVMAddress() const { return vmAddress; } + uint64_t getVMSize() const { return vmSize; } + uint64_t getFileOffset() const { return fileOffset; } + uint64_t getFileSize() const { return fileSize; } + + unsigned int getNumberOfSections() const { return (unsigned int)sections.size(); } + const Section& getSection(unsigned int index) const { return sections[index]; } + + /** The first section matching both names, or 0. */ + const Section* findSection(const std::string& segmentName, const std::string& sectionName) const; + /** The first section with that name, in any segment, or 0. */ + const Section* findSection(const std::string& sectionName) const; + + /** True when the address is backed by bytes in the file. */ + bool containsAddress(uint64_t vmAddress) const; + + /** File offset of `vmAddress`, relative to the beginning of the Mach-O image. */ + long long getFileOffsetForAddress(uint64_t vmAddress) const; + +private: + void readSections(unsigned int numberOfSections, bool is64Bit); + + std::string name; + uint64_t vmAddress; + uint64_t vmSize; + uint64_t fileOffset; + uint64_t fileSize; + unsigned int commandSize; + std::vector
sections; +}; + +#endif // SEGMENTCOMMAND_H diff --git a/MachO/symboltableentry.cpp b/MachO/symboltableentry.cpp index ffbc361..53f1a1f 100644 --- a/MachO/symboltableentry.cpp +++ b/MachO/symboltableentry.cpp @@ -2,6 +2,123 @@ #include "macho.h" #include "machofile.h" +#include +#include + +namespace { + +typedef char* (*SwiftDemangleFunction)(const char* mangledName, size_t mangledNameLength, + char* outputBuffer, size_t* outputBufferSize, + uint32_t flags); + +/** + * The demangler of the Swift runtime, loaded on demand. + * + * A name is demangled with it only when a Swift name turns up, so an image + * without Swift code never pays for the runtime. The host process is asked + * first, because an application that has Swift in it already has the runtime. + */ +SwiftDemangleFunction loadSwiftDemangle() { + SwiftDemangleFunction demangle = + (SwiftDemangleFunction)dlsym(RTLD_DEFAULT, "swift_demangle"); + if (demangle != 0) + return demangle; + + void* runtime = dlopen("/usr/lib/swift/libswiftCore.dylib", RTLD_LAZY | RTLD_LOCAL); + if (runtime == 0) + return 0; + return (SwiftDemangleFunction)dlsym(runtime, "swift_demangle"); +} + +SwiftDemangleFunction swiftDemangle() { + static SwiftDemangleFunction demangle = loadSwiftDemangle(); + return demangle; +} + +/** True for the prefixes of the Swift mangling: "$s", "$S" and the older "_T". */ +bool isSwiftMangledName(const std::string& name) { + if (name.size() < 2) + return false; + return name.compare(0, 2, "$s") == 0 || name.compare(0, 2, "$S") == 0 || + name.compare(0, 2, "_T") == 0; +} + +/** + * The prefixes the Objective-C metadata symbols are named with, and what each + * one stands for. + * + * The names come from the assembler, so they carry the leading underscore of + * the C naming convention -- and the protocol ones carry two of them, which is + * how every image measured so far spells them. Both spellings are listed, so + * that an image spelling them the other way is read the same. + */ +const struct { + const char* prefix; + SymbolTableEntry::Kind kind; +} kObjCSymbolPrefixes[] = { + { "_OBJC_CLASS_$_", SymbolTableEntry::KindObjCClass }, + { "_OBJC_METACLASS_$_", SymbolTableEntry::KindObjCMetaclass }, + { "_OBJC_PROTOCOL_$_", SymbolTableEntry::KindObjCProtocol }, + { "__OBJC_PROTOCOL_$_", SymbolTableEntry::KindObjCProtocol }, + { "_OBJC_IVAR_$_", SymbolTableEntry::KindObjCIvar }, +}; + +const unsigned int kObjCSymbolPrefixCount = sizeof(kObjCSymbolPrefixes) / sizeof(*kObjCSymbolPrefixes); + +/** The kind of an Objective-C metadata symbol, or NumKinds for any other name. */ +SymbolTableEntry::Kind objcSymbolKind(const std::string& name) { + for (unsigned int n = 0; n < kObjCSymbolPrefixCount; n++) { + if (name.compare(0, strlen(kObjCSymbolPrefixes[n].prefix), kObjCSymbolPrefixes[n].prefix) == 0) + return kObjCSymbolPrefixes[n].kind; + } + + // Everything else the runtime emits is named "_OBJC__$_", the + // exception type information of a class for instance. The tables the + // compiler keeps inside an object file carry one underscore more. + if (name.compare(0, 6, "_OBJC_") == 0 || name.compare(0, 7, "__OBJC_") == 0) + return SymbolTableEntry::KindObjCOther; + + return SymbolTableEntry::NumKinds; +} + +/** + * The class, protocol or ivar name a metadata symbol stands for, or an empty + * string when the symbol is not one of the metadata symbols. + */ +std::string objcSymbolName(const std::string& name) { + for (unsigned int n = 0; n < kObjCSymbolPrefixCount; n++) { + size_t length = strlen(kObjCSymbolPrefixes[n].prefix); + if (name.compare(0, length, kObjCSymbolPrefixes[n].prefix) == 0) + return name.substr(length); + } + return std::string(); +} + +/** + * The readable form of a Swift name, or an empty string. + * + * The demangler writes into a buffer of the caller's, and a name with a lot of + * generics in it needs more room than the first size offered, so a second, + * larger one is tried before giving up. + */ +std::string demangleSwiftName(const std::string& name) { + SwiftDemangleFunction demangle = swiftDemangle(); + if (demangle == 0) + return std::string(); + + static const size_t sizes[] = { 4096, 65536 }; + for (unsigned int n = 0; n < sizeof(sizes) / sizeof(*sizes); n++) { + std::vector buffer(sizes[n]); + size_t size = buffer.size(); + char* result = demangle(name.c_str(), name.size(), &buffer[0], &size, 0); + if (result != 0) + return std::string(result); + } + return std::string(); +} + +} // namespace + SymbolTableEntry::SymbolTableEntry(MachOFile& file, char* stringTable) : file(file), stringTable(stringTable) { @@ -13,17 +130,64 @@ SymbolTableEntry::~SymbolTableEntry() { std::string SymbolTableEntry::getName(bool shouldDemangle) const { const char *name = getInternalName(); - std::string result = name; - if (shouldDemangle) { - int status = 0; - // Convert real name to readable string. +1 to skip the leading underscore. - char *realName = abi::__cxa_demangle(name + 1, nullptr, nullptr, &status); - if (realName != nullptr) - result = realName; - free(realName); + if (!shouldDemangle) + return name; + + // The metadata symbols of the Objective-C runtime spell out what they name, + // and the name of the class, protocol or ivar is the part worth reading: + // "_OBJC_CLASS_$_NSString" reads "NSString". + std::string objcName = objcSymbolName(name); + if (!objcName.empty()) + return objcName; + + // A name in the symbol table carries the leading underscore of the C naming + // convention, and that underscore is not part of the name. What is left is + // either mangled -- and then a demangler says what it is -- or a C name, + // which is what it is once the underscore is gone. + if (name[0] != '_' || name[1] == '\0') + return name; + + std::string mangled = name + 1; + + // C++ first: the Itanium demangler is the one that ships with the C++ + // library, and it is not confused by anything else. + int status = 0; + char *cxxName = abi::__cxa_demangle(mangled.c_str(), nullptr, nullptr, &status); + if (cxxName != nullptr) { + std::string result = cxxName; + free(cxxName); + return result; } - return result; + + // Swift names have a demangler of their own, and it is not the C++ one. + if (isSwiftMangledName(mangled)) { + std::string swiftName = demangleSwiftName(mangled); + if (!swiftName.empty()) + return swiftName; + } + + return mangled; +} + +SymbolTableEntry::Kind SymbolTableEntry::getKind() const { + std::string name = getInternalName(); + + Kind objc = objcSymbolKind(name); + if (objc != NumKinds) + return objc; + + // Both manglings sit behind the leading underscore of the C naming + // convention, so it is dropped before they are looked for. + if (name.size() > 1 && name[0] == '_') { + std::string mangled = name.substr(1); + if (mangled.compare(0, 2, "_Z") == 0) + return KindCXX; + if (isSwiftMangledName(mangled)) + return KindSwift; + } + + return KindC; } SymbolTableEntry::Type SymbolTableEntry::getType() const { diff --git a/MachO/symboltableentry.h b/MachO/symboltableentry.h index c696fba..29114fc 100644 --- a/MachO/symboltableentry.h +++ b/MachO/symboltableentry.h @@ -2,6 +2,7 @@ #define SYMBOLTABLEENTRY_H #include "macho_global.h" +#include #include class MachOFile; @@ -10,6 +11,16 @@ class EXPORT SymbolTableEntry public: SymbolTableEntry(MachOFile& file, char* stringTable); virtual ~SymbolTableEntry(); + + /** + * The name of the symbol, demangled when asked for. + * + * Demangling is what makes a name readable: the leading underscore of the C + * naming convention goes, the mangled C++ and Swift names are turned into + * what they were written as, and the Objective-C metadata symbols lose the + * prefix that spells out their kind, so that "_OBJC_CLASS_$_NSString" reads + * "NSString". + */ std::string getName(bool shouldDemangle) const; enum Type { @@ -22,9 +33,38 @@ class EXPORT SymbolTableEntry }; Type getType() const; + + /** + * What the symbol is, as far as its name says. + * + * The symbol table records no language, and for most symbols the name is all + * there is to go on: the prefixes of the two manglings and of the + * Objective-C metadata are what tell the kinds apart. + */ + enum Kind { + KindC = 0, + KindCXX, + KindSwift, + KindObjCClass, + KindObjCMetaclass, + KindObjCProtocol, + KindObjCIvar, + KindObjCOther, + NumKinds + }; + + Kind getKind() const; virtual unsigned int getInternalType() const = 0; virtual const char* getInternalName() const = 0; + /** + * The ordinal of the library an imported symbol is bound to, for symbols + * that use the two level namespace: 1 is the first library of the load + * commands, 2 the second, and so on. Zero when the symbol is not bound to + * one library, which is the case for everything the image defines itself. + */ + virtual unsigned int getLibraryOrdinal() const = 0; + protected: MachOFile& file; char* stringTable; diff --git a/MachO/symboltableentry32.cpp b/MachO/symboltableentry32.cpp index fdd5d5a..b3d6143 100644 --- a/MachO/symboltableentry32.cpp +++ b/MachO/symboltableentry32.cpp @@ -14,4 +14,8 @@ unsigned int SymbolTableEntry32::getInternalType() const { return entry->n_type; } +unsigned int SymbolTableEntry32::getLibraryOrdinal() const { + return GET_LIBRARY_ORDINAL(file.getUint16(entry->n_desc)); +} + diff --git a/MachO/symboltableentry32.h b/MachO/symboltableentry32.h index 668cc50..c07994c 100644 --- a/MachO/symboltableentry32.h +++ b/MachO/symboltableentry32.h @@ -9,6 +9,7 @@ class SymbolTableEntry32 : public SymbolTableEntry SymbolTableEntry32(MachOFile& file, struct nlist* entry, char* stringTable); virtual const char* getInternalName() const; virtual unsigned int getInternalType() const; + virtual unsigned int getLibraryOrdinal() const; private: struct nlist* entry; }; diff --git a/MachO/symboltableentry64.cpp b/MachO/symboltableentry64.cpp index e68620b..245b94c 100644 --- a/MachO/symboltableentry64.cpp +++ b/MachO/symboltableentry64.cpp @@ -14,3 +14,7 @@ unsigned int SymbolTableEntry64::getInternalType() const { return entry->n_type; } +unsigned int SymbolTableEntry64::getLibraryOrdinal() const { + return GET_LIBRARY_ORDINAL(file.getUint16(entry->n_desc)); +} + diff --git a/MachO/symboltableentry64.h b/MachO/symboltableentry64.h index 9f1bca9..459ba6b 100644 --- a/MachO/symboltableentry64.h +++ b/MachO/symboltableentry64.h @@ -9,6 +9,7 @@ class SymbolTableEntry64 : public SymbolTableEntry SymbolTableEntry64(MachOFile& file, struct nlist_64* entry, char* stringTable); virtual const char* getInternalName() const; virtual unsigned int getInternalType() const; + virtual unsigned int getLibraryOrdinal() const; private: struct nlist_64* entry; }; diff --git a/README.md b/README.md index d8cb6a8..8ff39ff 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,195 @@ -[![License](https://img.shields.io/github/license/kwin/macdependency)](https://www.gnu.org/licenses/gpl-3.0.en.html) -[![Build Status](https://github.com/kwin/macdependency/actions/workflows/build.yml/badge.svg)](https://github.com/kwin/macdependency/actions/workflows/maven.yml) +![License](https://img.shields.io/github/license/kwin/macdependency) -MacDependency shows all dependent libraries and frameworks of a given executable, dynamic library or framework on Mac OS X. It is a GUI replacement for the otool command, and provides almost the same functionality as the Dependency Walker (http://www.dependencywalker.com) on Windows. +![Build Status](https://github.com/kwin/macdependency/actions/workflows/build.yml/badge.svg) + +MacDependency shows all dependent libraries and frameworks of a given executable, dynamic library or framework on Mac OS X. It is a GUI replacement for the otool command, and provides almost the same functionality as the Dependency Walker () on Windows. More information available in the [Wiki](../../wiki). -![Screenshot](images/macdependency.png) +![Screenshot](images/macdependency.jpg) + +## This fork + +This repository is a fork of [kwin/macdependency](https://github.com/kwin/macdependency). +It keeps everything the original does and adds: + +- the Objective-C metadata of an image, printed as a declaration; +- the system libraries that exist only in the **dyld shared cache**; +- a **Kind** column in the symbol table, with filters on the headers of the + Type and Kind columns; +- a **File Kind** column in the dependency tree, next to the renamed **Link Type** column; +- a log that is readable in dark mode and read-only. + +Below, **this fork** marks a feature that only exists here. + +## What it does + +- Opens an executable, a dynamic library or a framework and shows everything it + depends on as a tree, with the install name, how the dependency is linked, what + the file itself is, and the current and compatible version of every entry. +- For the selected entry it shows what the load command behind it says: file name, + file type, ID, size, UUID, architecture, last modified date, RPaths and the dynamic + linker it was linked against. *Reveal in Finder* jumps to the file. +- Reports what is wrong with the link — dependencies that cannot be found, version + mismatches, name mismatches, architecture mismatches — in the *Warnings & Errors* + tab, where every line links to the dependency it concerns. +- Switches between the architectures of a universal binary. +- Searches the symbol table by name, and narrows it down to one type or one kind + of symbol. + +On top of that, MacDependency takes the Objective-C metadata and the symbol table of +an image apart, which is what the rest of this document is about. + +## The dependency tree + +| Name (Install Name) | Link Type | File Kind | Current Version | Compatible Version | +| ------------------- | --------- | --------------- | --------------- | ------------------ | +| DVTFoundation | Normal | Dynamic Library | 1.0.0 | 1.0.0 | +| Foundation | Normal | Shared Cache | 2000.0.0 | 150.0.0 | +| LoggingSupport | Weak | Shared Cache | 1.0.0 | 1.0.0 | + +- **Link Type** — how the dependency is linked, i.e. which of the load commands + brought it in: `Normal` (`LC_LOAD_DYLIB`), `Weak` (`LC_LOAD_WEAK_DYLIB`, the + dependency is allowed to be missing) or `Delayed` (`LC_LAZY_LOAD_DYLIB`). + *This fork:* the column used to be called simply *Type*, which said nothing + about what it was a type of, now that a second column describes the file. +- **File Kind** — what the file itself is. *This fork.* + - `Executable File` — `MH_EXECUTE`; + - `Dynamic Library` — `MH_DYLIB`, and the other things that are loaded into + another process: bundles, stubs, kext bundles, the dynamic linker; + - `Shared Cache` — the image has no file on disk at all, it was read out of the + dyld shared cache (see below); + - `Unknown` — nothing was read for the row, because the library is missing or + has no architecture in common with its parent. +- The **Name** column is the one that takes up the slack when the list is resized; + the four columns after it keep the width they were given. +- A row is drawn in the ordinary text colour, in red when the dependency could not + be resolved and in yellow when it is only a warning. + +## Objective-C class definitions + +*This fork.* MacDependency reads the Objective-C metadata out of the Mach-O image +itself and prints it as a declaration, so a symbol like `_OBJC_CLASS_$_NSView` can +be turned back into the interface it came from: + +```objc +@interface NSView : NSResponder +{ + _NSTrackingAreaViewHelper *_trackingAreaHelper; + NSPSMatrix *_frameMatrix; + NSView *_superview; + NSMutableSet *_dragTypes; + CGRect _frame; + CGRect _bounds; + NSArray *_subviews; + CALayer *_layer; + NSWindow *_window; + ... +} + ++ (id)new; ++ (bool)requiresConstraintBasedLayout; +- (id)initWithFrame:(CGRect arg1); +@end +``` + +- **A single click** on a class symbol in the symbol table shows the declaration in + the *Class Definition* tab at the bottom of the window, without switching that tab + under you. **A double click** opens the same text in a window of its own: + monospaced, read-only, selectable, with a *Copy* button. +- Selecting the **metaclass** symbol (`_OBJC_METACLASS_$_NSView`) shows the class + methods of the class instead of its instance members. +- Selecting a symbol that is **not** a class says so rather than doing nothing: + *"Unsupported symbol: \_DVTAllocateObject is not an Objective-C class."* When the + class cannot be found in the image, that is reported as well. +- The declaration is complete: the superclass, the protocols the class adopts, the + ivars with their types, the `@property` declarations with their attributes, and + both the instance and the class methods. +- A property of the protocol object itself — a class property — is printed with the + modern spelling: `@property (class, readonly) bool supportsSecureCoding;`, as in + `@protocol NSSecureCoding`. +- Nothing is loaded and no code is run to produce this — the metadata is parsed from + the image. For a system framework that means from the **dyld shared cache**, see below. +- The reader covers the whole of an image's Objective-C metadata, categories and + protocols included (`@interface NSArray (DVTFoundationAdditions)`, standalone + `@protocol ... @end`). The output has been compared entry by entry with + [`ipsw class-dump`](https://github.com/blacktop/ipsw): for AppKit the two agree on + all 2729 classes and all 66955 methods, and on the name of every category and + protocol. + +## dyld shared cache + +Since macOS 11 most system libraries and frameworks no longer exist as files on +disk — they live in the dyld shared cache. MacDependency opens them anyway: it +locates the image inside the shared cache and reads it from the memory the cache is +mapped into. Such a dependency has no file to reveal in the Finder, which is what +the **File Kind** column says with `Shared Cache`. *This fork:* the tree used to +tell those rows apart by colouring them, which read badly; the column says it in +words instead, in a colour that can be read. + +## The symbol table + +The symbol table lists the exported and the imported symbols of the selected image. +It has three columns — Type, Kind and Name — and two filters, one on the type of a +symbol and one on its kind: + +| Type | Kind | Name | +| ------ | ---------- | -------------------------------------------------------------------------- | +| Export | ObjC class | `DVTFilePath` | +| Export | ObjC ivar | `DVTByteBuffer._bytes` | +| Export | C++ | `DVT::DVTArrayBuilder::_ensureCapacity(unsigned long)` | +| Export | Swift | `static DVTFoundation.IconOfFileIconReference.== infix(...) -> Swift.Bool` | +| Export | C | `DVTAllocateObject` | + +- **Kind** *This fork.* Says what a symbol is: `C`, `C++`, `Swift`, `ObjC class`, + `ObjC metaclass`, `ObjC protocol`, `ObjC ivar` or `ObjC`. A symbol table records + no language, so the kind is worked out from the name — from the prefixes of the + two manglings and of the Objective-C metadata. +- **The filters sit in the headers of the columns they filter.** *This fork.* The + header of the Type column and the header of the Kind column each carry a chevron; + clicking it opens the list of what can be chosen. The column shows what the filter + is about, and a row that is missing is one the column would have shown. A chevron + is drawn in the accent colour while its filter is narrowing the table, so that + rows which are hidden do not look like rows that were never there. +- **The Type filter** offers `All types`, `Export` and `Import`, one of them at a + time. The same filter is also on the **Export** and **Import** buttons above the + table, which is where it is reached from most of the time; whichever of the two is + used, the other follows. The table opens on the exports alone. Turning off the + last type left on is refused, because a table with nothing in it says less than one + that still shows something. +- **The Kind filter** offers `All Kinds` followed by every kind of symbol, and the + list is built from the kinds the model knows about, so it cannot fall out of step + with what the Kind column can show. +- **Demangle Names** turns mangled names back into what they were written as: C++ + names go through the Itanium demangler, Swift names through `swift_demangle`, and + the leading underscore of the C naming convention is dropped in every case. + Objective-C metadata symbols lose their prefix as well, so `_OBJC_CLASS_$_DVTFilePath` + reads `DVTFilePath`. With the option off, every name is shown exactly as the symbol + table holds it. +- **A double click on an imported symbol** follows it to the library that provides + it: the dependency tree selects that library, and its symbol table selects the + export of the same name. The type filter switches to *Export* on the way, because + the imports of the library that was jumped to are not what you are looking for. + +## Copying + +- **⌘C** copies the selected row of the symbol table exactly as the table displays + it, for instance `Export ObjC class DVTFilePath`. It acts only while the symbol + table has the focus and one of its rows is selected, so it stays out of the way + while the dependency tree is being used. +- The class definition text and the log are read-only but selectable, and ⌘C copies + the selection out of them as usual. + +## The log + + +The *Warnings & Errors* tab collects what went wrong while the file was being read: +dependencies that could not be found, version mismatches, architecture mismatches, +and the reasons a class definition could not be produced. Lines that concern a +particular dependency are links — clicking one jumps to it in the dependency tree. +The text follows the system appearance and is readable in dark mode as well. +*This fork:* it used to be drawn in black whatever the background, and to accept +edits; it now carries a colour of its own and is read-only. diff --git a/images/macdependency.jpg b/images/macdependency.jpg new file mode 100644 index 0000000..ce525f5 Binary files /dev/null and b/images/macdependency.jpg differ diff --git a/images/macdependency.png b/images/macdependency.png deleted file mode 100644 index ad491d5..0000000 Binary files a/images/macdependency.png and /dev/null differ