Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
reference/*
research/*
tests/samples/*
target/*
*.log
tests/samples/*
!tests/samples/bcb6/
!tests/samples/d13/
183 changes: 183 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,188 @@
# Changelog

## 0.4.0

Capability release. undelphi now reads Delphi's compiled units (`.dcu`,
`.dcp`) as well as binaries, places a program's DCU declarations in its
binary, reads and writes IDR knowledge bases and builds them from DCUs, and
names what a binary imports from runtime packages. Delphi 2010+ metadata
(attributes, record methods, indexed properties) and Delphi 13 are covered,
and `structure_extents` tells a disassembler where the metadata lies in the
code section. A few API changes break compatibility; see **Changed**.

### Added

- **Compiled units.** The `dcu` module decodes Delphi compiled units
(`.dcu`, `dcu::Unit`) and compiled packages (`.dcp`, `dcu::package`) from
Delphi 2 to 13: every declaration of the interface and implementation,
every type with its full layout (records without RTTI, private class
fields, enumeration names, procedural types), routines with parameters,
results and calling conventions, uses lists with imported symbols, and the
type and address tables that resolve every cross-reference.
`Unit::instance_size` gives a class's VMT instance size (the declared size
plus the Delphi 2009+ monitor slot). The records added after 10.3 are read
too: `Modifier::Documentation` (XML documentation comments),
`Modifier::DeclaredType` with `TypeSpelling` (the type as the source spells
it), `spellings` on `Modifier::TemplateParams` and
`TypeKind::TemplateCall`, the C++ header records (`Modifier::CppFlags`,
`Modifier::CppNames`) and `Unit::image_base`. Verified on every unit of
C++Builder 6, RAD Studio 10 Seattle and RAD Studio 13 (Win32, Win64,
Win64x), and fact for fact against DCU32INT. `dcu::library::Library`
indexes a compiler's unit directories and resolves types across units.
Examples `dcu_check`, `dcu_dump`, `dcu_facts`.
- **DCU code.** `dcu::Unit::block` keeps a unit's code and data block,
`Unit::memory` assigns each routine, typed constant and RTTI record its
part of it, and `Unit::fixup_size` gives each relocation's width, so a DCU
routine can be matched against a binary byte for byte.
- **Export names.** `dcu::mangle::Mangler` generates the mangled export name
of a DCU routine - Borland on Win32 (pre-2009 and modern dialects),
Itanium on Win64 (`dcu::itanium`) - so each export of a runtime package
binds to its declaration, overloads and generic instances included: 98 -
99 % of Seattle's and Delphi 6's exports, 99.6 % of Delphi 13's Win32
exports with a generated routine path and 98.3 % of its Win64 exports.
Long Borland names are shortened as the compiler does (MD5, base64).
Example `mangle_check`.
- **Locating DCU declarations in a binary.** `DelphiBinary::locate` (and
`dcu::locate::Locator` underneath) places a program's routines, typed
constants, RTTI records, variables and thread variables in its binary with
the compiler's units: each routine is verified byte for byte, and its
relocations (`dcu::locate::Encoding`) lead to the next. It names the unit
initialization table's slots (`dcu::locate::InitOwner`: a unit or a class
constructor pair) and the thread variables with their TLS offsets
(`Location::thread_variables`). On the fixture programs about 4 300
(Delphi 13) and 1 200 (Delphi 6) placements per program check against the
linker maps, and none is wrong.
- **IDR knowledge bases.** The `kb` module reads IDR's `kb*.bin` /
`syskb*.bin`: `KnowledgeBase::parse` reads one without copying - units,
constants, types (fields, properties, methods), variables, resource
strings and routines (prototype, code, relocation mask, named fixups) -
with name and unit lookups through the file's own indexes.
`Contents::to_bytes` writes the format byte for byte as IDR did (the
lookup orders sorted as Delphi's `TList.Sort` sorts); every published file
reads and writes back unchanged. `Dump::matches` tests code against a
record ignoring relocated bytes. `kb::build` makes the records from a
release's DCUs the way IDR's maker did - DCU32INT's type notation, the
maker's value format and its naming of compiler-generated declarations;
built from the Delphi 6 library they match kb6's wherever the units are
the same. Examples `kb_dump`, `kb_build`, `kb_diff`.
- **Runtime-package imports and exports.** The `imports` module and
`DelphiBinary::{imports, import_at, required_packages, external_parent,
field_class_import, exports}` name the types, classes and routines a
binary imports from runtime packages (or a package exports) by demangling
Borland and Itanium package symbols (`@$xp$14System@Integer`,
`@Classes@TPersistent@`, `_ZTVN6System7Classes11TPersistentE`), delay-loaded
imports included (`Import::is_delayed`). `DetectionSource::PackageImports`
identifies the compiler from an `rtlNN.bpl` / `vclNN.bpl` import and
`DetectionSource::CppRuntime` from the C++ runtime's markers.
- **Delphi 2010+ binary metadata.** `DelphiBinary::decode_attribute`
resolves an attribute's type and constructor and decodes its arguments;
attributes are exposed on fields (`ExtendedField::attributes`), properties
(`ExtendedProperty::attributes`), methods and parameters
(`MethodSignature::attributes`, `MethodParam::attributes`), records and
record fields. Records carry their methods, constructors and operators with
signatures (`RecordInfo::methods`, `rtti::RecordMethod`) and a managed
record's operator table (`rtti::RecordOperators`); `tkMRecord` decodes as a
record. `DelphiBinary::indexed_properties` reads indexed (array) properties
and `DelphiBinary::extended_methods` the extended-method entries, whose
flags decode through `signatures::ExtendedMethodInfo`. Delphi 13 is
detected by name (`Delphi 13 Florence`, `rtl370.bpl`).
- **Runtime tables.** `DelphiBinary::unit_init_table` decodes the unit
initialization table (`unitinit`): each unit's initialization and
finalization in startup order, Delphi 2010+ class constructor pairs, and
the 2010+ type table. `DelphiBinary::resource_strings` joins the string
table with each `resourcestring` record (`resstrings`), and
`DelphiBinary::interface_rtti` decodes the method table of an interface
compiled with method info (`intfrtti`).
- **Published-field and automation tables.** `DelphiBinary::field_class` /
`fields::Field::class_table` resolve legacy published fields through the
field class table, so `field_type` and `field_type_detail` type them (0 ->
100 % of published fields on the Delphi 7 and Delphi 12 samples).
`DelphiBinary::auto_entries` / `vmttables::AutoEntry` decode the
`vmtAutoTable` of `automated` sections.
- **Structure extents.** `DelphiBinary::structure_extents` returns where each
structure the crate reads lies in the image (`extents::StructureExtent`,
`extents::StructureKind`): VMT headers, virtual-method arrays, class names,
the published method, field, field class, dynamic, interface and
automation tables, interface vtables and RTTI records, each measured by the
walk that decodes it, plus the string constants (`StringLiteral`),
`resourcestring` records, the unit initialization table and GUID constants
Delphi places in the code section - so a disassembler can keep them out of
the code it decodes.
- **Own compiled fixtures.** C++Builder 6 / Delphi 6 programs, units and
packages (`tests/fixtures/bcb6/`, samples in `tests/samples/bcb6/`) and
Delphi 13 Win32 / Win64 builds of them plus `modernbin.exe` and `modern`
units for the 2010+ metadata (`tests/fixtures/d13/`, `tests/samples/d13/`),
with build scripts, linker maps and toolchain provenance;
`tests/programs.rs`, `tests/modern_rtti.rs`, `tests/dcu.rs`,
`tests/locate.rs`, `tests/extents.rs` and `tests/kb.rs` assert against them.

### Changed

- `signatures::MethodParam`, `rtti::SignatureParam` and `rtti::RecordField`
are no longer `Copy` (they carry attributes).
- `ExtendedProperty::attributes` and `ExtendedField::attributes` are decoded
`Vec<AttributeEntry>` fields, like those of records, methods and
parameters, instead of methods taking a pointer size;
`ExtendedProperty::attributes_raw` is gone and `ExtendedField` is no longer
`Copy`. Every `TAttrData` block is read by one strict reader
(`extrtti::read_attr_data`).
- Removed `AttributeEntry::iter_at` and `iter_class_attributes` (a greedy
scan nothing used) and `Visibility::Unknown` (never produced).
- Indexed-property accessors resolve by table position:
`DelphiExtMethod::index` is an entry's position in the extended-method
table, and `ArrayProperty::getter` / `setter` look `read_index` /
`write_index` up by it, so an unreadable entry no longer shifts the ones
after it.
- `AccessTarget::UnresolvedSlot` carries the VMT byte offset unchanged as a
`u64` (it was truncated to `u16`).
- Identifier checks accept UTF-8 letters, as Delphi 2009+ writes
identifiers; a non-ASCII name no longer ends a class, field, method,
property or record walk.
- The extended-RTTI accessors of `DelphiBinary` (`extended_properties`,
`extended_fields`, `extended_methods`, `indexed_properties`,
`class_attributes`, and the extended source of `method_signatures`) return
nothing when the build string or RTL package dates the compiler before
Delphi 2010, which the Delphi 2009 VMT header alone cannot tell apart.
- `DelphiBinary::string_layout` and `import_at` are cached and indexed, and
`ClassSet::find_by_type_info` finds a class by its type info:
`decode_attribute` is about six times faster on large binaries. Naming the
unit initialization table (`DelphiBinary::locate`) checks variable layout
by binary search, and thread-variable offsets past the TLS template are
rejected as misreads.
- `AccessKind::Virtual` values are VMT byte offsets (as stored on disk), and
`Access::resolve` resolves them correctly; it previously treated them as
slot indexes.
- A Delphi getter / setter of 0 decodes as `AccessKind::None` and a
`stored True` / `False` constant as `AccessKind::Const` (1 / 0), not
`Static`; `code_entrypoints` no longer emits entrypoints at VA 0 / 1.
- C++Builder output is reported as `Compiler::CppBuilder` (detected from
`fb:C++HOOK` / `___CPPdebugHook`).
- `EnumInfo::unit_name` is `None` when no plausible unit name follows the
values (C++Builder `bool`); nameless `tkMethod` records (C++Builder
`__closure` types) are accepted.
- The unit init table's documentation states that from Delphi 2010 its
entries include class constructor / destructor pairs, not only units.

### Fixed

- Win64 `TObject` header virtuals were named three slots off: Win64 puts the
`CPP_ABI_1` .. `CPP_ABI_3` virtuals in front of `Destroy`, so the slot
named `Destroy` was a C++ ABI stub (every Win64 binary since XE2). Delphi
2009+'s `ToString`, `GetHashCode` and `Equals` slots are now named too.
- A class without virtual methods of its own no longer reports the data
after its VMT as a virtual method.
- `tkProcedure` signatures with two or more parameters decode (the
per-parameter attribute block was not skipped).
- A class with attributes but no extended properties keeps its attributes.
- Win64 automation tables (`automated` sections) decode.
- A typeinfo import in the C++ global namespace (`@$xp$7Boolean`) demangles,
with an empty unit.
- Form `Extended` values (`DfmValue::as_f64`) round to the nearest `f64`
instead of scaling through `powi`, so subnormal results and values near
`f64`'s limits convert exactly.
- `Idr64.exe` is identified as C++Builder 6 (it is a C++Builder project), not
Delphi.

## 0.3.3

Maintenance release. No API or behavior changes.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
[package]
name = "undelphi"
version = "0.3.3"
version = "0.4.0"
edition = "2024"
rust-version = "1.88"
description = "Static analysis library for Delphi / C++Builder / Free Pascal compiled binaries - identification and metadata extraction (RTTI, VMTs, DFM forms, packages)"
description = "Static analysis library for Delphi / C++Builder / Free Pascal compiled binaries and units - identification and metadata extraction (RTTI, VMTs, DFM forms, packages, DCUs, IDR knowledge bases)"
license = "Apache-2.0"
repository = "https://github.com/ATRAPSLLC/undelphi"
readme = "README.md"
Expand Down
90 changes: 88 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ The `decode_type_detail` dispatcher follows a property's or field's
`PPTypeInfo` indirection and returns a `TypeDetail` enum so callers can
match on Kind without pre-dispatching. Delphi and FPC byte values are
both honoured (FPC reorders the `TTypeKind` enum - `tkClass` is `7` in
Delphi, `15` in FPC; both verified against `reference/`).
Delphi, `15` in FPC; both verified against the runtime sources).

### Extended RTTI (Delphi 2010+)

Expand Down Expand Up @@ -158,11 +158,80 @@ published-field table, init-table managed-field offsets, and declared
or padding), and managed-only entries (ref-counted fields the class
keeps private).

### Runtime packages, imports and exports

- **Imports** (`bin.imports()`, delay-loaded included) with each Borland
package symbol demangled: `@$xp$14System@Integer` is the typeinfo of
`System.Integer`, `@Classes@TPersistent@` the VMT of
`Classes.TPersistent`, and Win64's Itanium names (`_ZTVN6System7Classes11TPersistentE`)
read the same way, generic instances included.
- `bin.import_at(va)` names the import behind a `PPTypeInfo`, `vmtParent`
or field-class cell that points out of the module, and
`bin.required_packages()` lists the `.bpl`s an executable needs (its
`PACKAGEINFO` does not).
- **Exports** (`bin.exports()`) of a runtime package, the compiler's own
answer key for every class VMT, typeinfo and routine it defines.

### Startup tables, resource strings, interface methods

- **Unit initialization table** (`bin.unit_init_table()`): every unit's
`initialization` / `finalization` procedure in startup order, Delphi
2010+ class constructor / destructor pairs, and the 2010+ type table that
lists every RTTI type by declaring unit.
- **Resource strings** (`bin.resource_strings()`): the `RT_STRING` table
joined with each `resourcestring`'s `TResStringRec`.
- **Interface method RTTI** (`bin.interface_rtti(..)`): the methods of an
interface compiled with method info (`IInvokable`), with calling
conventions, parameters and result types.

### Structure extents

`bin.structure_extents()` gives the byte range of every structure the
crate reads - VMT headers, virtual-method arrays, method / field /
dynamic / interface / automation tables, interface vtables, RTTI records,
string constants - so a disassembler can keep them out of the code it
decodes.

### Compiled units (`.dcu` / `.dcp`)

The `dcu` module decodes the compiler's own record of a unit: every
declaration of its interface and implementation, the layout of every type
(records and classes with every field and offset, including private ones
and types without RTTI), every routine with its parameters, result and
calling convention, its uses lists and imports, and its code and RTTI with
their fixups. The format follows Hmelnov's DCU32INT for Delphi 2 - 10.3,
extended with the records Delphi 10.4 - 13 add; every unit of the Delphi 6,
10 Seattle and 13 libraries decodes.

- `dcu::Unit::parse` reads a `.dcu`, `dcu::package::Package::parse` the
units inside a `.dcp`, and `dcu::library::Library` opens a compiler's
library directory and resolves types across units.
- `dcu::mangle::Mangler` generates a routine's export name (Borland on
Win32, Itanium on Win64), matching 98 - 99 % of a runtime package's
exports.
- `bin.locate(&library)` places a program's DCU routines, typed constants
and variables in the linked binary and names the slots of its unit
initialization table.

### IDR knowledge bases

The `kb` module reads and writes the knowledge bases of IDR (the
Interactive Delphi Reconstructor), which describe the units of a Delphi
release's library: constants, types with their fields, properties and
methods, variables, resource strings, and every routine with its prototype
and its code, relocated bytes masked, so it can be recognised in a binary
(`Dump::matches`). `KnowledgeBase::parse` reads a file without copying and
looks records up by name or unit; `Contents::to_bytes` writes one, and
every published file (Delphi 2 - XE6 Win32, XE2 - XE6 Win64) reads and
writes back unchanged. `kb::build` makes a knowledge base from a release's
DCUs the way IDR's own were made: built from the Delphi 6 library, its
records match kb6's wherever the input units are the same.

## Supported targets

| Toolchain | Versions | Containers |
| --- | --- | --- |
| Embarcadero / Borland Delphi | Delphi 2 → Delphi 12 Athens (`dcc` v9 → v36) | PE32 / PE32+, Mach-O, ELF |
| Embarcadero / Borland Delphi | Delphi 2 → Delphi 13 Florence (`dcc` v9 → v37) | PE32 / PE32+, Mach-O, ELF |
| Embarcadero C++Builder | parsed identically to Delphi (same VCL/FMX RTTI) | PE32 / PE32+ |
| Free Pascal / Lazarus | FPC 1.0 → FPC 3.2+ (incl. trunk-shaped 3.3) | PE32 / PE32+, Mach-O, ELF |
| Arch | x86, x86_64, ARM, AArch64 | (per the build-string and / or container header) |
Expand All @@ -185,6 +254,23 @@ contemporary laptop.
cargo run --release --example dump -- path/to/binary.exe
```

- [`examples/dcu_dump.rs`](examples/dcu_dump.rs) - a decoded unit's
header, uses, type table and declarations.
- [`examples/dcu_check.rs`](examples/dcu_check.rs) - decodes every `.dcu`
and `.dcp` under a directory and reports the failures.
- [`examples/dcu_facts.rs`](examples/dcu_facts.rs) - a unit's types,
fields and routines in a line format comparable with DCU32INT's.
- [`examples/mangle_check.rs`](examples/mangle_check.rs) - generated
export names checked against a set of runtime packages.
- [`examples/verify_exports.rs`](examples/verify_exports.rs) -
`classes()` and `types()` checked against runtime packages' exports.
- [`examples/kb_dump.rs`](examples/kb_dump.rs) - an IDR knowledge
base's units, or every record of a given name.
- [`examples/kb_build.rs`](examples/kb_build.rs) - an IDR knowledge base
built from a directory of DCUs.
- [`examples/kb_diff.rs`](examples/kb_diff.rs) - two knowledge bases
compared record by record.

## License

Copyright 2026 ATRAPS LLC. Licensed under the
Expand Down
Loading
Loading