Nokia N-Gage and Symbian File Formats

SYMBIAN ARM N-GAGE

N-Gage games ship as MIME multipart packages containing one or more SIS v9.x (SISX) installation archives. Each SISX embeds E32 executables (Symbian native ARM binaries) and data files. Marmalade SDK games add another layer: the E32 is a thin runtime loader, and the actual game code lives in a separate S3E binary. All struct layouts and parsing details documented here are verified against working parser and emulator implementations.

Format Nesting Hierarchy
game.n-gage                          -- MIME multipart/mixed
  |
  +-- Part 0: metadata.xml            -- retailer info, store metadata
  |
  +-- Part 1: game.sis               -- SIS v9.x (SISX) package
  |     |
  |     +-- SISController             -- compressed manifest (file paths, metadata)
  |     |
  |     +-- SISData                   -- compressed file payloads
  |           |
  |           +-- runtime.exe         -- E32 image (Marmalade loader)
  |           +-- game.s3e            -- S3E binary (actual game code)
  |           +-- data.dtrz           -- DTRZ archive (game assets)
  |           +-- icon.png            -- resources
  |           +-- ...
  |
  +-- Part 2: patch.sis              -- optional partial upgrade

Complete Format Specification

The entire N-Gage file format hierarchy as a single nested struct definition. Each sub-format (MIME, SISX, E32, S3E) is defined inline. Refer to the individual sections below for field-level documentation, offset tables, and implementation notes.

N-Gage Complete Format Specification
-- N-Gage Distribution Package
-- MIME multipart/mixed envelope containing SIS installation archives

struct NGagePackage {
  content_type     : "multipart/mixed; boundary=<boundary>"
  parts[]          : {
    headers        : MIMEHeaders          -- Content-Type, Content-Transfer-Encoding
    body           : u8[...]              -- XML metadata | SIS binary data
  }

  -- Part 0: retailer metadata (text/xml)
  -- Part 1: main SIS package (application/sis)
  -- Part 2+: optional partial upgrades

  -- ================================================================
  -- SIS v9.x (SISX) - Type-Length-Value encoded installation archive
  -- ================================================================

  struct SISField {                       -- base TLV container
    type           : u32le                -- field type ID (0..41)
    length         : u32le                -- MSB set: bits[4..30]=len, bits[0..3]=pad
    data           : u8[length]
    _padding       : u8[0..3]             -- align to 4 bytes
  }

  struct SISArray {                       -- typed array (elements omit type field)
    type           : u32le                -- always 2
    length         : u32le
    element_type   : u32le                -- type ID of contained elements
    elements[]     : { length : u32le, data : u8[length], _pad : u8[0..3] }
  }

  struct SISCompressed {
    algorithm      : u32le                -- 0=none, 1=deflate (zlib-wrapped)
    uncompressed   : u64le                -- expected output size
    data           : u8[...]              -- compressed payload
  }

  struct SISContents {                    -- root (type 12)
    controller_crc : SISControllerChecksum?
    data_crc       : SISDataChecksum?
    controller     : SISCompressed {

      struct SISController {              -- type 13
        info         : SISInfo {
          uid        : u32le              -- unique package identifier
          vendor     : SISString          -- UCS-2 encoded
          names      : SISArray<SISString> -- per language
          vendors    : SISArray<SISString>
          version    : { major, minor, build : u32le }
          created    : SISDateTime
          type       : u8                 -- 0=install, 1=augment, 2=patch
          flags      : u8
        }
        options      : SISSupportedOptions
        languages    : SISSupportedLanguages
        prereqs      : SISPrerequisites
        properties   : SISProperties
        logo         : SISLogo?

        install      : SISInstallBlock {
          files      : SISArray {
            struct SISFileDescription {
              target   : SISString        -- e.g. "!:\sys\bin\game.exe"
              mime     : SISString
              caps     : SISCapabilities?
              hash     : SISHash
              op       : u32le            -- 1=install, 2=run, 4=text, 8=null
              op_opts  : u32le
              comp_len : u64le
              raw_len  : u64le
              file_idx : u32le            -- index into DataUnit files
            }
          }
          embedded   : SISArray<SISController>  -- sub-packages
          if_blocks  : SISArray<SISIf>         -- conditional installs
        }

        signatures   : SISSignatureCertificateChain*
        data_index   : u32le              -- selects DataUnit
      }
    }

