Skip to main content

types/
types.rs

1//! Structured type introspection over prototypes, named types, and a stack frame in one interned table.
2//!
3//! Every function prototype, named type, and stack frame walks out of the kernel as an owned
4//! snapshot whose every parameter, member, and slot is a real `TypeId` you resolve by handle,
5//! the idiomatic counterpart to a rendered declaration string. A prototype names a class only
6//! opaquely (`fb::JsObject *`), and resolving that name tells you whether the database carries
7//! its field layout or merely a forward declaration.
8//!
9//! Run: `cargo run -p idakit --example types -- path/to/database.i64 [TypeName]`
10
11use std::collections::HashSet;
12
13use idakit::prelude::*;
14
15const SHOW_PROTOS: usize = 12;
16const FRAME_BUDGET: usize = 3000;
17const MAX_NAMES: usize = 64;
18
19/// A one-line rendering of the type at `id`.
20///
21/// Aggregates render as their tag only (so a recursive type stays finite). `Ptr`/`Array`/
22/// `Function` recurse into element types, bottoming out at a named tag or scalar.
23fn one_line(table: &TypeTable, id: TypeId) -> String {
24    match &table.get(id).shape {
25        TypeShape::Void => "void".to_owned(),
26        TypeShape::Bool => "bool".to_owned(),
27        TypeShape::Int { bytes, signed } => {
28            format!(
29                "{}int{}",
30                if *signed { "" } else { "u" },
31                u32::from(*bytes) * 8
32            )
33        }
34        TypeShape::Float { bytes } => format!("float{}", u32::from(*bytes) * 8),
35        TypeShape::Ptr(inner) => format!("{} *", one_line(table, *inner)),
36        TypeShape::Array { elem, len } => format!("{}[{len}]", one_line(table, *elem)),
37        TypeShape::Struct { name, .. } => {
38            format!("struct {}", name.as_deref().unwrap_or("<anon>"))
39        }
40        TypeShape::Union { name, .. } => format!("union {}", name.as_deref().unwrap_or("<anon>")),
41        TypeShape::Enum { name, .. } => format!("enum {}", name.as_deref().unwrap_or("<anon>")),
42        TypeShape::Function {
43            ret,
44            params,
45            varargs,
46        } => {
47            let ps: Vec<String> = params.iter().map(|p| one_line(table, *p)).collect();
48            let tail = if *varargs { ", ..." } else { "" };
49            format!("{} ({}{})", one_line(table, *ret), ps.join(", "), tail)
50        }
51        TypeShape::Typedef { name, .. } | TypeShape::Opaque(name) => name.clone(),
52        TypeShape::Unknown => "<unknown>".to_owned(),
53    }
54}
55
56/// A type name worth resolving via [`Database::type_named`]: a definition's tag
57/// ([`TypeShape::tag_name`]) or an [`Opaque`](TypeShape::Opaque) reference.
58///
59/// The latter is a name a prototype mentions without carrying a body here, which resolving
60/// may or may not expand.
61fn referenced_name(shape: &TypeShape) -> Option<&str> {
62    match shape {
63        TypeShape::Opaque(name) => Some(name),
64        _ => shape.tag_name(),
65    }
66}
67
68/// Parameter count of a prototype image, or 0 for a non-function root.
69fn param_count(image: &Type) -> usize {
70    match image.shape() {
71        TypeShape::Function { params, .. } => params.len(),
72        _ => 0,
73    }
74}
75
76/// Prints an aggregate's fields, one per line, recursing into embedded (non-pointer)
77/// aggregates so nesting is visible.
78///
79/// `seen` breaks cycles; `indent` bounds depth.
80fn layout(table: &TypeTable, id: TypeId, indent: usize, seen: &mut HashSet<TypeId>) {
81    const MAX_DEPTH: usize = 3;
82    let (TypeShape::Struct { members, .. } | TypeShape::Union { members, .. }) =
83        &table.get(id).shape
84    else {
85        return;
86    };
87    let pad = "  ".repeat(indent);
88    for m in members {
89        println!(
90            "{pad}+{:#06x}  {:<28} {}",
91            m.bit_offset / 8,
92            m.name,
93            one_line(table, m.ty)
94        );
95        if indent < MAX_DEPTH && seen.insert(m.ty) {
96            layout(table, m.ty, indent + 1, seen);
97        }
98    }
99}
100
101/// Prints a resolved named type: its root shape, size, and (if the database carries a body)
102/// its full field layout.
103///
104/// A forward-declared name resolves but has no layout to show, so say so.
105fn print_layout(image: &Type, name: &str) {
106    let (table, root) = (image.types(), image.root());
107    println!("\n-- layout: {name} --");
108    match image.size() {
109        Some(s) => println!("  {}  ({s:#x} bytes)", one_line(table, root)),
110        None => println!("  {}  (no stored size)", one_line(table, root)),
111    }
112    if image.members().is_none_or(<[TypeMember]>::is_empty) {
113        println!("  forward-declared -- the database stores the name but no field layout");
114        return;
115    }
116    let mut seen = HashSet::from([root]);
117    layout(table, root, 1, &mut seen);
118}
119
120/// Signed fp-relative offset the way IDA shows it: `-0x18`, `0x8`.
121fn soff(offset: i64) -> String {
122    if offset < 0 {
123        format!("-{:#x}", offset.unsigned_abs())
124    } else {
125        format!("{offset:#x}")
126    }
127}
128
129/// Prints a function's stack frame: total size and every slot's offset, label, and resolved type.
130fn print_frame(frame: &StackFrame, ea: Address) {
131    println!(
132        "\n== stack frame: {ea:#x}  ({} bytes, {} slots) ==",
133        frame.size(),
134        frame.len()
135    );
136    for v in frame.slots() {
137        let ty = v
138            .ty()
139            .map_or_else(|| "-".to_owned(), |id| one_line(frame.types(), id));
140        let label = match v.kind() {
141            StackSlotKind::Variable { name, .. } if !name.is_empty() => name.clone(),
142            StackSlotKind::Variable { .. } => "<unnamed>".to_owned(),
143            StackSlotKind::ReturnAddress => "<return address>".to_owned(),
144            StackSlotKind::SavedRegisters => "<saved registers>".to_owned(),
145        };
146        println!("  {:>8}  {label}", soff(v.offset()));
147        if ty != "-" {
148            println!("            {ty}");
149        }
150    }
151}
152
153fn main() -> Result<(), Box<dyn std::error::Error>> {
154    let mut argv = std::env::args().skip(1);
155    let db = argv.next().expect("usage: types <db.i64> [TypeName]");
156    let arg_type = argv.next();
157
158    Ida::run(move |ida| -> Result<(), Error> {
159        ida.call(move |idb| -> Result<(), Error> {
160            idb.open(&db).call()?;
161
162            // Prototypes are sparse in a stripped release binary, so scan every function, not a
163            // prefix, so the reported ratio is honest and the sample isn't just entry-point stubs.
164            let mut total = 0usize;
165            let mut typed = 0usize;
166            let mut shown: Vec<(Address, String, Type)> = Vec::new();
167            let mut names: Vec<String> = Vec::new();
168            let mut best_frame: Option<(Address, StackFrame)> = None;
169            let mut best_vars = 0usize;
170            let mut frames_tried = 0usize;
171
172            for f in idb.functions() {
173                total += 1;
174
175                if let Some(image) = f.prototype_type()? {
176                    typed += 1;
177                    for (_, t) in image.types().iter() {
178                        if let Some(n) = referenced_name(&t.shape)
179                            && names.len() < MAX_NAMES
180                            && !names.iter().any(|x| x == n)
181                        {
182                            names.push(n.to_owned());
183                        }
184                    }
185                    if shown.len() < SHOW_PROTOS {
186                        shown.push((f.address(), f.name().as_str().to_owned(), image));
187                    }
188                }
189
190                if frames_tried < FRAME_BUDGET
191                    && let Some(frame) = idb.frame(f.address())?
192                {
193                    frames_tried += 1;
194                    // A local lives within its frame; an offset in the millions is IDA's own
195                    // misanalysis of a garbage function. Skip such frames so we showcase a real one.
196                    let locals = frame.slots().iter().filter(|v| !v.is_special());
197                    let sane = locals.clone().all(|v| v.offset().unsigned_abs() < 0x1_0000);
198                    let n = locals.count();
199                    if sane && (best_frame.is_none() || n > best_vars) {
200                        best_vars = n;
201                        best_frame = Some((f.address(), frame));
202                    }
203                }
204            }
205
206            println!("== function prototypes ==");
207            println!("{typed} of {total} functions carry a stored prototype.\n");
208            for (ea, sym, image) in &shown {
209                println!("  {ea:#x}  {}", one_line(image.types(), image.root()));
210                let short: String = sym.chars().take(64).collect();
211                if !short.is_empty() {
212                    println!("               {short}");
213                }
214            }
215            if let Some((ea, _, image)) = shown.iter().max_by_key(|(_, _, im)| param_count(im))
216                && let TypeShape::Function {
217                    ret,
218                    params,
219                    varargs,
220                } = image.shape()
221                && !params.is_empty()
222            {
223                println!("\n  every parameter is a resolved TypeId -- {ea:#x}:");
224                println!("    ret    {}", one_line(image.types(), *ret));
225                for (i, p) in params.iter().enumerate() {
226                    println!("    arg{i}   {}", one_line(image.types(), *p));
227                }
228                if *varargs {
229                    println!("    ...");
230                }
231            }
232
233            // The named-type pass: resolve every name the prototypes reference and classify what
234            // the database actually holds, a full body to expand, or just a forward declaration.
235            println!("\n== referenced named types ==");
236            if let Some(name) = &arg_type {
237                match idb.type_named(name) {
238                    Ok(image) => print_layout(&image, name),
239                    Err(e) => println!("  type_named({name:?}): {e}"),
240                }
241            } else {
242                let mut bodies: Vec<(String, Type)> = Vec::new();
243                let mut forward: Vec<String> = Vec::new();
244                let mut not_local = 0usize;
245                for name in &names {
246                    match idb.type_named(name) {
247                        Ok(image) if image.members().is_some_and(|m| !m.is_empty()) => {
248                            bodies.push((name.clone(), image));
249                        }
250                        Ok(_) => forward.push(name.clone()),
251                        Err(Error::TypeNotFound { .. }) => not_local += 1,
252                        Err(e) => println!("  type_named({name:?}): {e}"),
253                    }
254                }
255                println!(
256                    "{} referenced: {} with a full body, {} forward-declared, {} not a local type.",
257                    names.len(),
258                    bodies.len(),
259                    forward.len(),
260                    not_local
261                );
262                if let Some((name, image)) = bodies
263                    .iter()
264                    .max_by_key(|(_, im)| im.members().map_or(0, <[_]>::len))
265                {
266                    print_layout(image, name);
267                } else {
268                    for n in forward.iter().take(6) {
269                        println!("  forward-decl: {n}");
270                    }
271                }
272            }
273
274            match &best_frame {
275                Some((ea, frame)) => print_frame(frame, *ea),
276                None => println!("\n(no function has a stack frame)"),
277            }
278
279            idb.close(false);
280            println!("\nTYPES OK");
281            Ok(())
282        })?
283    })??;
284
285    Ok(())
286}