Skip to content

Module Binary Format

A compiled Move module is a self-contained binary blob that the Aptos Move VM can verify and execute. The binary is divided into three regions that appear in sequence: a fixed-size header, a variable-length table directory, and the table data itself. Every pool, handle table, and definition table referenced elsewhere in this documentation section lives inside one of these table data regions.

This page specifies the binary layout with enough detail to write a parser or serializer from scratch. For the types that appear inside these tables, see the Type System page. For the instructions stored in function bodies, see the Instruction Set Reference.

Every module binary begins with the same fixed-size header.

Offset Size Field
------ ----- ---------------------
0x00 4 Magic bytes
0x04 4 Version word
0x08 ... (table directory follows)

The first four bytes are always 0xA1 0x1C 0xEB 0x0B. Any file that does not start with this sequence is not a valid Move binary. The magic value is sometimes written as the 32-bit little-endian integer 0x0BEB1CA1.

Bytes 4 through 7 form a little-endian u32 that encodes the bytecode format version. On Aptos, this word is bitwise ORed with the constant APTOS_BYTECODE_VERSION_MASK (0x0A000000). To recover the plain version number, mask off the upper byte:

plain_version = version_word & 0x00FFFFFF

For example, a module compiled at bytecode version 9 stores 0x0A000009 in bytes 4—7. The VM reads this word, extracts 9, and uses that number to decide which features are available during verification. See the Bytecode Version History for what each version introduces.

The minimum accepted version is 5 and the current maximum is 10.

Most variable-length integers in the binary — table counts, indices, lengths, and many fields inside table entries — use ULEB128 (Unsigned Little-Endian Base 128) encoding. ULEB128 encodes a non-negative integer in one or more bytes:

  1. Take the lowest 7 bits of the value and place them in the output byte.
  2. If the remaining value (after shifting right by 7) is non-zero, set the high bit (0x80) of the current byte to indicate that more bytes follow, then repeat from step 1 with the shifted value.
  3. If the remaining value is zero, leave the high bit clear and stop.
ValueULEB128 Bytes
00x00
10x01
1270x7F
1280x80 0x01
6240xF0 0x04
163840x80 0x80 0x01

All index types (ModuleHandleIndex, StructHandleIndex, SignatureIndex, and so on) are u16 values serialized as ULEB128. The maximum representable index value is therefore 65,535.

Immediately after the 8-byte header, the binary contains a table directory that describes which tables are present and where their data begins.

Offset Encoding Field
------ -------- ------------------------------------------------
0x08 ULEB128 table_count -- number of table entries that follow
... table_entry[0]
... table_entry[1]
... ...
... table_entry[table_count - 1]

Each table entry has three fields:

FieldEncodingDescription
kindu8Table type code (see the reference below)
offsetULEB128Byte offset from the start of the table data region
lengthULEB128Length of this table’s data in bytes

Only non-empty tables appear in the directory. If a module has no friend declarations, for instance, the FRIEND_DECLS entry is omitted entirely.

The table data region starts immediately after the last directory entry. All offset values in the directory are relative to the beginning of this region — not to the beginning of the file.

After all table data, a module binary ends with one additional ULEB128 value: self_module_handle_idx. This index points into the Module Handles table and identifies which entry represents the module itself (as opposed to imported modules). Parsers must read this trailing value to fully consume the binary.

The following sections document every table type recognized by the Move binary format. Each entry lists the table’s numeric code, the meaning of its rows, and the byte-level layout of each row.

Each entry identifies a Move module by its account address and name. The module’s own self-handle and every imported module each get an entry.

FieldEncodingDescription
addressULEB128Index into ADDRESS_IDENTIFIERS table
nameULEB128Index into IDENTIFIERS table

Each entry describes a struct (or enum) type’s identity — which module defines it, its name, abilities, and type parameters. Both locally-defined and imported struct types appear here.

FieldEncodingDescription
moduleULEB128Index into MODULE_HANDLES table
nameULEB128Index into IDENTIFIERS table
abilitiesu8Ability bitmask (see Abilities)
type_param_countULEB128Number of type parameters
Per type param:
constraintsu8Required abilities for this type argument
is_phantomu8 (0 or 1)Whether this parameter is phantom