    data           : SISData {
      units        : SISArray {
        struct SISDataUnit {
          files    : SISArray {
            struct SISFileData {
              payload : SISCompressed<bytes>
            }
          }
        }
      }
    }
  }

  struct SISHeader {                      -- precedes SISContents (16 bytes)
    uid1           : u32le                -- 0x10201A7A
    uid2           : u32le
    uid3           : u32le                -- package UID
    uid_checksum   : u32le
  }

  -- ================================================================
  -- E32 Image - Symbian OS executable (ARM)
  -- ================================================================

  struct E32ImageHeaderV {                -- 124 bytes
    uid1           : u32le                -- 0x1000007A=EXE, 0x10000079=DLL
    uid2           : u32le
    uid3           : u32le                -- application UID
    uid_checksum   : u32le
    signature      : u8[4]               -- "EPOC"
    header_crc     : u32le
    module_version : u32le
    compression    : u32le                -- 0x101F7AFC=deflate, 0x102822AA=bytepair
    tools_version  : u32le
    timestamp      : u64le
    flags          : u32le                -- bit0=DLL, bit3-4=ABI, bit8=debug
    code_size      : u32le
    data_size      : u32le
    heap_min       : u32le
    heap_max       : u32le
    stack_size     : u32le
    bss_size       : u32le
    entry_point    : u32le                -- offset from code_base
    code_base      : u32le
    data_base      : u32le
    dll_ref_count  : u32le
    export_offset  : u32le
    export_count   : u32le
    text_size      : u32le
    code_offset    : u32le                -- file offset to code section
    data_offset    : u32le
    import_offset  : u32le
    code_reloc_off : u32le
    data_reloc_off : u32le
    priority       : u16le                -- 350 = foreground
    cpu_identifier : u16le                -- 0x2000=ARM, 0x2001=ARMv4

    struct E32ImportSection {            -- at import_offset
      total_size   : u32le
      blocks[]     : {
        dll_name_off : u32le            -- offset to DLL name string
        num_imports  : u32le
      }
      ordinals     : u32le[...]           -- import ordinal numbers
      dll_names    : cstring[...]         -- null-terminated ASCII
    }

    struct E32ExportTable {              -- at export_offset
      addresses    : u32le[export_count]  -- relative to code_base
    }

    struct BytepairHeader {              -- if compression = 0x102822AA
      _reserved    : u32le[3]
      unpacked     : u32le
      marker       : u8                   -- 0=256 pairs, else pair count
      pairs        : u8[marker*2]
      data         : u8[...]
    }
  }

  -- ================================================================
  -- S3E Binary - Marmalade SDK game code
  -- ================================================================

  struct S3EHeader {                      -- "XE3U" magic
    magic          : u8[4]               -- "XE3U" 0x55335845
    version        : u32le                -- e.g. 4.14.1 packed
    flags          : u16le
    arch           : u16le                -- 0=ARMv4t, 1=ARMv4, 2=ARMv5t, 3=ARMv5te
    fixup_offset   : u32le
    fixup_size     : u32le
    code_offset    : u32le
    code_file_size : u32le                -- code + initialized data on disk
    code_mem_size  : u32le                -- code_file_size + BSS
    sig_offset     : u32le
    sig_size       : u32le
    entry_offset   : u32le                -- relative to base_addr
    config_offset  : u32le                -- INI-format config block
    config_size    : u32le
    base_addr      : u32le                -- link-time base address
    extra_offset   : u32le
    extra_size     : u32le
    ext_hdr_size   : u32le
    data_offset    : u32le                -- if ext_hdr_size >= 8

    struct FixupSection {                -- at fixup_offset, repeated
      type         : u32le                -- 0=symbols, 1=internal, 2=ARM ext, 3-4=Thumb ext
      size         : u32le                -- includes this 8-byte header

      if type == 0 {                    -- symbol table
        count      : u16le
        names      : cstring[count]       -- S3E API names (ASCII, null-terminated)
      }
      if type == 1 {                    -- internal relocations
        count      : u32le
        offsets    : u32le[count]         -- words needing base fixup
      }
      if type == 2..4 {                 -- external relocations (ARM/Thumb)
        count      : u32le
        entries[]  : {
          off_hi   : u16le
          off_lo   : u16le
          sym_idx  : u16le              -- index into symbol table
        }
      }
    }

    -- API stubs generated at load time:
    -- stub[i] = SVC #(0xFE0000|i) + BX LR (8 bytes each)
  }
}

N-Gage Package Format

N-Gage distribution files use standard MIME multipart/mixed encoding. The outer envelope is plain text with a boundary separator.

