hcb2lua_decompiler/
cfg.rs1use std::collections::{BTreeMap, BTreeSet, VecDeque};
2
3use crate::decode::{Instruction, Function, Op};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum BlockTerm {
7 Fallthrough,
8 Jmp,
9 Jz,
10 Ret,
11 RetV,
12}
13
14#[derive(Debug, Clone)]
15pub struct BasicBlock {
16 pub id: usize,
17 pub start: u32,
18 pub end: u32, pub inst_indices: std::ops::Range<usize>,
20 pub preds: Vec<usize>,
21 pub succs: Vec<usize>,
22 pub term: BlockTerm,
23 pub in_depth: usize,
24 pub out_depth: usize,
25 pub is_loop_header: bool,
26}
27
28#[derive(Debug, Clone)]
29pub struct FunctionCfg {
30 pub blocks: Vec<BasicBlock>,
31 pub addr_to_block: BTreeMap<u32, usize>,
32 pub max_depth: usize,
33 pub stack_consistent: bool,
34}
35
36fn inst_stack_delta(inst: &Instruction, callee_args: &BTreeMap<u32, u8>) -> i32 {
37 match &inst.op {
38 Op::Nop | Op::InitStack { .. } | Op::Jmp { .. } | Op::Ret => 0,
39 Op::Jz { .. } => -1,
40 Op::RetV => -1,
41 Op::PushNil
42 | Op::PushTrue
43 | Op::PushI8(_)
44 | Op::PushI16(_)
45 | Op::PushI32(_)
46 | Op::PushF32(_)
47 | Op::PushString(_)
48 | Op::PushGlobal(_)
49 | Op::PushStack(_) => 1,
50 | Op::PushGlobalTable(_)
51 | Op::PushLocalTable(_) => 0,
52 | Op::PushTop
53 | Op::PushReturn => 1,
54 Op::PopGlobal(_) | Op::PopStack(_) => -1,
55 | Op::PopGlobalTable(_) | Op::PopLocalTable(_) => -2,
56 Op::Neg => 0,
57 Op::Add
58 | Op::Sub
59 | Op::Mul
60 | Op::Div
61 | Op::Mod
62 | Op::BitTest
63 | Op::And
64 | Op::Or
65 | Op::SetE
66 | Op::SetNE
67 | Op::SetG
68 | Op::SetLE
69 | Op::SetL
70 | Op::SetGE => -1,
71 Op::Call { target } => -(callee_args.get(target).copied().unwrap_or(0) as i32),
72 Op::Syscall { args, .. } => -(*args as i32),
73 Op::Unknown(_) => 0,
74 }
75}
76
77fn block_term(last: Option<&Instruction>) -> BlockTerm {
78 match last.map(|i| &i.op) {
79 Some(Op::Jmp { .. }) => BlockTerm::Jmp,
80 Some(Op::Jz { .. }) => BlockTerm::Jz,
81 Some(Op::Ret) => BlockTerm::Ret,
82 Some(Op::RetV) => BlockTerm::RetV,
83 _ => BlockTerm::Fallthrough,
84 }
85}
86
87pub fn build_cfg(func: &Function, callee_args: &BTreeMap<u32, u8>) -> FunctionCfg {
88 let mut leaders: BTreeSet<u32> = BTreeSet::new();
90 if let Some(first) = func.insts.first() {
91 leaders.insert(first.addr);
92 }
93
94 let mut addr_to_idx: BTreeMap<u32, usize> = BTreeMap::new();
96 for (i, inst) in func.insts.iter().enumerate() {
97 addr_to_idx.insert(inst.addr, i);
98 }
99
100 for (i, inst) in func.insts.iter().enumerate() {
101 match &inst.op {
102 Op::Jmp { target } | Op::Jz { target } => {
103 leaders.insert(*target);
104 if let Some(next) = func.insts.get(i + 1) {
105 leaders.insert(next.addr);
106 }
107 }
108 Op::Ret | Op::RetV => {
109 if let Some(next) = func.insts.get(i + 1) {
110 leaders.insert(next.addr);
111 }
112 }
113 _ => {}
114 }
115 }
116
117 let func_end = func
118 .insts
119 .last()
120 .map(|i| i.addr + 1)
121 .unwrap_or(func.start_addr);
122
123 let mut leader_vec: Vec<u32> = leaders.into_iter().collect();
125 leader_vec.sort();
126
127 let mut blocks: Vec<BasicBlock> = Vec::new();
128 let mut addr_to_block: BTreeMap<u32, usize> = BTreeMap::new();
129
130 for (bid, &start) in leader_vec.iter().enumerate() {
131 let end = leader_vec.get(bid + 1).copied().unwrap_or(func_end);
132 let si = addr_to_idx.get(&start).copied().unwrap_or(0);
133 let ei = addr_to_idx
134 .range(end..)
135 .next()
136 .map(|(_, &idx)| idx)
137 .unwrap_or(func.insts.len());
138
139 for (&a, _) in addr_to_idx.range(start..end) {
140 addr_to_block.insert(a, bid);
141 }
142
143 let last = if ei > 0 { func.insts.get(ei - 1) } else { None };
144 blocks.push(BasicBlock {
145 id: bid,
146 start,
147 end,
148 inst_indices: si..ei,
149 preds: Vec::new(),
150 succs: Vec::new(),
151 term: block_term(last),
152 in_depth: 0,
153 out_depth: 0,
154 is_loop_header: false,
155 });
156 }
157
158 for b in &mut blocks {
160 let last_idx = b.inst_indices.clone().last();
161 let last = last_idx.and_then(|i| func.insts.get(i));
162 match last.map(|i| &i.op) {
163 Some(Op::Jmp { target }) => {
164 if let Some(&tid) = addr_to_block.get(target) {
165 b.succs.push(tid);
166 }
167 }
168 Some(Op::Jz { target }) => {
169 if let Some(&tid) = addr_to_block.get(target) {
170 b.succs.push(tid);
171 }
172 let ft = func
174 .insts
175 .get(b.inst_indices.end)
176 .map(|i| i.addr);
177 if let Some(ft) = ft {
178 if let Some(&fid) = addr_to_block.get(&ft) {
179 b.succs.push(fid);
180 }
181 }
182 }
183 Some(Op::Ret) | Some(Op::RetV) => {}
184 _ => {
185 let ft = func
187 .insts
188 .get(b.inst_indices.end)
189 .map(|i| i.addr);
190 if let Some(ft) = ft {
191 if let Some(&fid) = addr_to_block.get(&ft) {
192 b.succs.push(fid);
193 }
194 }
195 }
196 }
197 }
198
199 for b in 0..blocks.len() {
201 let succs = blocks[b].succs.clone();
202 for s in succs {
203 if let Some(sb) = blocks.get_mut(s) {
204 sb.preds.push(b);
205 }
206 }
207 }
208
209 let mut in_depth: Vec<Option<usize>> = vec![None; blocks.len()];
211 if !blocks.is_empty() {
212 in_depth[0] = Some(0);
213 }
214 let mut q: VecDeque<usize> = VecDeque::new();
215 if !blocks.is_empty() {
216 q.push_back(0);
217 }
218 let mut max_depth = 0usize;
219 let mut stack_consistent = true;
220
221 while let Some(bid) = q.pop_front() {
222 let mut d = in_depth[bid].unwrap_or(0) as i32;
223 let b = &blocks[bid];
224
225 for i in b.inst_indices.clone() {
226 let inst = &func.insts[i];
227 d += inst_stack_delta(inst, callee_args);
228 if d < 0 {
229 d = 0;
230 }
231 max_depth = max_depth.max(d as usize);
232 }
233
234 let out = d as usize;
235 for &sid in &blocks[bid].succs {
236 match in_depth[sid] {
237 None => {
238 in_depth[sid] = Some(out);
239 q.push_back(sid);
240 }
241 Some(existing) => {
242 if existing != out {
245 stack_consistent = false;
246 let newd = existing.max(out);
247 if newd != existing {
248 in_depth[sid] = Some(newd);
249 q.push_back(sid);
250 }
251 }
252 }
253 }
254 }
255 }
256
257 for b in &mut blocks {
258 b.in_depth = in_depth[b.id].unwrap_or(0);
259 let mut d = b.in_depth as i32;
261 for i in b.inst_indices.clone() {
262 let inst = &func.insts[i];
263 d += inst_stack_delta(inst, callee_args);
264 if d < 0 {
265 d = 0;
266 }
267 }
268 b.out_depth = d as usize;
269 }
270
271 let mut loop_headers: BTreeSet<usize> = BTreeSet::new();
273 for b in &blocks {
274 for &s in &b.succs {
275 if let (Some(src), Some(dst)) = (blocks.get(b.id), blocks.get(s)) {
276 if dst.start < src.start {
277 loop_headers.insert(s);
278 }
279 }
280 }
281 }
282 for b in &mut blocks {
283 b.is_loop_header = loop_headers.contains(&b.id);
284 }
285
286 FunctionCfg {
287 blocks,
288 addr_to_block,
289 max_depth,
290 stack_consistent,
291 }
292}