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.1.0
    License
    Apache-2.0
    Last updated
    2 days ago
    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, the import directory and the export directory, and 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.

    #What is parsed

    LayerStatus
    DOS header (MZ, e_lfanew)✅
    NT signature (PE\0\0)✅
    COFF header (machine, sections, timestamp, characteristics)✅
    Optional header, PE32 and PE32+✅
    Data directories (up to 16)✅
    Section table (name, sizes, addresses, permissions)✅
    RVA → file offset (with zero-filled tail detection)✅
    Import directory, by name and by ordinal✅
    Export directory, named and ordinal-only✅
    CLI: info, sections, imports, exports, all✅

    Explicitly not implemented in 0.1.0: execution, disassembly, signature verification, .NET metadata, resources, delay-load imports, TLS callbacks and relocations. They are listed here so the boundary is a decision rather than a surprise.

    #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 section in pe.sections {
    println(section.name + " " + section.permissions()) // ".text r-x"
    }

    for library in pe.imports() {
    println(library.name) // "KERNEL32.dll"
    for entry in library.entries {
    match entry.name {
    Some(name) => println(" " + name)
    None => println(" ordinal " + entry.ordinal.unwrap())
    }
    }
    }

    Every function that can fail raises PeError, whose message() includes the offset. There is no "best effort" mode: a truncated or inconsistent file is rejected, because a partially parsed import table is worse than a loud failure.

    #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 at offset 0x80 machine: AMD64 subsystem: windows-console (3) image: base 0x180000000 size 0xcb000 entry: 0x2c5d0 sections: 8 imports: 104 libraries exports: 1697 symbols

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

    NAME VADDR VSIZE RAWPTR RAWSIZE PERM .text 0x1000 0x85014 0x1000 0x86000 r-x .rdata 0x88000 0x385b0 0x88000 0x39000 r-- .data 0xc1000 0x16b8 0xc1000 0x1000 rw- ...

    #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 are preserved. Real DLLs export and import symbols by ordinal, and a reader that only understands names drops those entries while reporting success. Both directions support ordinal references.

    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 values above 2 GiB instead of letting them wrap differently on each platform.

    #Testing

    Nine tests, all offline:

    • A hand-built PE32 image with two sections, one import library (one named and one ordinal import) and an export directory with two named and one ordinal-only export.
    • A hand-built PE32+ image exercising the widened optional header.
    • Malformed inputs: missing MZ, missing PE\0\0, truncated headers, an e_lfanew pointing outside the file, empty input.
    • A blackbox test that builds its own minimal image and only touches the public API.

    moon test --target wasm-gc

    The parser is also checked by hand against the real kernel32.dll (PE64, 8 sections, 104 import libraries, 1697 exports).

    #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 and export layoutsMicrosoft documentation
    goblin → pe modulebehaviour cross-check (documentation only)MIT
    pefilebehaviour cross-check (documentation only)MIT

    #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.

    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, ...).

    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.

    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::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::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::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::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_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.

    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

    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

    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.

    subsystem_name

    fn subsystem_name(subsystem : Int) -> String

    The subsystem the image targets.