N-Gage Multipart Structure
-- MIME multipart/mixed envelope
Content-Type: multipart/mixed; boundary="<boundary-string>"

--<boundary>
Content-Type: text/xml              -- Part 0: retailer metadata (XML)
[XML body]

--<boundary>
Content-Type: application/sis       -- Part 1: main SIS installation package
[SIS binary data]

--<boundary>
Content-Type: application/sis       -- Part 2: partial upgrade (optional)
[SIS binary data]

--<boundary>--                        -- terminator

MIME Parts

Part Content-Type Description
0 text/xml Retailer metadata. Nokia N-Gage store info, game title, vendor. Namespace: http://www.n-gage.nokia.com/schemas/DPB/RetailerInfo/1.0
1 application/sis Main SIS v9.x installation package containing all game files
2+ application/sis Optional partial upgrade or augmentation packages
> Implementation Detail

MIME boundaries use both \r\n\r\n and \n\n header separators in the wild. Parsers must handle both. The boundary string is extracted from the Content-Type header and may or may not be quoted.

SIS v9.x Format (SISX)

SIS v9.x (introduced with Symbian OS 9.1) uses Type-Length-Value encoding with little-endian byte ordering throughout. The format supports deflate compression, digital signatures, and embedded sub-packages.

File Header

SIS Header (16 bytes)
struct SISHeader {
  uid1          : u32le       -- 0x10201A7A (fixed magic)
  uid2          : u32le       -- reserved
  uid3          : u32le       -- package UID
  uid_checksum  : u32le       -- checksum of UIDs
}                                -- total: 16 bytes

TLV Encoding

Every SIS field is encoded as a Type-Length-Value triplet. The type identifies the field, the length gives the data size in bytes, and the value is the raw payload. All fields are padded to 4-byte alignment.

TLV Field Structure
struct SISField {
  type          : u32le       -- field type identifier (see table below)
  length        : u32le       -- data length in bytes
                                  -- if MSB set: bits[31]=1, bits[4..30]=length, bits[0..3]=padding
                                  -- if MSB clear: length is value, padding = (4 - len%4) % 4
  data          : u8[length]  -- field payload
  _padding      : u8[0..3]   -- zero bytes to 4-byte boundary
}
SISArray Structure
struct SISArray {
  type          : u32le       -- always 2 (SISArray)
  length        : u32le       -- total array data size
  element_type  : u32le       -- type ID of contained elements
  elements[]    : {           -- repeated until length consumed:
    length      : u32le       --   element data size (NO type field)
    data        : u8[length]  --   element payload
    _padding    : u8[0..3]   --   align to 4-byte boundary
  }
}
> Array Optimization

Elements inside a SISArray omit the type field. The array header declares the element type once, then each element is just length + data + padding. This saves 4 bytes per element.

Field Type IDs

ID Name ID Name
0 Invalid 22 SISCertificateChain
1 SISString 23 SISLogo
2 SISArray 24 SISFileDescription
3 SISCompressed 25 SISHash
4 SISVersion 26 SISIf
5 SISVersionRange 27 SISElseIf
6 SISDate 28 SISInstallBlock
7 SISTime 29 SISExpression
8 SISDateTime 30 SISData
9 SISUid 31 SISDataUnit
11 SISLanguage 32 SISFileData
12 SISContents 33 SISSupportedOption
13 SISController 34 SISControllerChecksum
14 SISInfo 35 SISDataChecksum
15 SISSupportedLanguages 36 SISSignature
16 SISSupportedOptions 37 SISBlob
17 SISPrerequisites 38 SISSignatureAlgorithm
18 SISDependency 39 SISSignatureCertificateChain
19 SISProperties 40 SISDataIndex
20 SISProperty 41 SISCapabilities
21 SISSignatures

Structure Hierarchy

A SISX file has a fixed hierarchy. The root SISContents field contains a compressed controller (metadata/manifest) and the raw file data.

