Skip to main content

neo_devpack_solidity/cli/bytecode/bytecode_disasm/
disassemble.rs

1/// Disassemble NeoVM bytecode into a human-readable textual listing.
2///
3/// This function is total: any byte sequence (including malformed or
4/// truncated bytecode) produces some output without panicking. That
5/// invariant is fuzzed by `fuzz/fuzz_targets/fuzz_target_disasm.rs`.
6pub fn disassemble_neovm_bytecode(bytecode: &[u8]) -> String {
7    let width = if bytecode.len() <= 0xFFFF { 4 } else { 6 };
8    let mut out = String::new();
9    let mut pc: usize = 0;
10
11    while pc < bytecode.len() {
12        let offset = pc;
13        let opcode = bytecode[pc];
14        pc += 1;
15
16        let opname = neo_devpack_solidity::runtime::spec::opcode_name(opcode)
17            .map(str::to_string)
18            .unwrap_or_else(|| format!("OP_{opcode:02X}"));
19
20        out.push_str(&format!("{offset:0width$X}: {opname}"));
21
22        match opcode {
23            // Push immediates
24            0x00 => {
25                // PUSHINT8
26                if let Some(v) = take_i8(bytecode, &mut pc) {
27                    out.push_str(&format!(" {v}"));
28                } else {
29                    out.push_str(" <unexpected EOF>");
30                    break;
31                }
32            }
33            0x01 => {
34                // PUSHINT16
35                if let Some(v) = take_i16(bytecode, &mut pc) {
36                    out.push_str(&format!(" {v}"));
37                } else {
38                    out.push_str(" <unexpected EOF>");
39                    break;
40                }
41            }
42            0x02 => {
43                // PUSHINT32
44                if let Some(v) = take_i32(bytecode, &mut pc) {
45                    out.push_str(&format!(" {v}"));
46                } else {
47                    out.push_str(" <unexpected EOF>");
48                    break;
49                }
50            }
51            0x03 => {
52                // PUSHINT64
53                if let Some(v) = take_i64(bytecode, &mut pc) {
54                    out.push_str(&format!(" {v}"));
55                } else {
56                    out.push_str(" <unexpected EOF>");
57                    break;
58                }
59            }
60            0x04 => {
61                // PUSHINT128 (raw LE bytes)
62                if let Some(bytes) = take(bytecode, &mut pc, 16) {
63                    out.push_str(&format!(" 0x{}", hex::encode(bytes)));
64                } else {
65                    out.push_str(" <unexpected EOF>");
66                    break;
67                }
68            }
69            0x05 => {
70                // PUSHINT256 (raw LE bytes)
71                if let Some(bytes) = take(bytecode, &mut pc, 32) {
72                    out.push_str(&format!(" 0x{}", hex::encode(bytes)));
73                } else {
74                    out.push_str(" <unexpected EOF>");
75                    break;
76                }
77            }
78            0x0A => {
79                // PUSHA — signed offset relative to this opcode (Pointer target).
80                if let Some(raw) = take_u32(bytecode, &mut pc) {
81                    out.push_str(&format!(" {}", raw as i32));
82                } else {
83                    out.push_str(" <unexpected EOF>");
84                    break;
85                }
86            }
87            0x0C => {
88                // PUSHDATA1
89                let Some(len) = take_u8(bytecode, &mut pc) else {
90                    out.push_str(" <unexpected EOF>");
91                    break;
92                };
93                let Some(data) = take(bytecode, &mut pc, len as usize) else {
94                    out.push_str(" <unexpected EOF>");
95                    break;
96                };
97                out.push_str(&format!(" len={len} 0x{}", hex::encode(data)));
98            }
99            0x0D => {
100                // PUSHDATA2
101                let Some(len) = take_u16(bytecode, &mut pc) else {
102                    out.push_str(" <unexpected EOF>");
103                    break;
104                };
105                let Some(data) = take(bytecode, &mut pc, len as usize) else {
106                    out.push_str(" <unexpected EOF>");
107                    break;
108                };
109                out.push_str(&format!(" len={len} 0x{}", hex::encode(data)));
110            }
111            0x0E => {
112                // PUSHDATA4
113                let Some(len) = take_u32(bytecode, &mut pc) else {
114                    out.push_str(" <unexpected EOF>");
115                    break;
116                };
117                let Some(data) = take(bytecode, &mut pc, len as usize) else {
118                    out.push_str(" <unexpected EOF>");
119                    break;
120                };
121                out.push_str(&format!(" len={len} 0x{}", hex::encode(data)));
122            }
123
124            // Flow control (wide forms used by compiler)
125            0x23 // JMP_L
126            | 0x25 // JMPIF_L
127            | 0x27 // JMPIFNOT_L
128            | 0x29 // JMPEQ_L
129            | 0x2B // JMPNE_L
130            | 0x2D // JMPGT_L
131            | 0x2F // JMPGE_L
132            | 0x31 // JMPLT_L
133            | 0x33 // JMPLE_L
134            | 0x35 // CALL_L
135            | 0x3E // ENDTRY_L
136            => {
137                let Some(rel) = take_i32(bytecode, &mut pc) else {
138                    out.push_str(" <unexpected EOF>");
139                    break;
140                };
141                out.push_str(&format!(" -> {}", fmt_target(bytecode.len(), offset, rel)));
142            }
143            0x3C => {
144                // TRY_L CatchOffset(int) FinallyOffset(int)
145                let Some(catch_rel) = take_i32(bytecode, &mut pc) else {
146                    out.push_str(" <unexpected EOF>");
147                    break;
148                };
149                let Some(finally_rel) = take_i32(bytecode, &mut pc) else {
150                    out.push_str(" <unexpected EOF>");
151                    break;
152                };
153                if catch_rel != 0 {
154                    out.push_str(&format!(
155                        " catch={}",
156                        fmt_target(bytecode.len(), offset, catch_rel)
157                    ));
158                }
159                if finally_rel != 0 {
160                    out.push_str(&format!(
161                        " finally={}",
162                        fmt_target(bytecode.len(), offset, finally_rel)
163                    ));
164                }
165            }
166            0x3B => {
167                // TRY CatchOffset(sbyte) FinallyOffset(sbyte)
168                let Some(catch_rel) = take_i8(bytecode, &mut pc) else {
169                    out.push_str(" <unexpected EOF>");
170                    break;
171                };
172                let Some(finally_rel) = take_i8(bytecode, &mut pc) else {
173                    out.push_str(" <unexpected EOF>");
174                    break;
175                };
176                if catch_rel != 0 {
177                    out.push_str(&format!(
178                        " catch={}",
179                        fmt_target(bytecode.len(), offset, catch_rel as i32)
180                    ));
181                }
182                if finally_rel != 0 {
183                    out.push_str(&format!(
184                        " finally={}",
185                        fmt_target(bytecode.len(), offset, finally_rel as i32)
186                    ));
187                }
188            }
189            0x22 // JMP
190            | 0x24 // JMPIF
191            | 0x26 // JMPIFNOT
192            | 0x28 // JMPEQ
193            | 0x2A // JMPNE
194            | 0x2C // JMPGT
195            | 0x2E // JMPGE
196            | 0x30 // JMPLT
197            | 0x32 // JMPLE
198            | 0x34 // CALL
199            | 0x3D // ENDTRY
200            => {
201                let Some(rel) = take_i8(bytecode, &mut pc) else {
202                    out.push_str(" <unexpected EOF>");
203                    break;
204                };
205                out.push_str(&format!(
206                    " -> {}",
207                    fmt_target(bytecode.len(), offset, rel as i32)
208                ));
209            }
210
211            // Slots / indices
212            0x56 => {
213                // INITSSLOT static_slots
214                let Some(count) = take_u8(bytecode, &mut pc) else {
215                    out.push_str(" <unexpected EOF>");
216                    break;
217                };
218                out.push_str(&format!(" {count}"));
219            }
220            0x57 => {
221                // INITSLOT locals,args
222                let Some(locals) = take_u8(bytecode, &mut pc) else {
223                    out.push_str(" <unexpected EOF>");
224                    break;
225                };
226                let Some(args) = take_u8(bytecode, &mut pc) else {
227                    out.push_str(" <unexpected EOF>");
228                    break;
229                };
230                out.push_str(&format!(" locals={locals} args={args}"));
231            }
232            0x5F | 0x67 | 0x6F | 0x77 | 0x7F | 0x87 => {
233                // LDSFLD/STSFLD/LDLOC/STLOC/LDARG/STARG (generic index form)
234                let Some(index) = take_u8(bytecode, &mut pc) else {
235                    out.push_str(" <unexpected EOF>");
236                    break;
237                };
238                out.push_str(&format!(" {index}"));
239            }
240
241            // Syscall
242            0x41 => {
243                let Some(id_bytes) = take(bytecode, &mut pc, 4) else {
244                    out.push_str(" <unexpected EOF>");
245                    break;
246                };
247                let id = [id_bytes[0], id_bytes[1], id_bytes[2], id_bytes[3]];
248                if let Some(name) = neo_devpack_solidity::runtime::spec::syscall_name(&id) {
249                    out.push_str(&format!(" {name}"));
250                } else {
251                    out.push_str(&format!(" 0x{}", hex::encode(id)));
252                }
253            }
254
255            // CALLT — calls a method token by 2-byte (u16 LE) token index. The
256            // operand MUST be consumed or every following instruction mis-aligns.
257            0x37 => {
258                let Some(idx_bytes) = take(bytecode, &mut pc, 2) else {
259                    out.push_str(" <unexpected EOF>");
260                    break;
261                };
262                let idx = u16::from_le_bytes([idx_bytes[0], idx_bytes[1]]);
263                out.push_str(&format!(" token={idx}"));
264            }
265
266            0xDB | 0xD9 => {
267                // CONVERT (0xDB) / ISTYPE (0xD9) take a mandatory 1-byte
268                // StackItemType operand. Without consuming it the operand byte
269                // was re-decoded as the next opcode, misaligning every
270                // subsequent instruction's offset in the listing (the compiler
271                // emits 0xDB 0x21 for essentially every storage-int load).
272                let Some(type_byte) = take(bytecode, &mut pc, 1) else {
273                    out.push_str(" <unexpected EOF>");
274                    break;
275                };
276                out.push_str(&format!(" type=0x{:02x}", type_byte[0]));
277            }
278
279            _ => {}
280        }
281
282        out.push('\n');
283    }
284
285    out
286}