Files
vphone-cli/VPhoneExecutable/VPhoneCommand/FirmwarePatcher/DyldSharedCache/DyldSharedCacheSymbolResolver.swift
T
2026-09-24 20:23:44 +09:00

227 lines
8.9 KiB
Swift

// DyldSharedCacheSymbolResolver.swift — Symbol addresses in the shared cache, without `ipsw`.
//
// Three CFW patchers used to shell out to `ipsw dyld symaddr`:
// `cfw_patch_camera_dsc.py`, `cfw_patch_iomfb_swapend.py` and
// `cfw_patch_iomfb_force_kern.py`. Between them they need two kinds of symbol,
// and the cache stores them in two different places:
//
// * **Exported** symbols — `_IOMobileFramebufferSwapBegin` and its siblings —
// live in each image's export trie, addressed as an offset from the image's
// mach header.
// * **Local** symbols — every `+[Class method]`, and private C functions such
// as `_kern_SwapEnd` — are stripped out of the images at cache-build time
// and parked in `dyld_shared_cache_<arch>.symbols`, sliced per image.
//
// Resolving both, per image, is what replaces the Go tool on the patch path.
import Foundation
import MachOKit
/// Symbol lookup across the images of a chunked dyld shared cache.
public final class DyldSharedCacheSymbolResolver {
/// Where a resolved address came from. Worth reporting: a symbol found only
/// in the local table is one `ipsw dyld symaddr` may also be slow to find,
/// and a symbol found in both should agree.
public enum Source: String, Sendable {
case exportTrie
case localSymbols
}
public struct Resolution: Sendable {
public let name: String
public let address: UInt64
public let source: Source
}
/// The cache and all its sub-caches as one view.
///
/// This has to be the full cache, not the unsuffixed file: an image's
/// export trie lives in the `.dyldlinkedit` sub-cache, and the main
/// chunk's own mapping table covers only its first megabyte, so a
/// `DyldCache` opened on it alone reports no images past `.00` and no
/// exported symbols at all.
private let cache: FullDyldCache
private let localSymbols: DyldSharedCacheLocalSymbolTable?
private let localSymbolsURL: URL
private let sharedRegionStart: UInt64
/// image path → mach header address.
private let imageAddresses: [String: UInt64]
private var symbolCacheByImage: [String: [String: Resolution]] = [:]
// MARK: - Construction
/// Open the cache whose unsuffixed chunk is `mainCacheURL`.
///
/// The `.symbols` side file is expected beside it. Its *absence* is not
/// fatal — exported symbols still resolve — but every ObjC method lookup
/// will then fail, so callers that need one should check `hasLocalSymbols`.
/// A file that is present but unparseable *is* fatal: that is a broken
/// cache, and reporting it as "no local symbols" would turn it into a pile
/// of `symbolNotFound`s pointing at the wrong thing.
public init(mainCacheURL: URL) throws {
cache = try FullDyldCache(url: mainCacheURL)
sharedRegionStart = cache.mainCacheHeader.sharedRegionStart
var addresses: [String: UInt64] = [:]
if let infos = cache.imageInfos {
for info in infos {
guard let path = info.path(in: cache) else { continue }
if addresses[path] == nil {
addresses[path] = info.address
}
}
}
imageAddresses = addresses
let symbolsURL = mainCacheURL.deletingLastPathComponent()
.appendingPathComponent(mainCacheURL.lastPathComponent + ".symbols")
localSymbols = FileManager.default.fileExists(atPath: symbolsURL.path)
? try DyldSharedCacheLocalSymbolTable(url: symbolsURL)
: nil
localSymbolsURL = symbolsURL
}
/// Open the cache a `DyldSharedCacheChunkSet` already found.
public convenience init(chunks: DyldSharedCacheChunkSet) throws {
try self.init(mainCacheURL: chunks.mainCacheURL)
}
public var hasLocalSymbols: Bool {
localSymbols != nil
}
/// Fail now if the local symbol table is not there.
///
/// For the discovery-shaped callers — `cfw_patch_iomfb_force_kern` looks
/// for `_kern_Swap*`, which exist only in the local table. Without this,
/// a missing `.symbols` file makes discovery return an empty set, which
/// reads as "this cache has no such functions" rather than "I had nowhere
/// to look".
public func requireLocalSymbols() throws {
guard localSymbols != nil else {
throw DyldSharedCacheError.localSymbolsMissing(path: localSymbolsURL.path)
}
}
/// Every image path in the cache, sorted.
public var imagePaths: [String] {
imageAddresses.keys.sorted()
}
/// The mach header address of `imagePath`.
public func headerAddress(ofImage imagePath: String) throws -> UInt64 {
guard let address = imageAddresses[imagePath] else {
throw DyldSharedCacheError.imageNotFound(imagePath)
}
return address
}
// MARK: - Lookup
/// Every symbol `imagePath` exposes, as name → resolution.
///
/// Export-trie entries are taken first, so a name that appears in both
/// tables reports the exported address — the one a caller of the public
/// entry point would branch to.
public func symbols(inImage imagePath: String) throws -> [String: Resolution] {
if let cached = symbolCacheByImage[imagePath] {
return cached
}
let headerAddress = try headerAddress(ofImage: imagePath)
var result: [String: Resolution] = [:]
if let machO = machOFile(forImage: imagePath) {
for exported in machO.exportedSymbols {
guard let offset = exported.offset, offset != 0 else { continue }
let address = headerAddress &+ UInt64(offset)
if result[exported.name] == nil {
result[exported.name] = Resolution(
name: exported.name,
address: address,
source: .exportTrie,
)
}
}
}
if let localSymbols {
let dylibOffset = headerAddress &- sharedRegionStart
if let entry = localSymbols.entry(forDylibOffset: dylibOffset) {
for (name, address) in localSymbols.symbols(in: entry) where result[name] == nil {
result[name] = Resolution(
name: name,
address: address,
source: .localSymbols,
)
}
}
}
symbolCacheByImage[imagePath] = result
return result
}
/// The address of `symbol` in `imagePath`.
///
/// A miss with no local symbol table loaded reports the missing table
/// rather than the missing symbol — most of what this resolves (every ObjC
/// method, every private C function) lives only in that table, so "not
/// found" would send the caller looking in the wrong place.
public func address(of symbol: String, inImage imagePath: String) throws -> UInt64 {
guard let resolution = try symbols(inImage: imagePath)[symbol] else {
try requireLocalSymbols()
throw DyldSharedCacheError.symbolNotFound(symbol: symbol, image: imagePath)
}
return resolution.address
}
/// Resolve several symbols at once, failing on the first one missing.
///
/// Reports every missing name rather than only the first: a patcher that
/// has lost five ObjC methods to a rename wants to see all five.
public func addresses(
of symbols: [String],
inImage imagePath: String,
) throws -> [String: UInt64] {
let table = try self.symbols(inImage: imagePath)
var result: [String: UInt64] = [:]
var missing: [String] = []
for symbol in symbols {
if let resolution = table[symbol] {
result[symbol] = resolution.address
} else {
missing.append(symbol)
}
}
guard missing.isEmpty else {
try requireLocalSymbols()
throw DyldSharedCacheError.symbolNotFound(
symbol: missing.joined(separator: ", "),
image: imagePath,
)
}
return result
}
/// Every symbol in `imagePath` whose name starts with `prefix`.
///
/// `cfw_patch_iomfb_force_kern` works this way: it does not know the set of
/// `_IOMobileFramebufferSwap*` entry points ahead of time, it discovers
/// them and pairs each with its `_kern_` sibling.
public func symbols(
inImage imagePath: String,
withPrefix prefix: String,
) throws -> [String: UInt64] {
try symbols(inImage: imagePath)
.filter { $0.key.hasPrefix(prefix) }
.mapValues(\.address)
}
// MARK: - Image lookup
private func machOFile(forImage imagePath: String) -> MachOFile? {
cache.machOFiles().first { $0.imagePath == imagePath }
}
}