SISContents (Root)
struct SISContents {
  controller_checksum  : SISControllerChecksum?   -- optional CRC-16
  data_checksum        : SISDataChecksum?         -- optional CRC-16
  controller           : SISCompressed<SISController>
  data                 : SISData
}
SISController (Metadata/Manifest)
struct SISController {
  info              : SISInfo
  options           : SISSupportedOptions
  languages         : SISSupportedLanguages
  prerequisites     : SISPrerequisites
  properties        : SISProperties
  logo              : SISLogo?                -- optional
  install_block     : SISInstallBlock         -- file list + conditions
  signatures        : SISSignatureCertificateChain*  -- 0 or more
  data_index        : SISDataIndex            -- u32: index into SISData units
}
SISInfo (Package Identity)
struct SISInfo {
  uid              : SISUid                  -- u32: unique package identifier
  vendor_name      : SISString               -- UCS-2 encoded
  names            : SISArray<SISString>     -- one per supported language
  vendor_names     : SISArray<SISString>     -- one per supported language
  version          : SISVersion              -- {major, minor, build} as 3x u32
  creation_time    : SISDateTime
  install_type     : u8                      -- see Install Types table
  install_flags    : u8
}

Data Storage

File contents are stored in the SISData section, organized into data units. Each data unit corresponds to a controller and contains individually compressed files.

SISData / SISDataUnit / SISFileData
struct SISData {
  data_units    : SISArray<SISDataUnit>   -- one per controller
}

struct SISDataUnit {
  file_data     : SISArray<SISFileData>   -- actual file contents
}

struct SISFileData {
  data          : SISCompressed<bytes>    -- individually compressed file
}

Compression

SISCompressed Wrapper
struct SISCompressed {
  algorithm          : u32le       -- 0 = none, 1 = deflate (RFC 1951)
  uncompressed_size  : u64le       -- expected output size
  compressed_data    : u8[...]     -- deflate stream (zlib-wrapped)
}                                   -- total: 12 + compressed_data
> Compression Gotcha

Algorithm 1 uses standard zlib-wrapped DEFLATE. All tested N-Gage files decompress with zlib.decompress(data). Some third-party tools produce raw deflate streams (no zlib header) -- try wrapped first, fall back to raw (zlib.decompress(data, -15)) if needed.

File Descriptions

The SISInstallBlock within the controller lists file metadata (target path, hash, operation) while actual file contents live in SISData. The file_index field links descriptions to data.

SISInstallBlock
struct SISInstallBlock {
  files                : SISArray<SISFileDescription>  -- files to install
  embedded_controllers : SISArray<SISController>      -- embedded sub-packages
  if_blocks            : SISArray<SISIf>             -- conditional install rules
}
SISFileDescription
struct SISFileDescription {
  target              : SISString       -- install path, e.g. "c:\sys\bin\game.exe"
  mime_type           : SISString       -- MIME type
  capabilities        : SISCapabilities? -- only for executables
  hash                : SISHash
  operation           : u32le           -- 1=install, 2=run, 4=text, 8=null
  operation_options   : u32le
  length              : u64le           -- compressed size
  uncompressed_length : u64le
  file_index          : u32le           -- index into DataUnit file array
}

Target paths use Symbian drive letter notation. The ! drive means "user-selectable" and typically maps to c:.

Install Types

Value Name Description
0x00 Installation Standard full installation
0x01 Augmentation Removable addition to existing package
0x02 PartialUpgrade Patch without removing existing files
0x03 PreInstalledApp Pre-installed on media card
0x04 PreInstalledPatch Pre-installed patch

N-Gage Array Nesting

N-Gage packages frequently use nested array structures that differ from standard SISX. Where the spec describes SISArray<SISDataUnit>, N-Gage files may use SISArray<SISArray<SISDataUnit>>. The same applies to SISFileData arrays within data units.

Standard vs N-Gage Data Nesting
-- Standard SISX:
SISData -> Array<DataUnit> -> Array<FileData>

-- N-Gage variant:
SISData -> Array<Array<DataUnit>> -> Array<Array<FileData>>

Parsers must handle this by recursively unwrapping nested arrays until the target element type is found, with a depth limit (8 levels is sufficient).

Recursive Array Unwrapping
function unwrapNestedArrays(data, targetType, maxDepth) {
  if (maxDepth <= 0) return null;
  const arrayType = readU32(data, 0);
  if (arrayType === targetType) return data;
  if (arrayType === 2) {  // SISArray
    const length = readU32(data, 4);
    const inner = data.slice(8, 8 + length);
    return unwrapNestedArrays(inner, targetType, maxDepth - 1);
  }
  return null;
}

String Encoding

All SISString fields use UCS-2 encoding (UTF-16LE). The length field is in bytes, not characters. For a string with N characters, the byte length is 2*N. Strings may be null-terminated.

