Disassembler
Turn a DoAction tag's AVM1 bytecode into readable text with Avm1Disassembler.Disassemble, choosing faithful p-code or best-effort AS2 reconstruction.
Overview
Avm1Disassembler (namespace ShockwaveFlash.Avm1.Text) is a static class that renders AVM1 bytecode as human-readable text. It is the read-only counterpart to the action model: it consumes the decoded Action objects and produces a string. It performs no control-flow analysis, no decompilation, and never executes anything — it walks the action list once and writes text.
There are two output dialects, selected by the Avm1DisassemblyKind enum:
namespace ShockwaveFlash.Avm1.Text;
public enum Avm1DisassemblyKind
{
Pcode,
As2
}Pcode(the default) is a faithful one-line-per-instruction listing.As2is a best-effort reconstruction of the original ActionScript 2 source.
Entry points
Disassemble has two overloads. One takes raw bytecode and decodes it for you; the other takes an already-decoded action list. Both default to Avm1DisassemblyKind.Pcode.
public static string Disassemble(ReadOnlyMemory<byte> bytecode, byte swfVersion, Avm1DisassemblyKind kind = Avm1DisassemblyKind.Pcode)
{
return Disassemble(Action.DecodeCollection(bytecode, swfVersion), swfVersion, kind);
}
public static string Disassemble(IReadOnlyList<Action> actions, byte swfVersion, Avm1DisassemblyKind kind = Avm1DisassemblyKind.Pcode)The bytecode overload simply forwards to Action.DecodeCollection(bytecode, swfVersion) (see the action model), so a DoAction tag's payload is turned into text in one call. swfVersion is needed because string decoding (the constant pool, push strings, identifiers) depends on the SWF version's text encoding.
The result is the assembled StringBuilder with a single TrimEnd('\n') applied, so there is no trailing newline.
var builder = new StringBuilder();
if (kind is Avm1DisassemblyKind.As2)
{
WriteAs2(builder, actions, swfVersion, 0, []);
}
else
{
IReadOnlyList<string> pool = [];
WritePcode(builder, actions, swfVersion, 0, ref pool);
}
return builder.ToString().TrimEnd('\n');Getting bytecode from a DoAction tag
A DoAction tag exposes its raw action bytes; pass them straight in along with the file's SWF version:
var text = Avm1Disassembler.Disassemble(doAction.ActionBytes, swf.Header.Version, Avm1DisassemblyKind.Pcode);Faithful p-code (Avm1DisassemblyKind.Pcode)
The p-code writer (WritePcode) emits exactly one textual line per action, in source order, with no reordering or elision. This is the dialect to use when you need a 1:1, lossless-ish view of what the bytecode actually contains.
What it captures:
- Every opcode by name. The default case falls back to
action.Opcode.ToString(), so even operand-less actions likePlay,Pop,Add,GetVariableprint their enum name verbatim. - Operands, formatted per action by the
PcodeHeadhelper. For example:
ActionGotoFrame a => $"GotoFrame {a.Frame}",
ActionGetURL a => $"GetURL {Quote(a.Url)}, {Quote(a.Target)}",
ActionStoreRegister a => $"StoreRegister {a.RegisterNumber}",
ActionJump a => $"Jump {a.BranchOffset}",
ActionIf a => $"If {a.BranchOffset}",
ActionDefineFunction a => $"DefineFunction {Quote(a.Name)}({string.Join(", ", a.Parameters)})",
ActionUnknown a => $"Unknown(0x{(byte)a.Opcode:X2})",- Branch offsets are printed as raw numbers (
Jump {a.BranchOffset},If {a.BranchOffset}). They are not resolved to labels or instruction indices — see limitations below. Pushis expanded one value per line. Each value inpush.PushValuesgets its ownPush ...line.- The constant pool is tracked and resolved. When an
ActionConstantPoolis seen, itsConstantsbecome the activepool, and laterConstant8/Constant16push values are rendered as the resolved string (viaFormatResolved) rather than an index. If an index is out of range it falls back toc{index}. - Nested bodies are decoded recursively and indented with two spaces per level.
ActionWith,ActionDefineFunction,ActionDefineFunction2, andActionTryre-decode their body memory viaAction.DecodeCollectionand emit a brace-delimited block.Tryadditionally emitsCatch(...)andFinallyblocks, with the catch target shown asr{CatchRegister}whenTryFlags.CatchInRegisteris set, otherwise theCatchVariablename.
Sample p-code output
A simple variable assignment (name = "value";) compiles to two pushes, a SetVariable, and End:
Push "name"
Push "value"
SetVariable
EndConstant-pool references are resolved back to their string when the pool is in scope. Given a ConstantPool of ["greeting"] followed by a Push Constant8(0):
ConstantPool "greeting"
Push "greeting"Nested blocks are decoded and indented. A With body containing Play then Pop:
With {
Play
Pop
}Best-effort AS2 (Avm1DisassemblyKind.As2)
The AS2 writer (WriteAs2) does a small symbolic stack evaluation to reconstruct something that reads like the original ActionScript 2. It maintains a Stack<string> of expression fragments and a Dictionary<int, string> of register contents, then rewrites stack-machine patterns into expressions and statements.
What it captures / reconstructs:
- Binary operators.
BinaryOperatormaps actions to infix symbols and rewrites... left right Opinto(left op right). For exampleActionAdd/ActionAdd2/ActionStringAddbecome+,ActionEquals/ActionEquals2/ActionStringEqualsbecome==,ActionLess/ActionLess2/ActionStringLessbecome<, plus-,*,/,%,>,&&,||,&,|,^. - Push values and registers. Pushes go onto the symbolic stack; a
PushValue.PushValueRegisterresolves to the last value stored into that register (viaActionStoreRegister), falling back tor{index}. - Variable and member access.
GetVariable/SetVariableproducenamereads andname = value;statements;GetMember/SetMemberproducetarget.member(ortarget[expr]when the name is not a valid identifier, via theMemberhelper). - Object and array literals.
InitObjectrebuilds{a: 1, b: 2}andInitArrayrebuilds[x, y]by popping the element count and then the elements. - Calls and construction.
CallFunction,CallMethod,NewObject, andNewMethodrebuildf(args),target.method(args), andnew Type(args), popping the argument count and arguments viaPopArguments. - Statements.
trace(...), plain expression statements fromPop, andfunction name(params) { ... }(named) or an inlinefunction(params) { ... }expression (anonymous) forDefineFunction/DefineFunction2. Function bodies are rendered recursively and indented byRenderAs2Body. - Unary not.
ActionNotbecomes!expr.
Whenever an action is reached that the AS2 writer does not model, it first flushes any pending stack expressions as statements, then prints that action using the same PcodeHead head as the p-code mode. So unrecognized opcodes degrade gracefully to a p-code line rather than being dropped.
Sample AS2 output
The same Push "name"; Push "value"; SetVariable sequence reconstructs to:
name = "value";A named function with a body of Push "hi"; Trace:
function f(a) {
trace("hi");
}Register tracking lets a stored value reappear at its use site. Storing "hi" into register 0, then assigning it to name, yields (the leftover "hi"; is the flushed Pop of the duplicated stack top):
"hi";
name = "hi";Linear-subset limitations
Both modes operate on a single linear pass over the action list. The AS2 mode in particular only reconstructs a straight-line subset of AVM1 and deliberately does not attempt real control-flow recovery. Concretely:
- No branch resolution.
JumpandIfprint their rawBranchOffset(a signed byte offset) in p-code. The AS2 writer has no case for them, so they fall through to the p-code-style head after a stack flush. There is no reconstruction ofif/else,while, orforfrom the underlying jumps. - No labels or addresses. Lines are not annotated with byte offsets or instruction indices, so a numeric branch offset cannot be correlated to a target line from the output alone.
- Symbolic stack is approximate.
Popon an empty stack yields the literal"undefined"(via thePophelper), andStoreRegisterwith nothing on the stack records"undefined". Counts forInitObject/InitArray/arguments are parsed best-effort withParseCount, which returns0for any non-integer fragment. Mismatched stack shapes therefore produce plausible-but-imperfect text rather than an error. - No semantic validation. The disassembler trusts the decoded actions; it does not verify that the reconstructed AS2 would recompile to the same bytecode. Treat AS2 output as a readable approximation and
Pcodeas the authoritative listing. - Constant pool scope is linear. The active pool is whatever the most recent
ActionConstantPoolset; there is no per-branch or per-scope pool analysis. An out-of-range or not-yet-seen constant index renders asc{index}.
When to use which
Use Pcode when you need an accurate, instruction-faithful view — debugging a decoder, diffing bytecode, or confirming exactly what a DoAction tag contains. Use As2 when you want a quick, readable sense of what the code does, accepting that branches and unusual stack patterns will be rendered loosely.
Action tree
Decode DoAction bytecode to a mutable action tree, mutate it, and re-encode byte-for-byte, with lenient vs strict handling of unknown and oversized opcodes.
Evaluator & coercion
How Avm1Machine linearly evaluates a DoAction byte stream into a global object table, the operators and coercion rules it implements, and how the Avm1Emitter writes a modified global table back to AVM1 bytecode.