Sign in

    moonpe

    Pure-MoonBit reader for Windows PE/COFF images: headers, section table, imports and exports, with precise byte-offset errors instead of panics.

    pe
    coff
    windows
    executable
    dll
    binary
    parser
    reverse-engineering
    Download zip
    Version
    0.5.0
    License
    Apache-2.0
    Last updated
    yesterday
    Downloads
    27

    #moonpe

    CI mooncakes.io

    A pure-MoonBit reader for Windows PE/COFF images — the format behind every .exe, .dll and .sys on Windows. It reads the DOS/NT headers, the section table, imports (including delay-loaded ones), exports, debug records with their PDB path, base relocations, the resource tree and VERSIONINFO, and it reports malformed input with the exact byte offset instead of panicking.

    #Why this exists

    MoonBit can already read Linux executables: moonbit-community/elf parses ELF object files. There is nothing for the other half of the world. Every Windows binary is a PE image, and tooling around those binaries — dependency checks, plugin validation, supply-chain triage — currently has no MoonBit building block.

    PE is also a format where "roughly right" is not enough. Offsets are relative to sections rather than the file, some bytes exist in memory but not on disk, and import tables mix name-based and ordinal-based references. Each of those is a place where a naive reader silently returns wrong data.

    #Relationship to xmyd0915/moonar

    moonar is an ar / COFF archive library: it handles .a, GNU thin archives and Microsoft .lib static or import libraries. moonpe handles PE images (.exe / .dll / .sys). They share only the 20-byte COFF file header and 40-byte section header that the format itself mandates; PE's DOS stub, optional header, data directories, import/export tables and RVA mapping are absent from moonar, and archive containers are absent from moonpe.

    Inputmoonar 0.6.1moonpe
    kernel32.dllAR001 at byte 0: expected ar archive magicPE64, 8 sections, 104 import libraries, 3 delay-loaded, 1697 exports
    demo.a0 118560 "bcheck.o"bad signature at 0x0: expected the 'MZ' DOS signature

    The full comparison, including the decisions a user can make from each output, is in docs/对比说明.md.

    #What is parsed

    AreaStatus
    DOS header (MZ, e_lfanew) and PE\0\0✅
    COFF header and optional header, PE32 and PE32+✅
    All 16 data directories, named and mapped to file offsets✅
    Section table (name, sizes, addresses, permissions)✅
    RVA → file offset, rejecting the zero-filled tail✅
    Imports by name and by ordinal✅
    Delay-load imports, RVA and VA descriptor forms✅
    Exports, including ordinal-only entries✅
    Debug directory, CodeView RSDS/NB10, PDB path and age✅
    Base relocations, counted by fixup type✅
    Section entropy and a release/triage check with a verdict✅
    TLS directory and callback count✅
    CLR header and .NET runtime version✅
    | Application manifest and equestedExecutionLevel | ✅ | | Resource tree (type / name / language) | ✅ | | VERSIONINFO: file/product version and string table | ✅ | | Exception, Security, Architecture, GlobalPtr, TLS, LoadConfig, BoundImport, IAT, CLR, Reserved | address range only (by design) |

    The last row is a deliberate boundary, not an omission: those directories are reported with their name, address and whether they map to file bytes. What is not implemented at all: execution, disassembly, signature verification, .NET metadata decoding, and writing or patching images.

    #Install

    moon add Rz-coder8848/moonpe

    import { "Rz-coder8848/moonpe" @pe }

    #Library usage

    let pe = @pe.Pe::parse(bytes)

    pe.bits() // 64
    pe.machine() // "AMD64"
    pe.subsystem() // "windows-console"
    pe.optional.entry_point // 0x2c5d0
    pe.optional.image_base // 0x180000000

    for library in pe.imports() {
    println(library.name)
    }
    for library in pe.delay_imports() { // the dependencies a naive reader misses
    println("delay: " + library.name)
    }
    for entry in pe.debug_entries() {
    println(entry.kind_name)
    }
    for resource in pe.resources() {
    println(resource.language.to_string())
    }
    match pe.version_info() {
    Some(info) => println(info.file_version)
    None => ()
    }

    #CLI

    The command uses moonbitlang/x to read the file — the library itself has no dependencies.

    moon run cmd/moonpe --target js -- info C:/Windows/System32/kernel32.dll

    file: C:/Windows/System32/kernel32.dll format: PE64 machine: AMD64 subsystem: windows-console image: base 0x180000000 size 0xcb000 entry: 0x2c5d0 sections: 8 imports: 104 libraries delay: 3 libraries exports: 1697 symbols pdb: kernel32.pdb version: Microsoft® Windows® Operating System 10.0.26100.9444

    Commands: info, sections, imports, exports, debug, relocations, resources, check, all.

    $ moon run cmd/moonpe --target js -- resources C:/Windows/System32/kernel32.dll TYPE NAME LANG RVA/SIZE MUI 1 0x409 0xc9458/200 VERSION 1 0x409 0xc90b0/932 $ moon run cmd/moonpe --target js -- relocations C:/Windows/System32/kernel32.dll PAGE ENTRIES DIR64 HIGHLOW OTHER 0x88000 218 217 0 0 0x89000 222 222 0 0 ...

    $ moon run cmd/moonpe --target js -- check C:/Windows/System32/kernel32.dll verdict: ok max entropy: 625 / 800 [INFO] DELAY_IMPORTS 3 delay-loaded libraries; static-only dependency checks miss these [INFO] CAPABILITIES imports CreateRemoteThreadEx, VirtualAllocEx, ... (loader APIs)
    The check deliberately treats those imports as a *capability*, not a verdict: `kernel32.dll` imports most of the list and still comes out `ok`. It only escalates when the imports appear together with writable-executable code or a high-entropy section. ## Design notes **Bounds-checked reads carry the offset.** `Reader::need` turns every overrun into `OutOfBounds { offset, needed, available }`. The same applies to a bad `e_lfanew`, a bogus optional-header magic, or a section table that declares more entries than the file can hold. **RVA mapping is not `rva + image_base`.** A section's `VirtualSize` and `RawSize` differ; the bytes between them exist in memory (zero-filled by the loader) but not in the file. `rva_to_offset` returns `None` for that tail rather than reading whatever follows the section on disk. **Ordinals and delay-load are preserved.** Real DLLs import and export symbols by ordinal, and many ship a large delay-load table: `shell32.dll` has 79 static imports and **138 delay-loaded** ones. A reader that only walks slot 1 reports less than half of that file's dependencies while still claiming success. **32-bit fields are read through `Int64` and range-checked.** The same source must behave identically when compiled for a 32-bit `Int` target and a 64-bit one; the reader rejects address-like values above 2 GiB instead of letting them wrap differently on each platform. Pure constants such as the `VS_FIXEDFILEINFO` signature go through a separate raw read, because they are not addresses. ## Testing Twenty-nine tests, all offline: hand-built PE32 and PE32+ images with sections, imports, delay imports (both descriptor forms), exports, debug records, relocations, a resource tree and `VERSIONINFO`; plus malformed inputs and a test that truncates a valid image at every possible length and requires a clean error every time. ```sh moon test --target wasm-gc

    Real-binary validation (output compared against system tools):

    FileResult
    System32\kernel32.dllPE64, 8 sections, 104 imports, 3 delay, 1697 exports
    SysWOW64\kernel32.dllPE32, 6 sections, 102 imports, 3 delay, 1669 exports
    System32\shell32.dllPE64, 79 imports, 138 delay, 843 exports
    System32\notepad.exePE64, 50 imports, 6 delay, 0 exports

    #Completion criteria for v1.0

    This project defines "complete format support" as a checklist rather than a feeling:

    1. all PE32 and PE32+ optional-header fields readable, with cross-width tests;
    2. all 16 data directories named, addressed and mapped;
    3. imports, delay imports, exports and the IAT readable;
    4. sections, relocations, debug/PDB, resource tree and VERSIONINFO readable;
    5. arbitrary truncation and malformed input raise PeError, never panic;
    6. at least three real system binaries (PE32 and PE32+) validated;
    7. this README documents the boundary, including the directories that are reported by address only.

    All seven are met in 0.5.0.

    #References

    moonpe is an original implementation, not a port, and contains no third-party code. Its behaviour is aligned with the published format documentation and with the behaviour of established readers:

    ReferenceUsed forLicense
    PE format specificationheader, section, import, export, resource layoutsMicrosoft documentation
    goblin → pe modulebehaviour cross-checkMIT
    pefilebehaviour cross-checkMIT
    xmyd0915/moonaradjacent archive library; relationship documented aboveApache-2.0

    #License

    Apache-2.0. See LICENSE.

    PeError

    pub(all) suberror PeError {
    OutOfBounds(Int, Int, Int)
    BadMagic(String, Int)
    Malformed(String, Int)
    Unsupported(String)
    }

    PeError::message

    fn PeError::message(self : PeError) -> String

    A one-line description with the offset rendered in hex.

    CheckReport

    pub(all) struct CheckReport {
    findings : Array[Finding]
    verdict : String
    max_section_entropy : Double
    } derive(
    Debug
    )

    ClrInfo

    pub(all) struct ClrInfo {
    runtime_version : String
    metadata_size : Int
    flags : Int
    entry_point_token : Int
    } derive(
    Debug
    )

    ClrInfo::to_repr

    CoffHeader

    pub(all) struct CoffHeader {
    machine : Int
    number_of_sections : Int
    timestamp : Int
    characteristics : Int
    } derive(
    Debug
    )

    The COFF file header that follows the PE\0\0 signature.

    CoffHeader::is_32bit_machine

    fn CoffHeader::is_32bit_machine(self : CoffHeader) -> Bool

    IMAGE_FILE_32BIT_MACHINE.

    CoffHeader::is_dll

    fn CoffHeader::is_dll(self : CoffHeader) -> Bool

    IMAGE_FILE_DLL — the image is a library, not an executable.

    CoffHeader::is_executable

    fn CoffHeader::is_executable(self : CoffHeader) -> Bool

    IMAGE_FILE_EXECUTABLE_IMAGE.

    DataDirectory

    pub(all) struct DataDirectory {
    index : Int
    virtual_address : Int
    size : Int
    } derive(
    Debug
    )

    One entry of the optional header's data-directory array. index is the documented slot number (0 = export table, 1 = import table, ...).

    DebugEntry

    pub(all) struct DebugEntry {
    kind : Int
    kind_name : String
    timestamp : Int
    size : Int
    address : Int
    pdb_signature : String?
    pdb_age : Int?
    pdb_path : String?
    } derive(
    Debug
    )

    DelayImportLibrary

    pub(all) struct DelayImportLibrary {
    name : String
    entries : Array[ImportEntry]
    rva_based : Bool
    } derive(
    Debug
    )

    Export

    pub(all) struct Export {
    name : String?
    ordinal : Int
    rva : Int
    } derive(
    Debug
    )

    Export::to_repr

    ExportTable

    pub(all) struct ExportTable {
    dll_name : String
    exports : Array[Export]
    } derive(
    Debug
    )

    A parsed export directory.

    Finding

    pub(all) struct Finding {
    severity : Severity
    code : String
    message : String
    } derive(
    Debug
    )

    Finding::to_repr

    FunctionTable

    pub(all) struct FunctionTable {
    entries : Int
    first_rva : Int
    last_rva : Int
    } derive(
    Debug
    )

    ImportEntry

    pub(all) struct ImportEntry {
    name : String?
    ordinal : Int?
    } derive(
    Debug
    )

    ImportLibrary

    pub(all) struct ImportLibrary {
    name : String
    entries : Array[ImportEntry]
    } derive(
    Debug
    )

    One DLL and everything imported from it.

    OptionalHeader

    pub(all) struct OptionalHeader {
    kind : PeKind
    entry_point : Int
    image_base : Int64
    section_alignment : Int
    file_alignment : Int
    size_of_image : Int
    size_of_headers : Int
    subsystem : Int
    dll_characteristics : Int
    directories : Array[DataDirectory]
    } derive(
    Debug
    )

    The parsed optional header fields this library cares about.
    pub(all) struct Pe {
    bytes : Bytes
    coff : CoffHeader
    optional : OptionalHeader
    sections : Array[Section]
    }

    Pe::bits

    fn Pe::bits(self : Pe) -> Int

    32 or 64.

    Pe::bound_imports

    fn Pe::bound_imports(self : Pe) -> Array[String] raise PeError

    The module names listed in the bound-import directory.

    Pe::check

    fn Pe::check(self : Pe) -> CheckReport

    Run the built-in checks.

    Pe::clr

    fn Pe::clr(self : Pe) -> ClrInfo? raise PeError

    Read the CLR header, or None for a native image.

    Pe::debug_entries

    fn Pe::debug_entries(self : Pe) -> Array[DebugEntry] raise PeError

    Read every debug-directory entry.

    Pe::delay_imports

    fn Pe::delay_imports(self : Pe) -> Array[DelayImportLibrary] raise PeError

    Read the delay-load import directory.

    Pe::directory

    fn Pe::directory(self : Pe, index : Int) -> DataDirectory?

    The data directory with the given slot number, if the header declares it.

    Pe::exports

    fn Pe::exports(self : Pe) -> ExportTable? raise PeError

    Read the export directory.

    Returns None when the image exports nothing, which is the normal case for executables and for many DLLs.

    Pe::function_table

    fn Pe::function_table(self : Pe) -> FunctionTable? raise PeError

    Read the runtime-function table, if the image has one.

    Pe::imports

    fn Pe::imports(self : Pe) -> Array[ImportLibrary] raise PeError

    Read the import directory.

    Returns an empty array when the image has no imports. Structural problems — a descriptor or thunk table that points outside the file — are reported as errors rather than skipped, because a silently truncated import list is worse than a loud failure.

    Pe::machine

    fn Pe::machine(self : Pe) -> String

    The CPU architecture name.

    Pe::manifest

    fn Pe::manifest(self : Pe) -> String? raise PeError

    The raw manifest text, if the image carries one.

    Pe::manifest_execution_level

    fn Pe::manifest_execution_level(self : Pe) -> String? raise PeError

    The requestedExecutionLevel value from the manifest, e.g. asInvoker, highestAvailable or requireAdministrator.

    Pe::parse

    fn Pe::parse(bytes : Bytes) -> Pe raise PeError

    Parse a PE image from memory.

    Every failure is a PeError carrying the offset it was detected at; the parser never panics on hostile input.

    Pe::relocations

    fn Pe::relocations(self : Pe) -> RelocationSummary raise PeError

    Read and summarise the relocation directory.

    Pe::resolved_directories

    fn Pe::resolved_directories(self : Pe) -> Array[ResolvedDirectory]

    Every declared data directory, in slot order.

    Pe::resources

    fn Pe::resources(self : Pe) -> Array[ResourceEntry] raise PeError

    Every resource leaf in the image.

    Pe::rva_to_offset

    fn Pe::rva_to_offset(self : Pe, rva : Int) -> Int?

    Map a relative virtual address to an offset in the file.

    Returns None for addresses inside a section's zero-filled tail: those bytes exist in memory but not on disk, and reading them would return whatever happened to be next in the file.

    Pe::section

    fn Pe::section(self : Pe, name : String) -> Section?

    Look a section up by its (NUL-trimmed) name.

    Pe::section_bytes

    fn Pe::section_bytes(self : Pe, section : Section) -> Bytes?

    The raw bytes backing a section, or None when the section has no file contents (.bss-style) or its declared range lies outside the file.

    Pe::section_entropy

    fn Pe::section_entropy(self : Pe, section : Section) -> Double?

    Entropy of a section's file-backed bytes.

    The range is scanned in place: a packed section can be hundreds of megabytes, and copying it just to count bytes would dominate the scan.

    Pe::section_for_rva

    fn Pe::section_for_rva(self : Pe, rva : Int) -> Section?

    The section that contains an RVA, if any.

    Pe::string_at_rva

    fn Pe::string_at_rva(self : Pe, rva : Int, limit : Int) -> String raise PeError

    Read a NUL-terminated ASCII string located at an RVA.

    Pe::subsystem

    fn Pe::subsystem(self : Pe) -> String

    The subsystem name.

    Pe::suspicious_imports

    fn Pe::suspicious_imports(self : Pe) -> Array[String]

    Imported function names that are common in loader and injection code.

    Pe::tls

    fn Pe::tls(self : Pe) -> TlsInfo? raise PeError

    Read the TLS directory, or None when the image has none.

    Pe::va_to_rva

    fn Pe::va_to_rva(self : Pe, address : Int) -> Int?

    Convert a virtual address to an RVA using the image base.

    Pe::version_info

    fn Pe::version_info(self : Pe) -> VersionInfo? raise PeError

    Read the version resource, if the image has one.

    The string table is checked first; the numeric fixed-file-info version is used as the fallback for file_version, because some build systems omit the FileVersion string.

    PeKind

    pub(all) enum PeKind {
    Pe32
    Pe32Plus
    } derive(Eq,
    Debug
    )

    The two optional-header layouts. Everything before the image base is identical between them; PE32+ widens the image base and the four stack/heap sizes from 4 to 8 bytes.

    PeKind::bits

    fn PeKind::bits(self : PeKind) -> Int

    32 or 64, the question every loader asks first.

    PeKind::equal

    fn PeKind::equal(PeKind, PeKind) -> Bool

    PeKind::not_equal

    fn PeKind::not_equal(x : PeKind, y : PeKind) -> Bool

    PeKind::to_repr

    RelocationBlock

    pub(all) struct RelocationBlock {
    page_rva : Int
    entries : Int
    absolute : Int
    high : Int
    low : Int
    highlow : Int
    dir64 : Int
    other : Int
    } derive(
    Debug
    )

    RelocationSummary

    pub(all) struct RelocationSummary {
    blocks : Array[RelocationBlock]
    total_entries : Int
    absolute : Int
    high : Int
    low : Int
    highlow : Int
    dir64 : Int
    other : Int
    } derive(
    Debug
    )

    ResolvedDirectory

    pub(all) struct ResolvedDirectory {
    index : Int
    name : String
    virtual_address : Int
    size : Int
    file_offset : Int?
    } derive(
    Debug
    )

    One data directory together with its name and file mapping.

    ResourceEntry

    pub(all) struct ResourceEntry {
    type_id : Int?
    type_name : String?
    name_id : Int?
    name_name : String?
    language : Int
    rva : Int
    size : Int
    code_page : Int
    } derive(
    Debug
    )

    Section

    pub(all) struct Section {
    name : String
    virtual_size : Int
    virtual_address : Int
    raw_size : Int
    raw_pointer : Int
    characteristics : Int
    } derive(
    Debug
    )

    Section::is_executable

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

    IMAGE_SCN_MEM_EXECUTE.

    Section::is_readable

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

    IMAGE_SCN_MEM_READ.

    Section::is_writable

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

    IMAGE_SCN_MEM_WRITE.

    Section::permissions

    fn Section::permissions(self : Section) -> String

    A short rwx rendering of the section permissions, the way objdump prints them. A section that is both writable and executable deserves a second look, which is why w and x are shown together rather than collapsed into a single "flags" number.

    Section::to_repr

    Severity

    pub(all) enum Severity {
    Info
    Warning
    High
    } derive(Eq,
    Debug
    )

    Severity::equal

    fn Severity::equal(Severity, Severity) -> Bool

    Severity::not_equal

    fn Severity::not_equal(x : Severity, y : Severity) -> Bool

    Severity::to_repr

    TlsInfo

    pub(all) struct TlsInfo {
    callbacks : Int
    zero_fill_size : Int
    raw_data_size : Int
    } derive(
    Debug
    )

    TlsInfo::to_repr

    VersionInfo

    pub(all) struct VersionInfo {
    file_version : String
    product_version : String
    company_name : String?
    product_name : String?
    file_description : String?
    original_filename : String?
    } derive(
    Debug
    )

    The fields a caller usually wants out of a VS_VERSIONINFO resource.

    byte_entropy

    fn byte_entropy(bytes : Bytes) -> Double?

    debug_type_name

    fn debug_type_name(kind : Int) -> String

    The documented name of a debug-directory type.

    directory_name

    fn directory_name(index : Int) -> String

    entropy_range

    fn entropy_range(bytes : Bytes, start : Int, end : Int) -> Double

    Entropy over bytes[start..end).

    has_flag

    fn has_flag(flags : Int, mask : Int) -> Bool

    Machine-independent helper for the COFF characteristic bits.

    hex

    fn hex(value : Int) -> String

    Lowercase hexadecimal without the 0x prefix. Offsets are never negative.

    hex64

    fn hex64(value : Int64) -> String

    Trimmed lowercase hexadecimal for a 64-bit value, used for image bases.

    hex_upper

    fn hex_upper(value : Int) -> String

    Uppercase hexadecimal, for machine names such as AMD64.

    machine_name

    fn machine_name(machine : Int) -> String

    The processor the image was built for.

    relocation_type_name

    fn relocation_type_name(kind : Int) -> String

    The documented name of a relocation type.

    resource_type_name

    fn resource_type_name(id : Int) -> String

    The documented name of a well-known resource type id.

    subsystem_name

    fn subsystem_name(subsystem : Int) -> String

    The subsystem the image targets.