Common Pitfalls

  • Array elements omit the type field -- only length + data + padding
  • Padding: (4 - (position % 4)) % 4 zero bytes after each field
  • File indices are relative to the current controller's data unit, selected by data_index
  • Controllers may contain nested controllers (type 13 fields within type 13) -- merge their data
  • For embedded SIS files, absolute data index = sum of all parent controller indices (see below)
  • Compressed data uses raw DEFLATE, but N-Gage sometimes wraps it in zlib

Data Index Calculation

For embedded SIS files with nested controllers, the absolute data unit index is the sum of all parent controller indices. Each controller's data_index field gives its offset within the parent's data units:

Data Index Example
Controller A (data_index=0)         -- uses data_units[0]
  |
  +-- Controller B (data_index=1)    -- absolute = 0+1 = data_units[1]
  |
  +-- Controller C (data_index=2)    -- absolute = 0+2 = data_units[2]
        |
        +-- Controller D (data_index=1) -- absolute = 0+2+1 = data_units[3]

E32 Image Format

E32 is the Symbian OS executable format. N-Gage E32 images are ARM binaries (ARMv4/v5) with optional deflate or bytepair compression. The format uses the "EPOC" signature and a UID-based type system.

E32 Image Header

E32ImageHeaderV (156 bytes minimum)
struct E32ImageHeaderV {
  -- UIDs and identity (16 bytes)
  uid1              : u32le       -- 0x1000007A (EXE) or 0x10000079 (DLL)
  uid2              : u32le       -- secondary UID
  uid3              : u32le       -- application UID
  uid_checksum      : u32le       -- CRC of uid1..uid3

  -- Header metadata (16 bytes)
  signature         : u8[4]       -- "EPOC" 0x434F5045
  header_crc        : u32le       -- CRC-32 of header
  module_version    : u32le       -- major.minor.build packed
  compression_type  : u32le       -- see compression table

  -- Build info (12 bytes)
  tools_version     : u32le
  time_lo           : u32le       -- build timestamp (low 32 bits)
  time_hi           : u32le       -- build timestamp (high 32 bits)

  -- Flags and sizes (28 bytes)
  flags             : u32le       -- EXE/DLL, ABI version, debug, SMP-safe
  code_size         : u32le       -- size of code section
  data_size         : u32le       -- size of initialized data
  heap_size_min     : u32le
  heap_size_max     : u32le
  stack_size        : u32le
  bss_size          : u32le       -- uninitialized data (zeroed at load)

  -- Addresses (12 bytes)
  entry_point       : u32le       -- offset from code_base
  code_base        : u32le       -- base address for code
  data_base         : u32le       -- base address for data

  -- Import/Export info (16 bytes)
  dll_ref_count     : u32le       -- number of imported DLLs
  export_offset     : u32le       -- offset to export table
  export_count      : u32le       -- number of exported functions
  text_size         : u32le       -- size of text section within code

  -- Section offsets (20 bytes)
  code_offset       : u32le       -- file offset to code
  data_offset       : u32le       -- file offset to data
  import_offset     : u32le       -- file offset to import table
  code_reloc_offset : u32le       -- file offset to code relocations
  data_reloc_offset : u32le       -- file offset to data relocations

  -- CPU and priority (4 bytes)
  priority          : u16le       -- process priority (350 = foreground)
  cpu_identifier    : u16le       -- target CPU (see CPU types)
}                                   -- total: 124 bytes (0x7C) base header

Field Offsets

Offset Size Field
0x00 4 uid1
0x04 4 uid2
0x08 4 uid3
0x0C 4 uid_checksum
0x10 4 signature ("EPOC")
0x14 4 header_crc
0x18 4 module_version
0x1C 4 compression_type
0x20 4 tools_version
0x24 8 timestamp (lo + hi)
0x2C 4 flags
0x30 4 code_size
0x34 4 data_size
0x38 4 heap_size_min
0x3C 4 heap_size_max
0x40 4 stack_size
0x44 4 bss_size
0x48 4 entry_point
0x4C 4 code_base
0x50 4 data_base
0x54 4 dll_ref_count
0x58 4 export_offset
0x5C 4 export_count
0x60 4 text_size
0x64 4 code_offset
0x68 4 data_offset
0x6C 4 import_offset
0x70 4 code_reloc_offset
0x74 4 data_reloc_offset
0x78 2 priority
0x7A 2 cpu_identifier

UID1 Values

UID1 Type Description
0x1000007A EXE Symbian executable
0x10000079 DLL Symbian dynamic library
0x10000037 APP Symbian application (legacy)

Compression Types

