Skip to content

GDT

Level: 201 · for anyone with a hex editor open

One line: A .gdt is a Java serialization stream carrying a 44-byte block — a magic, a version, two writeUTF strings, a type and a length — followed by a ZIP with one entry and no central directory, which unzip and Python's zipfile both refuse and raw zlib inflates without complaint.

What the loader reads

A Ghidra Data Type Archive is Ghidra's own file: a database of C types — windows_vs12_64.gdt, mac_osx.gdt, generic_clib_64.gdt — packed into one file by ItemSerializer.outputItem, the same routine that writes a GZF and a GZT. There is no specification but the source, and the source is short: an ObjectOutputStream writes a long, an int, two strings, an int and a long; then a ZipOutputStream writes one deflated entry named FOLDER_ITEM; then the file is closed. What that produces, byte by byte:

offset bytes what wrote it
0 ac ed 00 05 ObjectOutputStream's stream header: STREAM_MAGIC, STREAM_VERSION 5
4 77 TC_BLOCKDATA: a block of primitive data follows
5 one byte the block's length — 44 for an item named DTArchive, 42 for one named Archive
6 8 bytes MAGIC_NUMBER, 0x2e30212634e92c20, big-endian
14 4 bytes FORMAT_VERSION, 1
18 2 + n writeUTF(itemName): a 16-bit byte count, then modified UTF-8
2 + n writeUTF(contentType): Archive for a GDT, Program for a GZF, Trace for a GZT
4 fileType
8 length: the size of the database before compression
6 + block 50 4b 03 04 a ZIP local file header, flags 0x0808, method 8, sizes zero
the deflated database, then 50 4b 07 08 and a data descriptor — and then the file ends

In Python

Verified output of gdt_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. A JAVA SERIALIZATION STREAM, THEN ONE BLOCK OF DATA
------------------------------------------------------------------------
   ac ed 00 05 77 2c 2e 30 21 26 34 e9 2c 20 00 00
   00 01 00 09 44 54 41 72 63 68 69 76 65 00 07 41
   72 63 68 69 76 65 00 00 00 00 00 00 00 00 00 00 10 00

   bytes 0..4   ac ed 00 05   STREAM_MAGIC ac ed, STREAM_VERSION 5: ObjectOutputStream's header
   byte 4       0x77          TC_BLOCKDATA: a block of primitive data follows
   byte 5       0x2c = 44     its length in bytes
   bytes 6..14  2e 30 21 26 34 e9 2c 20   MAGIC_NUMBER 0x2e30212634e92c20, big-endian
   then         FORMAT_VERSION 1
                writeUTF item name     b'DTArchive'   (9 bytes, after a 2-byte count)
                writeUTF content type  b'Archive'
                fileType 0   length 4096 = the payload's size before compression
   block ends at 50 = 6 + 44

   writeLong, writeInt, writeUTF: Java's DataOutput, big-endian, with
   writeUTF counting bytes of modified UTF-8 in a 16-bit field. The
   whole 44 bytes travel as one serialization block.

2. THEN A ZIP WITH ONE ENTRY AND NO CENTRAL DIRECTORY
------------------------------------------------------------------------
   at 50: local header 0x04034b50   flags 0x0808   method 8 (deflate)   entry b'FOLDER_ITEM'
   crc 0x0  sizes 0/0: all zero, because flag bit 3 says a data descriptor follows the data
   PK\1\2 (central directory) present: False   PK\5\6 (end record) present: False
   last 16 bytes  50 4b 07 08 18 ba fd 13 1e 00 00 00 00 10 00 00   the descriptor: PK\7\8, crc, compressed size, size

   ItemSerializer calls closeEntry() and flush() on a ZipOutputStream
   and never finish(): the central directory is never written. A
   stream reader that walks local headers does not need one; an
   archive reader that starts from the end cannot find anything.

3. WHO CAN OPEN IT
------------------------------------------------------------------------
   isPackedFile (8 bytes at offset 6 == magic)   True
   zipfile.ZipFile on the ZIP part               refused: BadZipFile
   zlib, raw deflate from byte 91             4096 bytes out; descriptor crc matches: True; size matches: True
   the payload begins                            b'/01,4),*'

   Same bytes, three verdicts. The Java stream reader Ghidra uses
   reads entries in order and is satisfied; unzip and zipfile look
   for the end record first and give up; zlib does not know what a
   ZIP is and inflates what it is pointed at.

4. WHAT A REAL ONE HOLDS
------------------------------------------------------------------------
   item 'DTArchive'  content type 'Archive'   block length 44   length field 0xcac000 = 13,287,424 bytes unpacked
   item 'Archive'    content type 'Archive'   block length 42   length field 0x74000 = 475,136 bytes unpacked

   The sixteen .gdt files Ghidra 12.1.3 ships all open ac ed 00 05 77
   and then 2c or 2a -- 44 or 42 -- because 'DTArchive' is two bytes
   longer than 'Archive'. The block length is the only byte that
   varies before the magic, and the magic is what the loader reads.