Each entry describes a function’s signature — which module defines it, its name, parameter types, return types, and type parameter constraints.

FieldEncodingDescription
moduleULEB128Index into MODULE_HANDLES table
nameULEB128Index into IDENTIFIERS table
parametersULEB128Index into SIGNATURES table (parameter types)
return_ULEB128Index into SIGNATURES table (return types)
type_param_countULEB128Number of generic type parameters
Per type param:
constraintsu8Required ability set for this type argument

Starting at bytecode v7, function handles may also carry access specifiers and (from v8) function attributes. These are serialized after the type parameters when present.

Each entry pairs a generic function with concrete type arguments for a specific instantiation.

FieldEncodingDescription
handleULEB128Index into FUNCTION_HANDLES table
type_parametersULEB128Index into SIGNATURES table (concrete type args)

The Signatures table stores lists of types. Each entry is a Signature — a sequence of SignatureToken values representing parameter lists, return types, local variable types, or type arguments for generic instantiations.

FieldEncodingDescription
token_countULEB128Number of tokens in this signature
Per token:recursive SignatureTokenSee Signature Tokens

Each SignatureToken starts with a one-byte tag that identifies the type kind, optionally followed by additional data (indices, nested tokens, or counts). The complete encoding is documented on the Type System page.

A special convention: SignatureIndex(0) is typically the empty signature [], used for functions with no type arguments.

Each entry stores a compile-time constant value along with its type.

FieldEncodingDescription
type_recursive SignatureTokenThe type of the constant
sizeULEB128Length of the serialized data in bytes
dataraw bytesThe constant value in BCS serialization format

Constants are loaded at runtime by the LdConst instruction (opcode 0x07). The data bytes are in BCS (Binary Canonical Serialization) format.

Each entry is a UTF-8 string used as a name for modules, structs, functions, or fields.

FieldEncodingDescription
lengthULEB128Number of bytes in the UTF-8 string
bytesraw bytesThe UTF-8 encoded string

Identifiers must conform to Move’s naming rules: they start with a letter or underscore and contain only alphanumeric characters and underscores.

Each entry is a 32-byte account address. There is no length prefix — every entry is exactly 32 bytes.

FieldEncodingDescription
address32 bytesAccount address, big-endian

Each entry defines a struct type that is declared in this module. It connects a struct handle to the type’s field layout.

FieldEncodingDescription
struct_handleULEB128Index into STRUCT_HANDLES table
field_info_tagu80x01 = Native, 0x02 = Declared, 0x03 = DeclaredVariants (v7+)

If field_info_tag is 0x02 (Declared), the fields follow:

FieldEncodingDescription
field_countULEB128Number of fields
Per field:
nameULEB128Index into IDENTIFIERS
signaturerecursive SignatureTokenThe field’s type

If field_info_tag is 0x03 (DeclaredVariants, bytecode v7+), the variants follow:

FieldEncodingDescription
variant_countULEB128Number of variants
Per variant:
nameULEB128Index into IDENTIFIERS
field_countULEB128Number of fields in this variant
  Per field:
   nameULEB128Index into IDENTIFIERS
   signaturerecursive SignatureTokenThe field’s type

If field_info_tag is 0x01 (Native), no additional data follows.

Each entry pairs a struct definition with concrete type arguments for a generic instantiation.

FieldEncodingDescription
defULEB128Index into STRUCT_DEFS table
type_parametersULEB128Index into SIGNATURES table (concrete type args)

Each entry defines a function that is declared in this module. It connects a function handle to the function’s implementation.

FieldEncodingDescription
functionULEB128Index into FUNCTION_HANDLES table
visibilityu80x00 = Private, 0x01 = Public, 0x03 = Friend
is_entryu80x01 if this is an entry function, 0x00 otherwise
acquires_countULEB128Number of resources this function acquires
Per acquired resource:
struct_def_indexULEB128Index into STRUCT_DEFS table
code_flagu80x01 if a code body follows, 0x00 for native

If code_flag is 0x01, a Code Unit follows immediately. Native functions have no code body.