Value Algorithm Description
0x00000000 None Uncompressed image
0x101F7AFC Deflate zlib deflate (Symbian UID for deflate algorithm)
0x102822AA Bytepair Bytepair encoding (Symbian UID for bytepair algorithm)

When compressed, the header is stored uncompressed and everything after header_size bytes is the compressed payload. After decompression, section offsets within the code/data/import regions are relative to the start of the decompressed data, not the original file offsets.

Flags

Bit(s) Mask Name
0 0x0001 DLL (0=EXE, 1=DLL)
1 0x0002 Fixed address
3-4 0x0018 ABI version (0x08=v1, 0x18=v2)
7 0x0080 Export-only DLL
8 0x0100 Debuggable
9 0x0200 SMP-safe

CPU Types

Value Architecture
0x1000 x86
0x2000 ARM
0x2001 ARMV4 (most N-Gage DEFLATE-compressed binaries)
0x2002 ARMV5
0x2003 ARMV6
0x3000 MIPS
0x4000 WINS (Windows emulator)
0x5000 M*CORE

Import Section

The import section starts at import_offset with a 4-byte total size, followed by dll_ref_count import block headers, then the ordinal arrays and DLL name strings.

Import Section Layout
struct E32ImportSection {
  total_size    : u32le           -- size of entire import section
  blocks        : E32ImportBlock[dll_ref_count]
  -- followed by ordinal arrays and DLL name strings
}

struct E32ImportBlock {
  dll_name_offset  : u32le        -- offset from import section start to DLL name
  num_imports      : u32le        -- number of imported ordinals
}                                   -- total: 8 bytes per DLL

DLL names at the referenced offset may be length-prefixed (first byte = string length) or null-terminated. Parsers should try both.

Bytepair Decompression

Bytepair compression uses a substitution table. The decompressor reads a marker byte indicating how many pair entries follow, then expands each byte through the pair table during output.

Bytepair Header
struct BytepairHeader {
  _reserved       : u32le[3]    -- 12 bytes reserved
  unpacked_size   : u32le       -- decompressed output size
  marker          : u8          -- 0 = 256 pairs, else = pair count
  pair_table      : u8[marker*2] -- substitution pairs
  compressed_data : u8[...]     -- bytes referencing pair table
}
> Compressed Section Offsets

After decompression, section layout changes. Code starts at offset 0 of the decompressed data, data section follows at code_size, and the import section follows at code_size + data_size. Do not use the original file offsets from the header for decompressed data.

Export Section

The export table is an array of export_count function addresses, each a 32-bit offset from code_base. Ordinals are 1-based: export ordinal N is at export_offset + (N-1) * 4.

Export Table
struct E32ExportTable {
  addresses     : u32le[export_count]  -- function addresses relative to code_base
}

Extracted File Identification

Files extracted from SIS data units can be identified by their first bytes:

Magic Bytes Type Extension
0x1000007A (UID1) Symbian EXE .exe
0x10000079 (UID1) Symbian DLL .dll
0x10003A3F (UID1) Symbian APP .app
89 50 4E 47 PNG image .png
FF D8 FF JPEG image .jpg
42 4D BMP image .bmp
52 49 46 46 RIFF (WAV) .wav
4D 54 68 64 MIDI .mid
3C 3F 78 6D 6C XML .xml
58 45 33 55 S3E binary .s3e
5D LZMA compressed .lzma

S3E Format (Marmalade SDK)

Marmalade SDK (formerly Ideaworks3D) games on N-Gage use a two-stage loading architecture. The E32 executable is a thin runtime loader compiled from Marmalade SDK sources. The actual game code lives in a separate .s3e binary with the "XE3U" magic. Both MGS Mobile and Resident Evil: Degeneration on N-Gage use this format.

XE3U Header

S3E Binary Header (68+ bytes)
struct S3EHeader {
  magic           : u8[4]       -- "XE3U" 0x55335845
  version         : u32le       -- major.minor.patch packed (e.g. 4.14.1)
  flags           : u16le
  arch            : u16le       -- 0=ARMv4t, 1=ARMv4, 2=ARMv5t, 3=ARMv5te
  fixup_offset    : u32le       -- offset to fixup/relocation section
  fixup_size      : u32le       -- total fixup section size
  code_offset     : u32le       -- offset to code+data blob
  code_file_size  : u32le       -- bytes on disk (code + initialized data)
  code_mem_size   : u32le       -- code_file_size + BSS
  sig_offset      : u32le       -- signature section
  sig_size        : u32le
  entry_offset    : u32le       -- entry point relative to base_addr
  config_offset   : u32le       -- offset to embedded config text (INI-like)
  config_size     : u32le
  base_addr       : u32le       -- original link-time base address
  extra_offset    : u32le
  extra_size      : u32le
  ext_hdr_size    : u32le       -- extended header size
  data_offset     : u32le       -- if ext_hdr_size >= 8: data section offset
}
> Architecture Mismatch

