UNPKG

@awayfl/avm1

Version:

Virtual machine for executing AS1 and AS2 code

505 lines (434 loc) 15.7 kB
/* * Copyright 2015 Mozilla Foundation * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ //module Shumway.AVM1 { import { CHECK_AVM1_HANG_EVERY, generateActionCalls } from './interpreter'; import { ActionCodeBlock, ActionCodeBlockItem, ActionItemFlags, AnalyzerResults } from './analyze'; import { ActionCode, ParsedPushConstantAction, ParsedPushRegisterAction } from './parser'; import { AVM1ActionsData, AVM1Context } from './context'; import { avm1DebuggerEnabled } from './settings'; import { ActionsDataStream } from './stream'; import { notImplemented } from '@awayfl/swf-loader'; const IS_INVALID_NAME = /[^A-Za-z0-9_/]+/g; interface IExecutionContext { constantPool: any[]; registers: any[]; stack: any[]; isEndOfActions: boolean; } let cachedActionsCalls: StringMap<Function> = null; function getActionsCalls() { if (!cachedActionsCalls) { cachedActionsCalls = generateActionCalls(); } return cachedActionsCalls; } class CustomOperationAction { constructor( public inlinePop: boolean = false) {} } interface IHoistMap { forward: StringMap<string>; back: StringMap<string>; } interface IOptFlags { // function support apply stack as arguments AllowStackToArgs?: boolean; // generate arguments instad of passing as array // (ctx, [a, b]) => (ctx, a, b) PlainArgs?: boolean; ArgsCount?: number; // function support return value AllowReturnValue?: boolean; AllowCallapsDouble?: boolean; } interface ISharedFlags extends ActionItemFlags, IOptFlags {} const ActionOptMap: NumberMap<IOptFlags> = { [ActionCode.ActionGetMember]: { AllowReturnValue: true, AllowStackToArgs: true, AllowCallapsDouble: true, ArgsCount: 2, }, [ActionCode.ActionSetMember]: { AllowStackToArgs: true, ArgsCount: 3, }, [ActionCode.ActionAdd2]: { AllowStackToArgs: true, AllowReturnValue: true, PlainArgs: true, ArgsCount: 2, }, [ActionCode.ActionGreater]: { AllowStackToArgs: true, AllowReturnValue: true, ArgsCount: 2, }, }; /** * Bare-minimum JavaScript code generator to make debugging better. */ export class ActionsDataCompiler { private convertArgs(args: any[], id: number, res, ir: AnalyzerResults): string { const parts: string[] = []; let arg; const argsLen: number = args.length; let constant; let hint: string; let currentConstantPool; let registerNumber: number; let resName: string; for (let i: number = 0; i < argsLen; i++) { arg = args[i]; if (typeof arg === 'object' && arg !== null && !Array.isArray(arg)) { if (arg instanceof ParsedPushConstantAction) { if (ir.singleConstantPool) { constant = ir.singleConstantPool[(<ParsedPushConstantAction> arg).constantIndex]; parts.push(constant === undefined ? 'undefined' : JSON.stringify(constant)); } else { hint = ''; currentConstantPool = res.constantPool; if (currentConstantPool) { constant = currentConstantPool[(<ParsedPushConstantAction> arg).constantIndex]; hint = constant === undefined ? 'undefined' : JSON.stringify(constant); // preventing code breakage due to bad constant hint = hint.indexOf('*/') >= 0 ? '' : ' /* ' + hint + ' */'; } parts.push('constantPool[' + (<ParsedPushConstantAction> arg).constantIndex + ']' + hint); } } else if (arg instanceof ParsedPushRegisterAction) { registerNumber = (<ParsedPushRegisterAction> arg).registerNumber; if (registerNumber < 0 || registerNumber >= ir.registersLimit) { parts.push('undefined'); // register is out of bounds -- undefined } else { parts.push('registers[' + registerNumber + ']'); } } else if (arg instanceof AVM1ActionsData) { resName = 'code_' + id + '_' + i; res[resName] = arg; parts.push('res.' + resName); } else if (arg instanceof CustomOperationAction) { if (arg.inlinePop) parts.push('stack.pop()'); } else { notImplemented('Unknown AVM1 action argument type'); } } else if (arg === undefined) { parts.push('undefined'); // special case } else { parts.push(JSON.stringify(arg)); } } return parts.join(','); } /* eslint-disable-next-line max-len */ private convertAction(item: ActionCodeBlockItem, id: number, res, indexInBlock: number, ir: AnalyzerResults, items: ActionCodeBlockItem[], hoists: IHoistMap): string { // const calls = getActionsCalls(); const prevItem = items[indexInBlock - 1]; const flags: ISharedFlags = item.flags; let result = ''; if (flags?.optimised) { result = ` /* ${item.action.actionName} optimised */\n`; } if (flags?.killed) { return ` /* ${item.action.actionName} killed by optimiser */\n`; } if (!item.action.knownAction) { return ` // unknown actionCode ${item.action.actionCode} at ${item.action.position}\n`; } switch (item.action.actionCode) { case ActionCode.ActionJump: case ActionCode.ActionReturn: return ''; case ActionCode.ActionConstantPool: res.constantPool = item.action.args[0]; hoists.forward[`constPool${id}`] = `[${this.convertArgs(item.action.args[0], id, res, ir)}]`; return ` constantPool = ectx.constantPool = constPool${id};\n`; case ActionCode.ActionPush: return result + ' stack.push(' + this.convertArgs(item.action.args, id, res, ir) + ');\n'; case ActionCode.ActionStoreRegister: { const registerNumber = item.action.args[0]; if (registerNumber < 0 || registerNumber >= ir.registersLimit) { return ''; // register is out of bounds -- noop } return ' registers[' + registerNumber + '] = stack[stack.length - 1];\n'; } case ActionCode.ActionWaitForFrame: case ActionCode.ActionWaitForFrame2: { const args = this.convertArgs(item.action.args, id, res, ir); return ' if (calls.' + item.action.actionName + '(ectx,[' + args + '])) { position = ' + item.conditionalJumpTo + '; ' + 'checkTimeAfter -= ' + (indexInBlock + 1) + '; break; }\n'; } case ActionCode.ActionIf: return ' if (!!stack.pop()) { position = ' + item.conditionalJumpTo + '; ' + 'checkTimeAfter -= ' + (indexInBlock + 1) + '; break; }\n'; default: { let args = item.action.args ? this.convertArgs(item.action.args, id, res, ir) : ''; if (args && !flags.PlainArgs) { args = '[' + args + ']'; } if (item.action.actionCode === ActionCode.ActionDefineFunction2) { const name = `defFunArgs${id}`; hoists.forward[name] = args; args = name; } result += ' calls.' + item.action.actionName + '(ectx' + (args ? ', ' + args : '') + ');\n'; if (item.action.actionName == 'ActionCallMethod') { if (!prevItem) { result = `// strange oppcode at ${item.action.position}\n` + result; } if (prevItem && prevItem.action.actionCode == ActionCode.ActionPush) { const args = this.convertArgs(prevItem.action.args, id - 1, res, ir); if (args == '"gotoAndStop"' || args == '"gotoAndPlay"') { //|| args=='"nextFrame"' || args=='"prevFrame"'){ /* eslint-disable-next-line max-len */ result += ' if(ectx.scopeList && ectx.scopeList.scope && ectx.scopeList.scope.adaptee && !ectx.scopeList.scope.adaptee.parent){ ectx.framescriptmanager.execute_avm1_constructors(); return;}\n'; } } } return result; } } } inlineStackOpt(index: number, items: ActionCodeBlockItem[], pushStack: number[]): boolean { if (!pushStack.length) { return false; } const item = items[index]; const itemFlags: ISharedFlags = item.flags; const code = item.action.actionCode; const pushItem = items[pushStack[0]]; pushStack.length = 0; const flags = ActionOptMap[code]; if (!flags || !flags.AllowStackToArgs) { return false; } itemFlags.PlainArgs = flags.PlainArgs; const stackArgs = pushItem.action.args; // push stack args to getMemberArgs, to reduce array movements if (stackArgs.length === flags.ArgsCount) { pushItem.flags.killed = true; item.action.args = stackArgs; } else if (stackArgs.length > flags.ArgsCount) { const index = stackArgs.length - flags.ArgsCount; item.action.args = stackArgs.slice(index); stackArgs.length = index; pushItem.flags.optimised = true; } else { const delta = flags.ArgsCount - stackArgs.length; if (delta > 1) { // Optimiser pop operands to arguments in reverse order // this is BUGed if args more that 1. Skip this; return false; } pushItem.flags.killed = true; item.action.args = stackArgs.slice(); for (let i = 0; i < delta; i++) { item.action.args.unshift(new CustomOperationAction(true)); } } itemFlags.optimised = true; return true; } optimiser(block: ActionCodeBlock): void { const items = block.items; const pushStack = []; let lastStackOpPassed = true; for (let i = 0, l = items.length; i < l; i++) { const item = items[i]; const code = item.action.actionCode; item.flags = item.flags || {}; if (code !== ActionCode.ActionPush) { /* optimise push, push, push instruction to one push (arg, arg1, arg2) to many used in obfuscated code */ if (pushStack.length > 1) { const from = pushStack[0]; const to = pushStack[pushStack.length - 1]; const target = items[from].action; for (let i = from + 1; i <= to; i++) { target.args = target.args.concat(items[i].action.args); items[i].flags.killed = true; } } // we should skip optimiser if can't shure, that stack is inlined clear if (lastStackOpPassed) { lastStackOpPassed = this.inlineStackOpt(i, items, pushStack); } else { lastStackOpPassed = false; } pushStack.length = 0; } switch (code) { case ActionCode.ActionPush: { pushStack.push(i); // last op is pushStack, optimiser sould be passed lastStackOpPassed = true; break; } case ActionCode.ActionGetMember: { // collaps doubled calls to args const selfArgs = item.action.args; // args not inlined, skip if (!selfArgs?.length) { break; } const first = selfArgs[0]; // first item should be inline pop, that we shure that opp not statically passed if (!(first instanceof CustomOperationAction) || !first.inlinePop) { break; } let j = i - 1; // skip oppcodes, that is killed for (; j >= 0; j--) { if (!items[j].flags.killed) { break; } } if (j >= 0) { const topOpp = items[j]; // chain const topArgs = topOpp.action.args; if (topOpp.action.actionCode === ActionCode.ActionGetMember && topArgs?.length) { topOpp.flags.killed = true; item.flags.optimised = true; // prepend arguments from top, drop first, because it pop selfArgs.shift(); for (let i = topArgs.length - 1; i >= 0; i--) { selfArgs.unshift(topArgs[i]); } } } } } } } generate(ir: AnalyzerResults, debugPath: string = null): Function { const blocks = ir.blocks; const res = {}; const hoists: IHoistMap = { forward: {}, back: {} }; const debugName = ir.dataId.replace(IS_INVALID_NAME, '_'); const header = 'return function ' + debugName + '(ectx) {\n' + 'var position = 0;\n' + 'var checkTimeAfter = 0;\n' + 'var constantPool = ectx.constantPool, registers = ectx.registers, stack = ectx.stack;\n'; let beforeHeader = ''; let fn = ''; if (avm1DebuggerEnabled.value) { fn += '/* Running ' + debugName + ' */ ' + 'if (Shumway.AVM1.Debugger.pause || Shumway.AVM1.Debugger.breakpoints.' + debugName + ') { debugger; }\n'; } fn += 'while (!ectx.isEndOfActions) {\n' + `if (checkTimeAfter <= 0) { checkTimeAfter = ${CHECK_AVM1_HANG_EVERY}; ectx.context.checkTimeout(); }\n\n` + 'switch(position) {\n'; let uuid = 0; blocks.forEach((b: ActionCodeBlock) => { fn += ' case ' + b.label + ':\n'; this.optimiser(b); const actLines = b.items.map((item: ActionCodeBlockItem, index: number) => { return this.convertAction(item, uuid++, res, index, ir, b.items, hoists); }); fn += actLines.join(''); fn += ' position = ' + b.jump + ';\n' + ' checkTimeAfter -= ' + b.items.length + ';\n' + ' break;\n'; }); fn += ' default: ectx.isEndOfActions = true; break;\n}\n}\n' + 'return stack.pop();};'; beforeHeader += '\n// hoisted vars\n'; for (const key in hoists.forward) { beforeHeader += `var ${key} = ${hoists.forward[key]};\n`; } beforeHeader += '\n'; fn = beforeHeader + header + fn; fn += '//# sourceURL=http://jit/' + (debugPath && !IS_INVALID_NAME.test(debugPath) ? debugPath : debugName); try { return (new Function('calls', 'res', fn))(getActionsCalls(), res); } catch (e) { // eslint-disable-next-line no-debugger debugger; throw e; } } } // Instead of compiling, we can match frequently used actions patterns and use // the dictionary functions without analyzing or compilations of the code. // The functions/patterns were selected by analyzing the large amount of // real-life SWFs. export function findWellknowCompilation(actionsData: AVM1ActionsData, context: AVM1Context): Function { const bytes = actionsData.bytes; let fn: Function = null; if (bytes.length === 0 || bytes[0] === ActionCode.None) { // Empty/no actions or first command is ActionEnd. fn = actionsNoop; } else if (bytes.length >= 2 && bytes[1] === ActionCode.None) { // Single bytes actions: ActionPlay, ActionStop, ActionStopSounds // Example: 07 00 switch (bytes[0]) { case ActionCode.ActionPlay: fn = actionsPlay; break; case ActionCode.ActionStop: fn = actionsStop; break; case ActionCode.ActionStopSounds: fn = actionsStopSounds; break; } } else if (bytes.length >= 7 && bytes[6] === ActionCode.None && bytes[0] === ActionCode.ActionGotoFrame && bytes[1] === 2 && bytes[2] === 0 && bytes[5] === ActionCode.ActionPlay) { // ActionGotoFrame n, ActionPlay // Example: 81 02 00 04 00 06 00 const frameIndex = bytes[3] | (bytes[4] << 8); fn = actionsGotoFrame.bind(null, [frameIndex, true]); } else if (bytes.length >= 6 && bytes[0] === ActionCode.ActionGoToLabel && bytes[2] === 0 && bytes.length >= bytes[1] + 5 && bytes[bytes[1] + 4] === ActionCode.None && bytes[bytes[1] + 3] === ActionCode.ActionPlay) { // ActionGoToLabel s, ActonPlay // Example: 8c 03 00 73 31 00 06 00 const stream = new ActionsDataStream(bytes.subarray(3, 3 + bytes[1]), context.swfVersion); const label = stream.readString(); fn = actionsGotoLabel.bind(null, [label, true]); } // TODO debugger pause and breakpoints ? return fn; } function actionsNoop(ectx: IExecutionContext) { // no operations stub } function actionsPlay(ectx: IExecutionContext) { getActionsCalls().ActionPlay(ectx); } function actionsStop(ectx: IExecutionContext) { getActionsCalls().ActionStop(ectx); } function actionsStopSounds(ectx: IExecutionContext) { getActionsCalls().ActionStopSounds(ectx); } function actionsGotoFrame(args: any[], ectx: IExecutionContext) { getActionsCalls().ActionGotoFrame(ectx, args); } function actionsGotoLabel(args: any[], ectx: IExecutionContext) { getActionsCalls().ActionGoToLabel(ectx, args); }