Each entry identifies a specific field within a struct definition.

FieldEncodingDescription
ownerULEB128Index into STRUCT_DEFS table
fieldULEB128Field offset within the struct (MemberCount, u16)

Each entry pairs a field handle with concrete type arguments for accessing a field on a generic struct.

FieldEncodingDescription
handleULEB128Index into FIELD_HANDLES table
type_parametersULEB128Index into SIGNATURES table (concrete type args)

Each entry declares a friend module that may call friend-visible functions in this module.

FieldEncodingDescription
addressULEB128Index into ADDRESS_IDENTIFIERS table
nameULEB128Index into IDENTIFIERS table

Bytecode v5+. Each entry is a key-value pair of opaque bytes used for compiler metadata, source maps, or other tooling data. The VM does not interpret metadata contents.

FieldEncodingDescription
key_lengthULEB128Length of the key in bytes
keyraw bytesThe metadata key
val_lengthULEB128Length of the value in bytes
valueraw bytesThe metadata value

Bytecode v7+. Each entry identifies a field that is shared across one or more variants of an enum type.

FieldEncodingDescription
struct_indexULEB128Index into STRUCT_DEFS table (the enum definition)
variant_countULEB128Number of variant indices that follow
Per variant:
variantULEB128Variant index (u16) within the enum’s variant list
fieldULEB128Field offset within the variant (MemberCount, u16)

Bytecode v7+. Each entry pairs a variant field handle with concrete type arguments.

FieldEncodingDescription
handleULEB128Index into VARIANT_FIELD_HANDLES table
type_parametersULEB128Index into SIGNATURES table (concrete type args)

Bytecode v7+. Each entry identifies a specific variant of an enum type.

FieldEncodingDescription
struct_indexULEB128Index into STRUCT_DEFS table (the enum definition)
variantULEB128Variant index (u16) within the enum’s variant list

Bytecode v7+. Each entry pairs a struct variant handle with concrete type arguments.

FieldEncodingDescription
handleULEB128Index into STRUCT_VARIANT_HANDLES table
type_parametersULEB128Index into SIGNATURES table (concrete type args)

The following table lists every table type with its code for quick reference.

CodeTable TypeSince
0x01MODULE_HANDLESv5
0x02STRUCT_HANDLESv5
0x03FUNCTION_HANDLESv5
0x04FUNCTION_INSTv5
0x05SIGNATURESv5
0x06CONSTANT_POOLv5
0x07IDENTIFIERSv5
0x08ADDRESS_IDENTIFIERSv5
0x0ASTRUCT_DEFSv5
0x0BSTRUCT_DEF_INSTv5
0x0CFUNCTION_DEFSv5
0x0DFIELD_HANDLESv5
0x0EFIELD_INSTv5
0x0FFRIEND_DECLSv5
0x10METADATAv5
0x11VARIANT_FIELD_HANDLESv7
0x12VARIANT_FIELD_INSTv7
0x13STRUCT_VARIANT_HANDLESv7
0x14STRUCT_VARIANT_INSTv7

Note: code 0x09 is reserved and unused in the current format.

Every non-native function definition contains a code unit that describes the function body. The code unit is serialized inline within the FUNCTION_DEFS table entry, immediately after the function definition’s metadata fields.

Field Encoding Description
----------------- --------- -----------------------------------
locals ULEB128 Index into SIGNATURES table
(types of local variables)
bytecode_count ULEB128 Number of instructions that follow
bytecode[0] variable First instruction
bytecode[1] variable Second instruction
... ... ...
bytecode[count-1] variable Last instruction

The locals field is a SignatureIndex that points to a Signature in the SIGNATURES table. That signature lists the types of all local variables declared in the function body (it does not include function parameters, which are part of the function handle’s parameter signature). At runtime, the VM allocates a locals array whose first entries hold the function’s parameters (copied from the caller’s operand stack) and whose remaining entries correspond to the locals signature.