The header declares "ARMv4t" but the code section contains Thumb-2 instructions (ARMv6T2+). Old Thumb BL prefix/suffix encoding (two 16-bit halves) and Thumb-2 BL (single 32-bit instruction) produce the same result when J1=J2=1, so the code runs correctly on ARMv6+ despite the v4t label.

Memory Layout

The S3E loader relocates the binary from its link-time base_addr to a runtime load address. Typical layout:

Runtime Memory Map
-- Typical memory layout after S3E load
0x00000000 - 0x00000FFF  null page (writes dropped)
0x00001000 - 0x00001007  halt stub: NOP + B . (infinite loop)
0x00100000 - code end   code section (write-protected)
code end   - data end   initialized data section (RW)
data end   - mem end    BSS (zeroed at load, RW)
mem end+4K - stubs end  S3E API stubs (SVC + BX LR pairs)
0x00300000              framebuffer (176x208, 16bpp = 73216 bytes)
0x02000000              heap base (malloc arena)

Fixup Section

The fixup section contains relocations and the S3E API symbol table. It is organized as a sequence of sub-sections, each with a type and size header.

Fixup Sub-section Header
struct FixupSection {
  type    : u32le       -- 0=symbols, 1=internal relocs, 2-4=external relocs
  size    : u32le       -- total section size (including this header)
  data    : u8[size-8]  -- section-specific payload
}
Type Name Payload Structure
0 Symbol table u16le count, then count null-terminated ASCII strings (S3E API names)
1 Internal relocations u32le count, then count u32 offsets. Each points to a word needing base-address fixup
2 ARM external relocations u32le count, then count entries of {u16 offset_hi, u16 offset_lo, u16 symbol_index}. Patches ARM BL instructions
3-4 Thumb external relocations Same structure as type 2. Patches Thumb BL/BLX instructions

API Stub Generation

For each symbol in the symbol table, the loader creates a two-instruction ARM stub at a fixed address above the BSS:

S3E API Stub (8 bytes each)
-- stub[i] at stubBase + i*8:
0xEFFExxxx    SVC #(0xFE0000 | i)   -- trap to HLE handler
0xE12FFF1E    BX  LR                -- return to caller

External relocations then patch the original BL/BLX call sites in the code section to branch to these stubs instead.

ARM BL Relocation (Type 2)

ARM BL instructions encode the branch offset in 24 bits (signed, shifted left 2). The relocation replaces the offset to target the API stub:

ARM BL Patching
-- ARM BL instruction encoding:
--   bits[31:28] = condition (preserved from original)
--   bits[27:25] = 101 (branch opcode)
--   bit[24]     = L (1 for BL, 0 for B)
--   bits[23:0]  = signed offset / 4

stubAddr  = stubBase + symbolIndex * 8
callAddr  = loadBase + relocOffset
offset24  = (stubAddr - (callAddr + 8)) >> 2  -- ARM pipeline: PC = addr + 8
newInstr  = (origInstr & 0xF0000000) | 0x0B000000 | (offset24 & 0x00FFFFFF)

Thumb BL/BLX Relocation (Type 3-4)

Thumb BL uses two 16-bit halfwords. The offset is split across them:

Thumb BL Patching
-- Thumb BL encoding (two halfwords, little-endian in memory):
--   halfword[0] = 0xF000 | offset_hi[10:0]   (prefix)
--   halfword[1] = 0xE800 | offset_lo[10:0]   (suffix, BLX)

offset    = stubAddr - (callAddr + 4)          -- Thumb pipeline: PC = addr + 4
offset_hi = (offset >> 12) & 0x7FF
offset_lo = (offset >> 1)  & 0x7FF

-- Written as bytes (little-endian):
mem[addr+0] = offset_hi & 0xFF
mem[addr+1] = 0xF0 | ((offset_hi >> 8) & 0x07)
mem[addr+2] = offset_lo & 0xFF
mem[addr+3] = 0xE8 | ((offset_lo >> 8) & 0x07)

Internal Relocation

