Skip to main content

ddc_dec/
cfg.rs

1//! Basic-block splitting and CFG construction for Dalvik code units.
2//!
3//! Mirrors jcdc's `block.rs` but for a register machine: leaders come from
4//! branch targets, post-branch pcs, try boundaries and handler entries.
5//! Payload pseudo-instructions are never leaders (they are data).
6
7use ddc_dex::code::CodeItem;
8use ddc_dex::insn::{Insn, InsnKind, Payload};
9
10#[derive(Debug, Clone)]
11pub struct Block {
12    pub id: usize,
13    /// First code unit (inclusive).
14    pub start: u32,
15    /// End code unit (exclusive).
16    pub end: u32,
17    /// Instruction INDEX range `[lo, hi)` into the code item's linear
18    /// `insns` stream (blocks are contiguous pc ranges of a linear decode —
19    /// the previous per-block `Vec<Insn>` CLONE was a top allocation cost).
20    pub ins_lo: usize,
21    pub ins_hi: usize,
22    /// Successors. Conditional: `[fallthrough, taken]`; switch: default
23    /// first, then key order.
24    pub succ: Vec<usize>,
25    pub pred: Vec<usize>,
26    /// Exception range indices whose handler this block is.
27    pub handlers: Vec<usize>,
28}
29
30#[derive(Debug, Clone)]
31pub struct ExcRange {
32    /// Code-unit range `[start, end)`.
33    pub start: u32,
34    pub end: u32,
35    /// Handler entry code unit.
36    pub handler: u32,
37    /// `None` = catch-all.
38    pub catch_type: Option<std::sync::Arc<str>>,
39}
40
41pub struct DexCfg {
42    pub blocks: Vec<Block>,
43    pub entry: usize,
44    pub exc_ranges: Vec<ExcRange>,
45    /// The method's linear decode stream, moved in from the `CodeItem`.
46    /// Blocks reference it via `ins_lo..ins_hi` index ranges.
47    pub insns: Vec<Insn>,
48    /// Total code units (pc of one-past-the-last instruction).
49    pub code_units: u32,
50    /// Block start pcs sorted for `block_at`.
51    starts: Vec<u32>,
52}
53
54impl DexCfg {
55    /// Build the CFG for one method body. `type_name` resolves type ids for
56    /// catch types. Takes the `CodeItem` by `&mut` and MOVES its `insns`
57    /// stream into the CFG — blocks then index into it instead of each
58    /// owning a deep clone (the old per-block `Vec<Insn>` copies summed to a
59    /// second full decode of every method; on weibo that was tens of
60    /// millions of instruction clones).
61    pub fn build(code: &mut CodeItem, type_name: &dyn Fn(u32) -> std::sync::Arc<str>) -> DexCfg {
62        let insns = std::mem::take(&mut code.insns);
63        let code_units: u32 = insns.last().map(|last| last.pc + last.size).unwrap_or(0);
64
65        // 1. Leaders.
66        let mut leaders: Vec<u32> = vec![0];
67        for ins in insns.iter() {
68            let next = ins.pc + ins.size;
69            if ins.is_branch() && next <= code_units {
70                leaders.push(next);
71            }
72            for t in insn_targets(ins) {
73                leaders.push(t);
74            }
75            // Switch CASE targets are control-flow leaders too. Ordinary
76            // switches get their case heads for free (each case ends in
77            // `goto break`, and a branch's fall-through is a leader) —
78            // but the weixin protobuf `op()` shape ends every case in a
79            // direct `return`, so NOTHING else marks the heads: all case
80            // bodies fused into one block, every switch edge resolved to
81            // that single block, and the structurer emitted empty
82            // `case 1..6: break;` with the bodies linearized after the
83            // switch (returns dropped — semantics change). The payload
84            // LOCATION is data; the offsets INSIDE it are targets.
85            match &ins.kind {
86                InsnKind::PackedSwitch { payload_pc, .. }
87                | InsnKind::SparseSwitch { payload_pc, .. } => {
88                    for t in switch_targets(code.payloads.get(payload_pc), ins.pc) {
89                        leaders.push(t);
90                    }
91                }
92                _ => {}
93            }
94        }
95        // Try boundaries and handler entries.
96        let mut ranges: Vec<ExcRange> = Vec::new();
97        for t in &code.tries {
98            let end = t.start_addr.saturating_add(t.insn_count);
99            if t.start_addr >= end || t.start_addr >= code_units {
100                continue;
101            }
102            leaders.push(t.start_addr);
103            if end < code_units {
104                leaders.push(end);
105            }
106            if let Some(h) = code.handler_of(t) {
107                for (ty, addr) in &h.catches {
108                    if *addr < code_units {
109                        leaders.push(*addr);
110                        ranges.push(ExcRange {
111                            start: t.start_addr,
112                            end,
113                            handler: *addr,
114                            catch_type: Some(type_name(*ty)),
115                        });
116                    }
117                }
118                if let Some(addr) = h.catch_all {
119                    if addr < code_units {
120                        leaders.push(addr);
121                        ranges.push(ExcRange {
122                            start: t.start_addr,
123                            end,
124                            handler: addr,
125                            catch_type: None,
126                        });
127                    }
128                }
129            }
130        }
131        leaders.retain(|&l| l < code_units || (l == 0 && code_units == 0));
132        leaders.sort_unstable();
133        leaders.dedup();
134        if leaders.is_empty() {
135            leaders.push(0);
136        }
137
138        // 2. pc → block id (last leader <= pc).
139        let block_of_pc = |p: u32| -> Option<usize> {
140            let i = leaders.partition_point(|&l| l <= p);
141            if i == 0 {
142                None
143            } else {
144                Some(i - 1)
145            }
146        };
147
148        // 3. Blocks as index ranges over the linear instruction stream.
149        // (pc-sorted insns; a block's instructions are exactly the stream
150        // slice whose pcs fall in [start, end).)
151        let mut blocks: Vec<Block> = Vec::with_capacity(leaders.len());
152        for (id, &start) in leaders.iter().enumerate() {
153            let end = leaders
154                .get(id + 1)
155                .copied()
156                .unwrap_or(code_units)
157                .max(start);
158            let lo = insns.partition_point(|i| i.pc < start);
159            let hi = insns.partition_point(|i| i.pc < end);
160            blocks.push(Block {
161                id,
162                start,
163                end,
164                ins_lo: lo,
165                ins_hi: hi,
166                succ: Vec::new(),
167                pred: Vec::new(),
168                handlers: Vec::new(),
169            });
170        }
171
172        // 4. Successors.
173        for b in blocks.iter_mut() {
174            if b.ins_hi <= b.ins_lo {
175                continue;
176            }
177            let last = &insns[b.ins_hi - 1];
178            match &last.kind {
179                InsnKind::Goto { target } => {
180                    if let Some(tb) = block_of_pc(*target) {
181                        b.succ.push(tb);
182                    }
183                }
184                InsnKind::If { target, .. } => {
185                    let next_pc = last.pc + last.size;
186                    if let Some(f) = block_of_pc(next_pc) {
187                        b.succ.push(f);
188                    }
189                    if let Some(tb) = block_of_pc(*target) {
190                        b.succ.push(tb);
191                    }
192                }
193                InsnKind::PackedSwitch { payload_pc, .. }
194                | InsnKind::SparseSwitch { payload_pc, .. } => {
195                    let next_pc = last.pc + last.size;
196                    if let Some(f) = block_of_pc(next_pc) {
197                        b.succ.push(f); // default arm first
198                    }
199                    for t in switch_targets(code.payloads.get(payload_pc), last.pc) {
200                        if let Some(tb) = block_of_pc(t) {
201                            if !b.succ.contains(&tb) {
202                                b.succ.push(tb);
203                            }
204                        }
205                    }
206                }
207                InsnKind::ReturnVoid | InsnKind::Return { .. } | InsnKind::Throw { .. } => {}
208                _ => {
209                    let next_pc = last.pc + last.size;
210                    if next_pc < code_units {
211                        if let Some(f) = block_of_pc(next_pc) {
212                            b.succ.push(f);
213                        }
214                    }
215                }
216            }
217        }
218        for b in blocks.iter_mut() {
219            let mut seen = Vec::new();
220            b.succ.retain(|s| {
221                if seen.contains(s) {
222                    false
223                } else {
224                    seen.push(*s);
225                    true
226                }
227            });
228        }
229
230        // 5. Predecessors + handler back-references (jcdc semantics: a block
231        // is protected when its FIRST offset lies in [start, end)).
232        for b in 0..blocks.len() {
233            let succ = blocks[b].succ.clone();
234            for s in succ {
235                if s < blocks.len() {
236                    blocks[s].pred.push(b);
237                }
238            }
239        }
240        for (ri, r) in ranges.iter().enumerate() {
241            if let Some(h) = blocks.iter().position(|b| b.start == r.handler) {
242                blocks[h].handlers.push(ri);
243            }
244        }
245        for b in blocks.iter_mut() {
246            b.pred.sort_unstable();
247            b.pred.dedup();
248            b.handlers.sort_unstable();
249            b.handlers.dedup();
250        }
251
252        let mut starts = leaders;
253        starts.sort_unstable();
254        DexCfg {
255            blocks,
256            entry: 0,
257            exc_ranges: ranges,
258            insns,
259            code_units,
260            starts,
261        }
262    }
263
264    /// The block's instructions (slice of the linear decode stream).
265    /// Blocks in reverse postorder over NORMAL (successor) edges;
266    /// blocks unreachable that way — exception handlers, dead code —
267    /// appended afterwards, ordered by start pc. A deterministic
268    /// execution-order approximation: strictly better than pc order,
269    /// which interleaves handler bodies into the flow they interrupt
270    /// (compute_final_reads consumed allocation views at reads whose
271    /// real successors still read the register).
272    pub fn rpo_order(&self) -> Vec<usize> {
273        let n = self.blocks.len();
274        // Iterative postorder DFS over succ (entry first).
275        let mut seen = vec![false; n];
276        let mut post: Vec<usize> = Vec::new();
277        if n > 0 {
278            let mut stack: Vec<(usize, usize)> = vec![(0, 0)];
279            seen[0] = true;
280            while let Some(top) = stack.last_mut() {
281                let (b, i) = *top;
282                if i < self.blocks[b].succ.len() {
283                    top.1 += 1;
284                    let s = self.blocks[b].succ[i];
285                    if s < n && !seen[s] {
286                        seen[s] = true;
287                        stack.push((s, 0));
288                    }
289                } else {
290                    post.push(b);
291                    stack.pop();
292                }
293            }
294        }
295        let mut order: Vec<usize> = post.into_iter().rev().collect();
296        // Unreached blocks (exception-handler entries, dead code) after.
297        let mut rest: Vec<usize> = (0..n).filter(|&b| !seen[b]).collect();
298        rest.sort_unstable_by_key(|&b| self.blocks[b].start);
299        order.extend(rest);
300        order
301    }
302    pub fn block_ins(&self, b: &Block) -> &[Insn] {
303        &self.insns[b.ins_lo..b.ins_hi]
304    }
305
306    /// The block's last instruction.
307    pub fn block_last(&self, b: &Block) -> Option<&Insn> {
308        if b.ins_hi > b.ins_lo {
309            self.insns.get(b.ins_hi - 1)
310        } else {
311            None
312        }
313    }
314
315    pub fn block_at(&self, p: u32) -> Option<usize> {
316        let i = self.starts.partition_point(|&l| l <= p);
317        if i == 0 {
318            None
319        } else {
320            Some(i - 1)
321        }
322    }
323
324    /// Exception edges `(range idx, from block, to block)` derived from the
325    /// protected-region rule (first offset inside `[start, end)`, has code).
326    pub fn exc_edges(&self) -> Vec<(usize, usize, usize)> {
327        let mut out = Vec::new();
328        for (ri, r) in self.exc_ranges.iter().enumerate() {
329            if let Some(h) = self.blocks.iter().position(|b| b.start == r.handler) {
330                for b in &self.blocks {
331                    if b.start >= r.start && b.start < r.end && b.ins_hi > b.ins_lo {
332                        out.push((ri, b.id, h));
333                    }
334                }
335            }
336        }
337        out
338    }
339
340    /// Machine-neutral view for the shared structurer (ids preserved).
341    pub fn to_core(&self) -> jdc_core::cfg::Cfg {
342        let blocks = self
343            .blocks
344            .iter()
345            .map(|b| jdc_core::cfg::Block {
346                id: b.id,
347                start: b.start,
348                end: b.end,
349                ins_len: (b.ins_hi - b.ins_lo) as u32,
350                succ: b.succ.clone(),
351                pred: b.pred.clone(),
352                handlers: b.handlers.iter().map(|&h| h as u32).collect(),
353            })
354            .collect();
355        let ranges = self
356            .exc_ranges
357            .iter()
358            .map(|r| jdc_core::cfg::ExcRange {
359                start: r.start,
360                end: r.end,
361                handler: r.handler,
362                catch_type: r.catch_type.clone(),
363            })
364            .collect();
365        let edges = self
366            .exc_edges()
367            .into_iter()
368            .map(|(range, from, to)| jdc_core::cfg::ExcEdge { range, from, to })
369            .collect();
370        jdc_core::cfg::Cfg::from_parts(blocks, self.entry, ranges, edges)
371    }
372}
373
374/// Absolute branch targets of one instruction.
375fn insn_targets(ins: &Insn) -> Vec<u32> {
376    match &ins.kind {
377        InsnKind::Goto { target } => vec![*target],
378        InsnKind::If { target, .. } => vec![*target],
379        // Payload locations are data, not control targets.
380        _ => Vec::new(),
381    }
382}
383
384/// Absolute case targets of a switch from its payload.
385pub fn switch_targets(payload: Option<&Payload>, switch_pc: u32) -> Vec<u32> {
386    match payload {
387        Some(Payload::Packed { targets, .. }) => targets
388            .iter()
389            .map(|t| (switch_pc as i64 + *t as i64) as u32)
390            .collect(),
391        Some(Payload::Sparse { pairs }) => pairs
392            .iter()
393            .map(|(_, t)| (switch_pc as i64 + *t as i64) as u32)
394            .collect(),
395        _ => Vec::new(),
396    }
397}
398
399/// Switch keys for `Term::Switch` construction.
400pub fn switch_keys(payload: Option<&Payload>) -> SwitchKeys {
401    match payload {
402        Some(Payload::Packed { first_key, targets }) => SwitchKeys::Table {
403            low: *first_key,
404            targets: targets.clone(),
405        },
406        Some(Payload::Sparse { pairs }) => SwitchKeys::Lookup {
407            pairs: pairs.clone(),
408        },
409        _ => SwitchKeys::None,
410    }
411}
412
413pub enum SwitchKeys {
414    Table { low: i32, targets: Vec<i32> },
415    Lookup { pairs: Vec<(i32, i32)> },
416    None,
417}