disassemble.js

JavaScript source for the SWF disassembler, 142 lines: printing decoded instructions as a listing, with labels on branch targets and named registers. It's lib/disassemble.js in swf-disassembler.zip.

//  Prints decoded actions as a listing: one instruction per line with its offset, labels on every
//  branch target, and function, with and try bodies indented inside them.

import { hex, registerNames } from './actions.js';

export function disassemble(list, base) {
  const out = [];
  const targets = new Set();
  collectTargets(list, targets);
  print(list, base, targets, new Map(), '', out);
  return out.join('\n');
}

function collectTargets(list, targets) {
  for (const ins of list) {
    if (ins.target !== undefined)
      targets.add(ins.target);
    for (const body of [ins.body, ins.catchBody, ins.finallyBody])
      if (body)
        collectTargets(body, targets);
  }
}

function print(list, base, targets, regs, indent, out) {
  for (const ins of list) {
    if (targets.has(ins.offset))
      out.push(`${indent}${label(ins.offset, base)}:`);
    const at = hex(ins.offset - base);
    const line = (text) => out.push(`${indent}  ${at}  ${text}`);

    switch (ins.code) {
      case 0x88:
        line(`constantPool  ${ins.pool.length} strings`);
        ins.pool.forEach((s, i) => out.push(`${indent}          ${String(i).padStart(4)}  ${quote(s)}`));
        break;

      case 0x96:
        line(`push          ${ins.values.map((v) => value(v, regs)).join(', ')}`);
        break;

      case 0x99:
      case 0x9D:
        line(`${ins.name.padEnd(13)} ${label(ins.target, base)}`);
        break;

      case 0x87:
        line(`storeRegister ${register(ins.register, regs)}`);
        break;

      case 0x9B:
      case 0x8E: {
        const inner = new Map([...regs, ...registerNames(ins)]);
        const params = ins.params.map((p) => (p.register ? `r:${p.register}=` : '') + p.name).join(', ');
        let extra = '';
        if (ins.code === 0x8E) {
          const pre = Object.entries(ins.preload).filter(([k, v]) => v && !k.startsWith('suppress')).map(([k]) => k);
          const sup = Object.entries(ins.preload).filter(([k, v]) => v && k.startsWith('suppress')).map(([k]) => k);
          extra = `  ; ${ins.registerCount} registers` + (pre.length ? `, preload ${pre.join(' ')}` : '') + (sup.length ? `, ${sup.join(' ')}` : '');
        }
        line(`${ins.name} ${ins.fname || '(anonymous)'}(${params})${extra}`);
        out.push(`${indent}        {`);
        print(ins.body, base, targets, inner, indent + '    ', out);
        out.push(`${indent}        }`);
        break;
      }

      case 0x94:
        line('with');
        out.push(`${indent}        {`);
        print(ins.body, base, targets, regs, indent + '    ', out);
        out.push(`${indent}        }`);
        break;

      case 0x8F: {
        const caught = ins.catchInRegister ? `r:${ins.catchRegister}` : ins.catchName;
        line('try');
        out.push(`${indent}        {`);
        print(ins.body, base, targets, regs, indent + '    ', out);
        out.push(`${indent}        }`);
        if (ins.hasCatch) {
          out.push(`${indent}        catch (${caught}) {`);
          print(ins.catchBody, base, targets, regs, indent + '    ', out);
          out.push(`${indent}        }`);
        }
        if (ins.hasFinally) {
          out.push(`${indent}        finally {`);
          print(ins.finallyBody, base, targets, regs, indent + '    ', out);
          out.push(`${indent}        }`);
        }
        break;
      }

      case 0x81:
        line(`gotoFrame     ${ins.frame}`);
        break;
      case 0x83:
        line(`getURL        ${quote(ins.url)}, ${quote(ins.window)}`);
        break;
      case 0x8A:
        line(`waitForFrame  ${ins.frame}, skip ${ins.skip}`);
        break;
      case 0x8D:
        line(`waitForFrame2 skip ${ins.skip}`);
        break;
      case 0x8B:
      case 0x8C:
        line(`${ins.name.padEnd(13)} ${quote(ins.value)}`);
        break;
      case 0x9A:
        line(`getURL2       method ${ins.method}${ins.loadTarget ? ', target' : ''}${ins.loadVariables ? ', variables' : ''}`);
        break;
      case 0x9F:
        line(`gotoFrame2    ${ins.play ? 'play' : 'stop'}${ins.sceneBias ? `, scene bias ${ins.sceneBias}` : ''}`);
        break;

      default:
        line(ins.raw && ins.raw.length ? `${ins.name.padEnd(13)} ${[...ins.raw].map((b) => hex(b, 2)).join(' ')}` : ins.name);
    }
  }
}

export function label(offset, base) {
  return `L${hex(offset - base)}`;
}

function register(n, regs) {
  return regs.has(n) ? `r:${n} (${regs.get(n)})` : `r:${n}`;
}

function value(v, regs) {
  switch (v.type) {
    case 'string': return quote(v.value);
    case 'register': return register(v.value, regs);
    case 'null': return 'null';
    case 'undefined': return 'undefined';
    default: return String(v.value);
  }
}

export function quote(s) {
  return "'" + String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t') + "'";
}