Each instruction in the bytecode array starts with a single opcode byte, optionally followed by one or more operands. The encoding of operands depends on the instruction:

  • Index operands (pool indices, definition indices) are encoded as ULEB128.
  • Branch offsets are encoded as u16 in little-endian format. They are absolute offsets from the beginning of the function’s bytecode array.
  • Integer immediates (LdU8, LdU64, etc.) are encoded in fixed-width little-endian format matching their type width (1 byte for u8, 8 bytes for u64, 32 bytes for u256, and so on).
  • Closure masks (PackClosure, PackClosureGeneric) are encoded as ULEB128 u64 values.

The complete instruction encoding for each opcode is documented in the Instruction Set Reference.

Consider a function fun add_one(x: u64): u64 { x + 1 }. Its code unit might look like:

locals: SignatureIndex(0) // no additional locals beyond parameters
bytecode_count: 4
bytecode:
0x0B 0x00 // MoveLoc(0) -- push parameter x
0x06 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 // LdU64(1)
0x16 // Add
0x02 // Ret

MoveLoc(0) pushes the first parameter (x) onto the stack. LdU64(1) pushes the constant 1 as an 8-byte little-endian immediate. Add pops both values and pushes the sum. Ret returns the result to the caller.

The following diagram shows the complete layout of a module binary from start to finish.

┌─────────────────────────────────────┐
│ Magic: 0xA1 0x1C 0xEB 0x0B │ 4 bytes
├─────────────────────────────────────┤
│ Version word (LE u32) │ 4 bytes
├─────────────────────────────────────┤
│ Table count (ULEB128) │ 1--5 bytes
├─────────────────────────────────────┤
│ Table entry 0: │
│ kind (u8) │
│ offset (ULEB128) │
│ length (ULEB128) │
├─────────────────────────────────────┤
│ Table entry 1 ... │
│ ... │
│ Table entry N-1 │
├═════════════════════════════════════┤ <-- table data region starts here
│ Table data (MODULE_HANDLES) │
│ Table data (STRUCT_HANDLES) │
│ Table data (FUNCTION_HANDLES) │
│ Table data (FUNCTION_INST) │
│ Table data (SIGNATURES) │
│ Table data (IDENTIFIERS) │
│ Table data (ADDRESS_IDENTIFIERS) │
│ Table data (CONSTANT_POOL) │
│ Table data (METADATA) │
│ Table data (STRUCT_DEFS) │
│ Table data (STRUCT_DEF_INST) │
│ Table data (FUNCTION_DEFS) │
│ Table data (FIELD_HANDLES) │
│ Table data (FIELD_INST) │
│ Table data (FRIEND_DECLS) │
│ Table data (variant tables, v7+) │
├─────────────────────────────────────┤
│ self_module_handle_idx (ULEB128) │ trailing index
└─────────────────────────────────────┘

The table data sections appear in the order determined by the serializer. In practice the order follows the table type codes (MODULE_HANDLES first, variant tables last), but a parser should rely on the directory offsets rather than assuming any particular order.

The VM enforces strict version-based compatibility rules:

  • Minimum version. The VM rejects any module with a bytecode version below 5. There is no support for legacy versions.
  • Maximum version. The VM rejects any module with a bytecode version above its own maximum (currently 10). Publishing a module compiled for a future version causes the transaction to fail.
  • Feature gating. Each instruction, signature token, and table type is associated with the version that introduced it. The bytecode verifier rejects features that are newer than the module’s declared version. For example, a module declaring version 6 cannot contain PackVariant instructions or variant-related tables, even if the VM itself supports version 7+.

Modules compiled at different bytecode versions can coexist on-chain and call each other freely. The version number governs only what a module is allowed to contain, not what it can interact with. A v5 module can call functions in a v10 module and vice versa.

The APTOS_BYTECODE_VERSION_MASK (0x0A000000) distinguishes Aptos-compiled modules from modules compiled for other Move-based chains. The upper byte of the version word acts as a chain identifier. When parsing a module binary, mask the version word with 0x00FFFFFF to extract the plain version number.

If a binary’s upper byte does not match the expected mask, the VM rejects it. This prevents accidental deployment of modules compiled for a different Move runtime.

The serializer guarantees that no table contains duplicate entries. Identical signatures, identical identifiers, and identical addresses each appear exactly once in their respective tables. Parsers can rely on index equality as a proxy for structural equality within a single module.