Measured: the sixteen archives Ghidra ships

Measured 2026-09-13 — Ghidra 12.1.3 (brew), the first bytes of every .gdt under Features/Base/data/typeinfo, and one of them taken apart with struct and zlib. Not machine-checked: the files are Ghidra's
all 16 files      ac ed 00 05 77 2c|2a  2e 30 21 26 34 e9 2c 20  00 00 00 01 ...

mac_osx.gdt       3,272,587 bytes on disk
  block length 44   magic 0x2e30212634e92c20   version 1
  itemName 'DTArchive'   contentType 'Archive'   fileType 0   length 13,287,424
  ZIP local header at 50: flags 0x0808, method 8, entry 'FOLDER_ITEM', crc/sizes 0
  raw inflate from byte 91: 13,287,424 bytes; descriptor crc b94c5f5e matches; length matches
  PK\1\2 present: False   PK\5\6 present: False
  unzip:   End-of-central-directory signature not found
  zipfile: BadZipFile
  file(1): Java serialization data, version 5     (file-5.41 and file-5.45 alike)
  the inflated payload begins  2f 30 31 2c 34 29 2c 2a   '/01,4),*'

Nine of the sixteen are named DTArchive inside and seven Archive, which is the whole of why the block-length byte is 2c in some and 2a in others. file(1) reads the first four bytes and stops, correctly: it is Java serialization data, and everything Ghidra-specific begins at byte 6.

A ZIP with no end

Section 2 is the finding. ItemSerializer calls closeEntry() and flush() on its ZipOutputStream and never finish() or close(), so the central directory and the end-of-central-directory record are never written — the file stops after the data descriptor. Java's own ZipInputStream does not mind, because it reads local headers in order and never looks for the end. Every reader that starts from the end, which is every archive tool (Android APK is the page on why), finds nothing: unzip says the signature is missing, zipfile raises. Section 3 opens the same bytes three ways, and zlib — which knows nothing of ZIP and inflates from wherever it is pointed — gets the whole database back with the CRC intact.

So the file is a ZIP by the letter of the local-header format and not a ZIP to any ZIP program, which is a version of binary is a verdict: whether these bytes are an archive is a fact about the reader.

The strings are Java's

writeUTF is DataOutput's: a 16-bit count of bytes, then modified UTF-8 — the Java class file encoding, with NUL as c0 80 and an emoji as six bytes. For a GDT the two strings are ASCII and the distinction is invisible; the GZF page, where the item name is the program's, works it through.

What Ghidra checks

GdtLoader accepts a file when its name ends in .gdt and ItemSerializer.isPackedFile says yes — which skips six bytes, reads eight as a big-endian long, and compares with MAGIC_NUMBER. Nothing before offset 6 and nothing after 14 is examined; the GZT page measures what that leaves out. The loader's name in 12.1.3 is Ghidra Data Type Archive Format, and importing a .gdt produces a data type archive in the project, not a program.

If you are coming from Python or ABAP

Python. There is no ObjectInputStream in the standard library and none is needed: the block is struct.unpack_from('>QI', data, 6), two '>H'-prefixed strings, and '>iq'. For the ZIP part do not reach for zipfile — it will raise — but for zlib.decompressobj(-15), which is raw deflate with no header; its unused_data afterwards is the data descriptor, and zlib.crc32 over the output should equal the descriptor's second word. zlib.compress(b)[2:-4] is the matching writer: strip zlib's two-byte header and four-byte trailer and what is left is what a ZIP stores.

ABAP. (Not machine-checked — CI cannot run ABAP.) cl_abap_zip expects a central directory and will not open this file; cl_abap_gzip speaks gzip and zlib, not raw deflate, so neither standard class reaches the payload. The shape to recognise is a stream format that a streaming reader handles and a random-access reader does not — an IDoc file without a control-record index has the same property — and the fix is to read it in order, not to look for an index that was never written.

Try it

  1. xxd -l 64 any .gdt under your Ghidra's Features/Base/data/typeinfo/. Read the block length at byte 5 and the item name after byte 18.
  2. file it. Then unzip -l it. Then, in Python, inflate it with zlib.decompressobj(-15) from the byte after FOLDER_ITEM.
  3. Compare the length field in the block with the number of bytes the inflate returns.
  4. strings -n 8 payload | head on the inflated bytes. The first eight are the buffer file's own magic.
  5. Rename a .gdt to .gzf and offer it to Ghidra's importer. The loader that accepts it will say what it found inside.

See also