machscope

    Mach-O and DWARF inspection in pure MoonBit: segments, symbols, code signature entitlements and crash-address symbolication, with no Apple toolchain required.

    macho
    dwarf
    binary-analysis
    dsym
    apple
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    3 hours ago
    Downloads
    4

    #machscope

    Mach-O and DWARF inspection in pure MoonBit.

    Reads Apple's executable format with no Apple toolchain present. Linux CI cannot run otool, codesign or atos, and a browser cannot run them either; both can run MoonBit.

    #Quick start

    moon run cmd/main -- internal/fixtures/ls.x86_64.macho

    arch x86_64: header x86_64 MH_MAGIC_64 (little-endian), subtype 3, filetype 2, 18 commands, 1736 bytes cpu type 16777223, subtype 3, capabilities 0x0 filetype 2 flags 0x200085

    The segment table, verified field by field against otool -l:

    $ moon run cmd/main -- --sections internal/fixtures/ls.x86_64.macho __PAGEZERO vmaddr 0x0 vmsize 0x100000000 fileoff 0 filesize 0 __TEXT vmaddr 0x100000000 vmsize 0x5000 fileoff 0 filesize 20480 __text addr 0x100000718 size 15095 offset 1816 __stubs addr 0x10000420f size 504 offset 16911

    The code signature identity, matching codesign -dvv:

    $ moon run cmd/main -- --signature internal/fixtures/ls.x86_64.macho identifier com.apple.ls version 0x20400 flags 0x0 hash 2 slots 8

    #What it does today

    CapabilityStatus
    Thin Mach-O, 32/64-bit, both byte ordersdone
    Universal (fat) binaries, fat_arch and fat_arch_64done
    Load-command framing with full bounds checkingdone
    Type-level cpusubtype / capability-bit splitdone
    Segment and section table, address ↔ file offsetdone
    Symbol table with names resolved through the string tabledone
    Embedded code signature: superblob, CodeDirectorydone
    Entitlements, reached through the PKCS#7 DER wrapperdone
    DWARF 5 line tables, address → file:line:columndone
    CLI: header, --commands, --symbols, --sections, --signature, --exports, --lookupdone
    --json output for every modedone
    dyld export triedone
    Chained fixups, rebase/bind opcodesplanned
    .debug_info variable and type lookupplanned

    #Design notes

    Every field read goes through a bounds-checked cursor. A corrupt file yields a typed MachscopeError naming the field and offset that failed; it never yields an out-of-range read or a partially-parsed result. Lengths from the file are checked before they are used to allocate or iterate.

    Parsing is two-stage: parse_slice walks the whole load-command table, and only then are commands interpreted. A command table that does not tile sizeofcmds is rejected before any command-specific code runs.

    #Symbolicating a crash address

    Checked against dwarfdump on the fixture:

    $ machscope 0x100000490 internal/fixtures/sample.dsym t.c:3:33

    dwarfdump --debug-line reports the same address as line 3, column 33. The path comes from the line program's directory and file tables, which DWARF 5 encodes as form descriptions rather than plain strings.

    #JSON output

    --json is accepted with any mode; the report always carries the header and the command table, and adds segments, symbols or exports when the corresponding flag is present. Thin and universal files produce the same shape, with the slice's placement in the file as extra fat_* keys.

    $ machscope --json --exports sample.bin { "architectures": [ { "header": { "magic": "MH_MAGIC_64", ... }, "commands": [ ... ], "exports": [ { "name": "_main", "offset": "0x478", "flags": 0, "weak": false, "reexport": false } ] } ] }

    Addresses are hex strings rather than JSON numbers: a 64-bit address does not survive a round trip through a double.

    #Usage

    let file = @machscope.parse(bytes)
    match file {
    Thin(slice) => {
    println(slice.header.kind) // MH_MAGIC_64
    println(slice.header.cpu_type) // 16777228 (arm64)
    println(slice.header.cpu_subtype_base())
    println(slice.header.cpu_capabilities()) // 0x80 for arm64e
    for cmd in slice.commands {
    println(cmd.cmd) // e.g. LC_SEGMENT_64
    }
    }
    Fat(slices) => {
    for slice in slices {
    println("\{slice.cpu_type} @ \{slice.offset}+ \{slice.size}")
    }
    }
    }

    #Performance

    moon run bench, on an M-series Mac, mean of 50 runs:

    fixture operation mean ls.x86_64.macho parse (header+commands) 2.7 us ls.x86_64.macho segments 14.7 us ls.x86_64.macho symbols 47.6 us ls.fat.macho parse both slices 6.4 us ls.fat.macho exports, both slices 10.3 us sample.dsym line tables 39.9 us sample.dsym address lookup 40.1 us

    Parsing is linear in file size. The symbol table dominates the thin file because it resolves 93 names through the string table; everything else is a single pass over its own section.

    #Testing

    moon test

    Three layers:

    1. Synthetic buffers pin down each rejection rule. No real linker emits an unaligned cmdsize, so these cases have to be constructed.
    2. Real fixtures in internal/fixtures/: /bin/ls from macOS 15 (universal and thinned per architecture), a dSYM built from sample.c, and that binary.
    3. Differential tests assert values copied from otool -h, otool -f, nm, codesign -dvv, dyld_info -exports and dwarfdump --debug-line for those same fixtures.

    #Licence

    Apache-2.0. The fixture binaries are Apple system binaries used only as test inputs and are not redistributed as part of the library's functionality; see internal/fixtures/README.md.

    MachscopeError

    pub(all) suberror MachscopeError {
    UnexpectedEnd(offset~ : Int64, needed~ : Int64, available~ : Int64)
    BadMagic(magic~ : UInt)
    BadValue(field~ : String, value~ : Int64)
    OutOfBounds(what~ : String, offset~ : Int64, size~ : Int64, limit~ : Int64)
    BadName(offset~ : Int64)
    Inconsistent(what~ : String, detail~ : String)
    LimitExceeded(what~ : String, limit~ : Int64)
    Unsupported(what~ : String)
    }

    Every way a Mach-O or DWARF buffer can be rejected.

    Requests are never answered with a partially-parsed structure: a malformed image surfaces as one of these variants instead of an out-of-bounds read. Offsets in the payloads are absolute file offsets so that a caller can point at the exact bytes that failed.

    MachscopeError::output

    fn MachscopeError::output(self : MachscopeError, logger : &Logger) -> Unit

    Exposes the error message as a plain method.

    MachscopeError::to_string

    fn MachscopeError::to_string(self : MachscopeError) -> String

    Exposes the error message as a plain method.

    Bounds

    pub(all) struct Bounds {
    bytes : Bytes
    start : Int64
    limit : Int64
    }

    A buffer together with the range of it that is legal to read.

    start and limit are absolute file offsets, which lets a slice of a fat binary be handed to the parser without copying: the slice's Bounds covers its own byte range and every error message still reports a file offset.

    Bounds::fixed_name

    fn Bounds::fixed_name(self : Bounds, offset : Int64, width : Int64, what : String) -> String raise MachscopeError

    Reads a fixed-width, zero-padded NUL-terminated name field, e.g. the 16-byte segname of a segment command.

    Bytes after the first NUL are ignored, which is what the loader does: the remainder of the field is padding and not necessarily zeroed.

    Bounds::length

    fn Bounds::length(self : Bounds) -> Int64

    Number of readable bytes.

    Bounds::of_bytes

    fn Bounds::of_bytes(bytes : Bytes) -> Bounds

    Wraps a whole buffer as readable from offset 0 to its length.

    Bounds::read_i32

    fn Bounds::read_i32(self : Bounds, cur : Cursor) -> (Int64, Cursor) raise MachscopeError

    Decodes a signed 32-bit integer, sign-extending into Int64.

    Used for fields such as cputype, which the loader treats as signed and which must round-trip for unknown architectures.

    Bounds::read_i8

    fn Bounds::read_i8(self : Bounds, cur : Cursor) -> (Int64, Cursor) raise MachscopeError

    Decodes a signed 8-bit integer, sign-extending into Int64.

    Bounds::read_u16

    fn Bounds::read_u16(self : Bounds, cur : Cursor) -> (UInt, Cursor) raise MachscopeError

    Decodes an unsigned 16-bit integer in the cursor's byte order.

    Bounds::read_u32

    fn Bounds::read_u32(self : Bounds, cur : Cursor) -> (UInt, Cursor) raise MachscopeError

    Decodes an unsigned 32-bit integer in the cursor's byte order.

    Accumulated as Int64 so that a value with the high bit set does not turn negative before it is reinterpreted as UInt32.

    Bounds::read_u64

    fn Bounds::read_u64(self : Bounds, cur : Cursor) -> (Int64, Cursor) raise MachscopeError

    Decodes an unsigned 64-bit integer in the cursor's byte order.

    Accumulated as Int64 so that all 64 bits round-trip; callers that need the unsigned interpretation mask at the use site.

    Bounds::read_u8

    fn Bounds::read_u8(self : Bounds, cur : Cursor) -> (UInt, Cursor) raise MachscopeError

    Decodes an unsigned 8-bit integer.

    Bounds::require

    fn Bounds::require(self : Bounds, offset : Int64, size : Int64, what : String) -> Unit raise MachscopeError

    Fails unless [offset, offset + size) lies inside the readable range.

    Parsers call this before interpreting a length field that came from the file, so a corrupt length is reported as OutOfBounds with the offending field name rather than being trusted.

    Bounds::slice

    fn Bounds::slice(self : Bounds, offset : Int64, n : Int64, what : String) -> Bytes raise MachscopeError

    Reads n bytes starting at offset, copying them into a fresh buffer.

    CodeDirectory

    pub(all) struct CodeDirectory {
    magic : UInt
    length : Int64
    version : UInt
    flags : UInt
    identifier : String
    team_id : String
    hash_type : UInt
    hash_size : UInt
    page_size : UInt
    code_slots : UInt
    }

    A decoded CodeDirectory.

    Only the fields that identify the signature are decoded: the version tells a reader how far the structure extends, and hashing the rest would require re-deriving every page hash, which is a verification concern rather than an inspection one.

    Cursor

    pub(all) struct Cursor {
    pos : Int64
    endian : Endian
    }

    Position of a byte within the buffer, together with the byte order used to decode multi-byte integers at that position.

    A Cursor is a value, not a view: advancing returns a new cursor and never touches the shared buffer, so the same source bytes can be read by several parsers without copying.

    DerElement

    pub(all) struct DerElement {
    tag : UInt
    content_start : Int64
    content_end : Int64
    next : Int64
    }

    One DER element: its tag, its content range and the offset just past it.

    Endian

    pub(all) enum Endian {
    Little
    Big
    }

    Byte order of the file being read.

    Mach-O files record their own endianness in the magic number; fat headers and DWARF sections inherit the order of the slice or compilation unit that contains them.

    Endian::tag

    fn Endian::tag(self : Endian) -> String

    The byte order as a short tag, for JSON and CLI output.

    Export

    pub(all) struct Export {
    name : String
    address : UInt64
    flags : UInt64
    original_name : String
    }

    One symbol exported by the image.

    Export::is_reexport

    fn Export::is_reexport(self : Export) -> Bool

    Whether the export is re-exported from another library.

    Export::is_weak

    fn Export::is_weak(self : Export) -> Bool

    Whether the export is a weak definition.

    Export::kind

    fn Export::kind(self : Export) -> UInt64

    The kind of an export, from the low bits of its flags.

    Export::to_json

    One export as a JSON object.

    ExportTrieRegion

    pub(all) struct ExportTrieRegion {
    offset : Int64
    size : Int64
    }

    Where the export trie lives inside the file.

    FatSlice

    pub(all) struct FatSlice {
    cpu_type : Int64
    cpu_subtype : Int64
    offset : Int64
    size : Int64
    align : UInt
    image : MachSlice
    }

    One entry of a fat binary's architecture table, with its slice parsed.

    FatArch fields keep the names used in <mach-o/fat.h>.

    FatSlice::cpu_capabilities

    fn FatSlice::cpu_capabilities(self : FatSlice) -> Int64

    The architecture table entry's capability flags.

    FatSlice::cpu_subtype_base

    fn FatSlice::cpu_subtype_base(self : FatSlice) -> Int64

    The architecture table entry's cpusubtype with capability bits removed.

    LineRow

    pub(all) struct LineRow {
    address : Int64
    line : UInt
    column : UInt
    file : UInt
    is_stmt : Bool
    end_sequence : Bool
    op_index : UInt
    basic_block : Bool
    prologue_end : Bool
    epilogue_begin : Bool
    isa : UInt
    discriminator : UInt
    }

    One decoded row of a line table: an address together with the source position that applies from it until the next row.

    LineTable

    pub(all) struct LineTable {
    version : UInt
    comp_dir : String
    dirs : Array[String]
    files : Array[String]
    rows : Array[LineRow]
    }

    The state of a line program that persists across file and directory lists.

    DWARF 5 changed how the file list is encoded; both the version 2-4 list of NUL-terminated names and the version 5 list of file entries with directory indices are decoded, because a dSYM in the wild may be either.

    MachFile

    pub(all) enum MachFile {
    Thin(MachSlice)
    Fat(Array[FatSlice])
    }

    Result of opening a file: either one thin image or the slices of a fat one.

    MachHeader

    pub(all) struct MachHeader {
    kind : MachKind
    cpu_type : Int64
    cpu_subtype : Int64
    file_type : UInt
    ncmds : UInt
    sizeofcmds : UInt
    flags : UInt
    reserved : UInt
    }

    The mach_header fields that are meaningful independent of word size.

    Field names follow <mach-o/loader.h> so that a reader can compare them against the C definition without a mapping table.

    MachHeader::cpu_capabilities

    fn MachHeader::cpu_capabilities(self : MachHeader) -> Int64

    The header's capability flags, from the high byte of cpusubtype.

    MachHeader::cpu_subtype_base

    fn MachHeader::cpu_subtype_base(self : MachHeader) -> Int64

    The header's cpusubtype with capability bits removed.

    MachHeader::describe

    fn MachHeader::describe(self : MachHeader) -> String

    A one-line summary of the header, in the shape file(1) prints.

    Used by the CLI's default output and by error reports, so that a user looking at a failure knows which image the failure came from.

    MachHeader::to_json

    The header of one architecture as a JSON object.

    MachKind

    pub(all) enum MachKind {
    Mach64
    Mach32
    Mach64Be
    Mach32Be
    }

    The four byte orders and word sizes a Mach-O image can be wrapped in.

    The magic number is read as a 32-bit little-endian word first; a value that matches none of these four is not a Mach-O file at all.

    MachKind::endian

    fn MachKind::endian(self : MachKind) -> Endian

    Byte order of a kind.

    MachKind::header_size

    fn MachKind::header_size(self : MachKind) -> Int64

    Size of mach_header / mach_header_64 in bytes.

    MachKind::pointer_size

    fn MachKind::pointer_size(self : MachKind) -> Int

    Word size of a kind, in bytes.

    MachSlice

    pub(all) struct MachSlice {
    header : MachHeader
    commands : Array[RawCommand]
    bounds : Bounds
    }

    A validated thin Mach-O image: the header, its load-command framing and the byte range the slice occupies inside the file it came from.

    RawCommand

    pub(all) struct RawCommand {
    cmd : UInt
    size : UInt
    offset : Int64
    }

    A load command as framed by the parser, before its payload is interpreted.

    Section

    pub(all) struct Section {
    name : String
    segment_name : String
    addr : Int64
    size : Int64
    file_offset : UInt
    align : UInt
    reloc_offset : UInt
    nreloc : UInt
    flags : UInt
    }

    One section_64 entry.

    Section::is_debug

    fn Section::is_debug(self : Section) -> Bool

    Whether the section is marked as debug information.

    This is the attribute strip -S looks at when deciding what to remove, and the one that tells a reader a __DWARF section is worth parsing.

    Section::is_zero_fill

    fn Section::is_zero_fill(self : Section) -> Bool

    Whether this section occupies no file bytes.

    Segment

    pub(all) struct Segment {
    name : String
    vmaddr : Int64
    vmsize : Int64
    file_offset : Int64
    file_size : Int64
    init_prot : UInt
    nsects : UInt
    flags : UInt
    sections : Array[Section]
    }

    One segment_command_64 with its sections.

    Segment::contains_address

    fn Segment::contains_address(self : Segment, addr : Int64) -> Bool

    Whether a virtual address falls inside this segment.

    Segment::contains_offset

    fn Segment::contains_offset(self : Segment, offset : Int64) -> Bool

    Whether a file offset falls inside the bytes this segment occupies on disk.

    Segment::to_json

    One segment and its sections as a JSON object.

    SignatureBlobRef

    pub(all) struct SignatureBlobRef {
    slot : UInt
    offset : Int64
    magic : UInt
    length : Int64
    }

    One entry of the SuperBlob index.

    SignatureRegion

    pub(all) struct SignatureRegion {
    offset : Int64
    size : Int64
    }

    Where the signature lives inside the file.

    SourceLocation

    pub(all) struct SourceLocation {
    file : String
    line : UInt
    column : UInt
    }

    What an address resolved to: a file, a line and a column.

    SourceLocation::to_json

    A resolved source position as a JSON object.

    Symbol

    pub(all) struct Symbol {
    name : String
    n_type : UInt
    n_sect : UInt
    n_desc : UInt
    n_value : Int64
    }

    One nlist_64 entry.

    name is resolved against the string table rather than left as an offset, because every caller wants the name and none wants the offset.

    Symbol::is_debug

    fn Symbol::is_debug(self : Symbol) -> Bool

    Whether the entry is a debug entry rather than a linkable symbol.

    nm hides these by default; a tool that wants to match nm must filter the same way.

    Symbol::is_external

    fn Symbol::is_external(self : Symbol) -> Bool

    Whether the symbol is visible outside its object.

    Symbol::kind

    fn Symbol::kind(self : Symbol) -> UInt

    The n_type discriminator with the N_EXT flag removed.

    Symbol::to_json

    One symbol as a JSON object.

    SymtabCommand

    pub(all) struct SymtabCommand {
    symoff : Int64
    nsyms : UInt
    stroff : Int64
    strsize : UInt
    }

    LC_SYMTAB: where the symbol table and its string table live.

    nlist entries do not carry their own names; each holds a 32-bit offset into the string table at stroff.

    CPU_SUBTYPE_MASK

    let CPU_SUBTYPE_MASK : Int64

    Mask selecting the capability bits of a cpusubtype field.

    <mach-o/machine.h> defines CPU_SUBTYPE_MASK as 0xff000000: the low 24 bits identify the subtype, the high 8 bits carry capabilities such as pointer authentication for arm64e. Both the thin header and the fat architecture table store the combined value, so callers that want to compare against constants like CPU_SUBTYPE_ARM64E must mask first.

    CSMAGIC_CODEDIRECTORY

    let CSMAGIC_CODEDIRECTORY : UInt

    Magic of a CodeDirectory blob.

    CSMAGIC_EMBEDDED_SIGNATURE

    let CSMAGIC_EMBEDDED_SIGNATURE : UInt

    Magic of the outer SuperBlob.

    CSSLOT_ENTITLEMENTS

    let CSSLOT_ENTITLEMENTS : UInt

    The CSSLOT_ENTITLEMENTS index type: a DER-encoded entitlement plist.

    The value is 5, but Apple's embedded signatures use slot 0x10000 for it; the two are distinct constants in <Security/SecCode.h> and only the latter appears in binaries produced by the current toolchain.

    EXPORT_FLAG_REEXPORT

    let EXPORT_FLAG_REEXPORT : UInt64

    EXPORT_SYMBOL_FLAGS_REEXPORT: the symbol is re-exported from a library.

    EXPORT_FLAG_STUB_AND_RESOLVER

    let EXPORT_FLAG_STUB_AND_RESOLVER : UInt64

    EXPORT_SYMBOL_FLAGS_STUB_AND_RESOLVER.

    EXPORT_FLAG_WEAK

    let EXPORT_FLAG_WEAK : UInt64

    EXPORT_SYMBOL_FLAGS_WEAK_DEFINITION: a weak definition can be replaced.

    EXPORT_KIND_ABSOLUTE

    let EXPORT_KIND_ABSOLUTE : UInt64

    EXPORT_SYMBOL_FLAGS_KIND_ABSOLUTE: the value is not an address.

    EXPORT_KIND_MASK

    let EXPORT_KIND_MASK : UInt64

    EXPORT_SYMBOL_FLAGS_KIND_MASK: the low two bits select the kind.

    EXPORT_KIND_REGULAR

    let EXPORT_KIND_REGULAR : UInt64

    EXPORT_SYMBOL_FLAGS_KIND_REGULAR: the address is the symbol's address.

    EXPORT_KIND_THREAD_LOCAL

    let EXPORT_KIND_THREAD_LOCAL : UInt64

    EXPORT_SYMBOL_FLAGS_KIND_THREAD_LOCAL.

    LC_CODE_SIGNATURE

    let LC_CODE_SIGNATURE : UInt

    LC_CODE_SIGNATURE.

    LC_DYLD_EXPORTS_TRIE

    let LC_DYLD_EXPORTS_TRIE : UInt

    LC_DYLD_EXPORTS_TRIE: names the export trie directly.

    LC_DYLD_INFO

    let LC_DYLD_INFO : UInt

    LC_DYLD_INFO and LC_DYLD_INFO_ONLY.

    LC_DYLD_INFO_ONLY

    let LC_DYLD_INFO_ONLY : UInt

    64-bit variant.

    LC_DYSYMTAB

    let LC_DYSYMTAB : UInt

    String table alone, used by object files with no symbols.

    LC_SEGMENT

    let LC_SEGMENT : UInt

    Load-command numbers this library decodes into typed values.

    The full list is in <mach-o/loader.h>; only the ones that carry data the rest of the library exposes are named here.

    LC_SEGMENT_64

    let LC_SEGMENT_64 : UInt

    64-bit variant of LC_SEGMENT.

    LC_SYMTAB

    let LC_SYMTAB : UInt

    Symbol and string table locations.

    N_EXT

    let N_EXT : UInt

    N_EXT: external symbol.

    N_STAB

    let N_STAB : UInt

    N_STAB: bits set mean this is a debugging entry, not a linkable symbol.

    N_TYPE

    let N_TYPE : UInt

    N_TYPE: the mask selecting a symbol's kind.

    SECTION_TYPE

    let SECTION_TYPE : UInt

    The section type bits of section.flags.

    S_ATTR_DEBUG

    let S_ATTR_DEBUG : UInt

    S_ATTR_DEBUG: the section holds debugging information.

    S_ZEROFILL

    let S_ZEROFILL : UInt

    S_ZEROFILL: the section occupies memory but no bytes in the file.

    TAG_CTX_0

    let TAG_CTX_0 : UInt

    The tag Apple uses for a context-specific constructed element (0xA0).

    TAG_CTX_1

    let TAG_CTX_1 : UInt

    The second context-specific constructed tag in the CMS layout (0xA1).

    TAG_OCTET_STRING

    let TAG_OCTET_STRING : UInt

    The universal tag number for an OCTET STRING.

    TAG_OID

    let TAG_OID : UInt

    The universal tag number for an OBJECT IDENTIFIER.

    TAG_SEQUENCE

    let TAG_SEQUENCE : UInt

    The universal tag number for a SEQUENCE or SEQUENCE OF.

    TAG_SET

    let TAG_SET : UInt

    The universal tag number for a SET or SET OF.

    TAG_UTF8

    let TAG_UTF8 : UInt

    The universal tag number for a UTF8String.

    children

    fn children(bytes : Bytes, parent : DerElement) -> Array[DerElement] raise MachscopeError

    Collects the immediate children of a constructed element.

    Indefinite-length parents have their children walked until the end-of-contents marker; definite-length parents stop at their declared end.

    code_directory

    fn code_directory(slice : MachSlice, region : SignatureRegion) -> CodeDirectory? raise MachscopeError

    Decodes the CodeDirectory blob from the index.

    debug_line_section

    fn debug_line_section(slice : MachSlice) -> Section? raise MachscopeError

    The .debug_line section of a slice.

    debug_line_str_section

    fn debug_line_str_section(slice : MachSlice) -> Section? raise MachscopeError

    The .debug_line_str section, which DW_FORM_line_strp offsets index.

    entitlements_der

    fn entitlements_der(slice : MachSlice, region : SignatureRegion) -> Bytes? raise MachscopeError

    Extracts the entitlements plist from a signature, if there is one.

    The payload is DER-wrapped: a PKCS#7 SignedData whose entitlement attribute holds the property list. The returned bytes are the plist itself, not yet parsed.

    export_trie

    fn export_trie(slice : MachSlice) -> ExportTrieRegion? raise MachscopeError

    Finds the export trie, preferring LC_DYLD_EXPORTS_TRIE and falling back to the export fields of LC_DYLD_INFO.

    Both forms appear in the wild: the standalone command was added later, and older linkers put the same bytes in LC_DYLD_INFO only.

    exports

    fn exports(slice : MachSlice) -> Array[Export] raise MachscopeError

    Reads the symbols from an export trie.

    The walk uses an explicit stack so a corrupt trie cannot exhaust the call stack.

    file_offset_of_address

    fn file_offset_of_address(segs : Array[Segment], addr : Int64) -> Int64?

    Translates a virtual address to a file offset.

    Returns None for addresses in a segment's zero-fill tail, which occupies memory but no bytes on disk, and None for addresses outside every segment.

    file_report

    fn file_report(file : MachFile, segments_? : Bool, symbols_? : Bool, exports_? : Bool) ->
    Json
    raise MachscopeError

    Builds a report for a whole file.

    A thin image reports architectures with one entry; a fat one reports each slice. The shape is the same either way so that a consumer does not have to branch on which kind of file it was given.

    find_section

    fn find_section(segs : Array[Segment], segment_name : String, section_name : String) -> Section?

    Finds a section by names. The segment name is matched as well because __text exists in more than one segment across formats.

    find_signed_data

    fn find_signed_data(bytes : Bytes) -> DerElement raise MachscopeError

    Finds a SignedData element inside a code-signature blob.

    The blob is a DER ContentInfo: a SEQUENCE holding an OID and a context-specific wrapper around the SignedData. The walk returns the SignedData element so its nested attributes can be searched.

    find_symtab

    fn find_symtab(slice : MachSlice) -> SymtabCommand? raise MachscopeError

    Finds the first LC_SYMTAB in a slice.

    hex

    fn hex(v : Int64) -> String

    Formats an integer as lowercase hexadecimal.

    Lives in the library rather than the CLI because every caller that prints an offset or a flags word needs it, and because it is the one conversion whose output the tests can assert against otool.

    line_tables

    fn line_tables(slice : MachSlice, comp_dir? : String) -> Array[LineTable] raise MachscopeError

    Parses every line program in a .debug_line section.

    comp_dir comes from the caller because in DWARF 2-4 it lives in the .debug_info compilation unit, not in the line prologue; a dSYM reader that has not parsed .debug_info can still resolve paths relative to it.

    lookup_address

    fn lookup_address(table : LineTable, addr : Int64) -> SourceLocation?

    Looks up the source location of an address.

    Returns the row with the greatest address less than or equal to the query, which is the row that governs the address. A row marked end_sequence ends its run, so an address at or past it belongs to no row.

    octet_string

    fn octet_string(bytes : Bytes, el : DerElement) -> Bytes raise MachscopeError

    Reads an OCTET STRING's payload.

    Concatenates the fragments of a constructed octet string, which is the form a large entitlement plist takes.

    parse

    fn parse(bytes : Bytes) -> MachFile raise MachscopeError

    Opens a buffer as a Mach-O file, accepting both thin and fat images.

    A fat image is parsed slice by slice; a slice that fails to parse makes the whole call fail, because a universal binary with one unreadable architecture cannot be reported as a usable universal binary.

    parse_slice

    fn parse_slice(bounds : Bounds, offset : Int64) -> MachSlice raise MachscopeError

    Parses a header and its load-command framing at offset inside bounds.

    Every command is checked to start and end inside the slice and inside the declared sizeofcmds region; a command whose cmdsize is smaller than the 8-byte generic header, or not a multiple of 4, makes the whole image unreadable because the table cannot be walked any further.

    read_element

    fn read_element(bytes : Bytes, offset : Int64, limit : Int64) -> DerElement raise MachscopeError

    Reads one element's header and content range.

    Handles both definite lengths (short and long form) and the indefinite form BER permits, which ends at a zero-length end-of-contents marker.

    segments

    fn segments(slice : MachSlice) -> Array[Segment] raise MachscopeError

    Decodes every LC_SEGMENT_64 in a slice.

    32-bit LC_SEGMENT is not decoded: every binary that carries DWARF today is 64-bit, and silently returning an empty table for a 32-bit file would be worse than the caller knowing. A 32-bit slice with segment commands is reported as unsupported.

    signature_blobs

    fn signature_blobs(slice : MachSlice, region : SignatureRegion) -> Array[SignatureBlobRef] raise MachscopeError

    Reads the SuperBlob index of a signature region.

    Every blob's own header is read here so that a caller can inspect the index without decoding blobs it does not need, and so that a blob pointing outside the region is reported before anything tries to parse it.

    signature_region

    fn signature_region(slice : MachSlice) -> SignatureRegion? raise MachscopeError

    Finds the LC_CODE_SIGNATURE command in a slice.

    slice_report

    fn slice_report(slice : MachSlice, segments_? : Bool, symbols_? : Bool, exports_? : Bool) ->
    Json
    raise MachscopeError

    Builds a report for one architecture.

    Sections are omitted when not requested so that a caller asking only for the header does not pay for a full table walk, and so that a failure in one section does not take down the whole report.

    string_table_entry

    fn string_table_entry(slice : MachSlice, tab : SymtabCommand, offset : UInt) -> String raise MachscopeError

    Reads the NUL-terminated string at offset inside the string table.

    Mach-O string tables are sequences of NUL-terminated UTF-8 strings; an offset that does not land on a terminator before the end of the table means the table is corrupt, which is reported rather than truncated silently.

    subtype_base

    fn subtype_base(raw : Int64) -> Int64

    The low 24 bits of a cpusubtype: the subtype without capability bits.

    subtype_capabilities

    fn subtype_capabilities(raw : Int64) -> Int64

    The high 8 bits of a cpusubtype: pointer authentication and similar capability flags.

    symbols

    fn symbols(slice : MachSlice) -> Array[Symbol] raise MachscopeError

    Decodes every nlist_64 in the symbol table.

    The count comes from nsyms and the loop is bounded by it, which is safe because symtab_command already checked that all nsyms entries are readable.

    symtab_command

    fn symtab_command(slice : MachSlice, cmd : RawCommand) -> SymtabCommand raise MachscopeError

    Decodes an LC_SYMTAB command.