Nokia N-Gage and Symbian File Formats
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.
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 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.
-- 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 |
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
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.
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
}
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
}
}
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.
struct SISContents {
controller_checksum : SISControllerChecksum? -- optional CRC-16
data_checksum : SISDataChecksum? -- optional CRC-16
controller : SISCompressed<SISController>
data : SISData
}
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
}
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.
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
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
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.
struct SISInstallBlock {
files : SISArray<SISFileDescription> -- files to install
embedded_controllers : SISArray<SISController> -- embedded sub-packages
if_blocks : SISArray<SISIf> -- conditional install rules
}
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 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).
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)) % 4zero 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:
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
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.
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.
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
}
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.
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
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
}
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:
-- 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.
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:
-- 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 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 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).
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)