Internal relocations adjust pointers when the binary is loaded at a different base address than it was linked for. Each relocation entry is a 4-byte offset within the code+data blob. The loader reads the u32 at that offset and adds the relocation delta (loadBase - base_addr).

Embedded Configuration

S3E binaries embed an INI-format configuration block at config_offset. This contains Marmalade project settings in [section] / key=value format. The runtime queries this for display size, capabilities, and game-specific parameters.

S3E API Surface

MGS Mobile imports 159 S3E symbols: 72 GL/EGL functions and 87 S3E runtime functions covering file I/O, audio, input, display, memory, and device queries.

Category Count Key Functions
GL/EGL 72 eglInitialize, eglCreateContext, eglSwapBuffers, glDrawElements, glTexImage2D, glMatrixMode, ...
File I/O 16 s3eFileOpen, s3eFileRead, s3eFileWrite, s3eFileSeek, s3eFileTell, s3eFileGetSize, ...
Audio 8 s3eAudioPlay, s3eAudioStop, s3eAudioSetInt, ...
Sound 9 s3eSoundChannelPlay, s3eSoundSetInt, ...
Device 6 s3eDeviceGetInt, s3eDeviceYield, s3eDeviceExit, ...
Surface 5 s3eSurfaceShow, s3eSurfaceGetInt, s3eSurfaceSetup, ...
Memory 5 s3eMemoryMalloc, s3eMemoryFree, s3eMemoryRealloc, ...
Keyboard 4 s3eKeyboardGetState, s3eKeyboardSetInt, ...
Timer 3 s3eTimerGetMs, s3eTimerGetUST, ...
Other 31 Camera, Compression, Config, Debug, Ext, SecureStorage, Vibra

User File System Callbacks

S3E games can register custom file system handlers through the UFS (User File System) callback table. This allows the game engine to intercept all file I/O and redirect it through custom archive formats (e.g. DTRZ).

> Callback Table Order (Marmalade v4.14.1)

The UFS callback registration order is non-obvious and does not match the standard fopen/fclose/fread ordering: Open, Read, Close, Write, Tell, Seek, EOF, GetSize, Exists. Note that Read/Close and Tell/Seek are swapped relative to the standard C library ordering. The Read callback receives arguments as (buf, elemSize, count, handle) with the handle in r3, matching the fread convention.

LZMA-Compressed S3E

Some S3E binaries (e.g. Resident Evil: Degeneration) are LZMA-compressed. The compressed S3E starts with the LZMA magic byte 0x5D instead of "XE3U". Decompress first, then parse the XE3U header from the result.

DTRZ Archive Format

Marmalade games pack game assets into DTRZ archives, loaded through the S3E UFS callback system. The DTRZ reader is initialized on the first file read operation, which involves decompressing the archive index (roughly 4 million ARM instructions for initialization on MGS Mobile). Subsequent reads are faster as the index is cached.

The UFS callback table is registered by the game binary at startup. When the S3E runtime calls s3eFileOpen, the registered Open callback receives the path and returns a handle. The handle structure includes a pointer at offset +0x20 to internal DTRZ data, which the Read callback dereferences via LDR r0, [r3, #0x20].

Build Information

N-Gage Marmalade games include .mmp build files (Symbian project files) that reveal the SDK configuration.

Setting Value
Libraries arenaenv, euser, libc, libdl, libm, libpthread, libz, runtime, ngiutil
Macros I3D_ARCH_ARM, I3D_OS_EPOC, I3D_OS_NGI, S3E_NGI_VERSION=112
Heap 256KB minimum, 16MB maximum
Stack 20KB

Format Detection

Quick identification of N-Gage related binaries by magic bytes:

Magic / UID1 Format Description
Content-Type: multipart/mixed N-Gage package MIME multipart container
0x10201A7A SISX SIS v9.x installation package
0x1000007A E32 EXE Symbian executable
0x10000079 E32 DLL Symbian dynamic library
XE3U (0x55335845) S3E Marmalade SDK binary
0x5D (first byte) LZMA-compressed S3E Decompress first, then check for XE3U
EPOC (at offset 16) E32 signature Confirms E32 header validity

Further Reading

Specifications

  • Symbian OS Internals (Wiley) - E32 image format details
  • Nokia N-Gage SDK documentation
  • Marmalade SDK documentation - S3E binary format and UFS API

Implementations

  • Symbian Seance - Browser-based ARM emulator with S3E HLE
  • EKA2L1 - Symbian OS emulator (E32 loader reference)