UNPKG

@awayfl/avm1

Version:

Virtual machine for executing AS1 and AS2 code

438 lines (437 loc) 19.1 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 { var _a; import { CHECK_AVM1_HANG_EVERY, generateActionCalls } from './interpreter'; import { ParsedPushConstantAction, ParsedPushRegisterAction } from './parser'; import { AVM1ActionsData } from './context'; import { avm1DebuggerEnabled } from './settings'; import { ActionsDataStream } from './stream'; import { notImplemented } from '@awayfl/swf-loader'; var IS_INVALID_NAME = /[^A-Za-z0-9_/]+/g; var cachedActionsCalls = null; function getActionsCalls() { if (!cachedActionsCalls) { cachedActionsCalls = generateActionCalls(); } return cachedActionsCalls; } var CustomOperationAction = /** @class */ (function () { function CustomOperationAction(inlinePop) { if (inlinePop === void 0) { inlinePop = false; } this.inlinePop = inlinePop; } return CustomOperationAction; }()); var ActionOptMap = (_a = {}, _a[78 /* ActionCode.ActionGetMember */] = { AllowReturnValue: true, AllowStackToArgs: true, AllowCallapsDouble: true, ArgsCount: 2, }, _a[79 /* ActionCode.ActionSetMember */] = { AllowStackToArgs: true, ArgsCount: 3, }, _a[71 /* ActionCode.ActionAdd2 */] = { AllowStackToArgs: true, AllowReturnValue: true, PlainArgs: true, ArgsCount: 2, }, _a[103 /* ActionCode.ActionGreater */] = { AllowStackToArgs: true, AllowReturnValue: true, ArgsCount: 2, }, _a); /** * Bare-minimum JavaScript code generator to make debugging better. */ var ActionsDataCompiler = /** @class */ (function () { function ActionsDataCompiler() { } ActionsDataCompiler.prototype.convertArgs = function (args, id, res, ir) { var parts = []; var arg; var argsLen = args.length; var constant; var hint; var currentConstantPool; var registerNumber; var resName; for (var i = 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[arg.constantIndex]; parts.push(constant === undefined ? 'undefined' : JSON.stringify(constant)); } else { hint = ''; currentConstantPool = res.constantPool; if (currentConstantPool) { constant = currentConstantPool[arg.constantIndex]; hint = constant === undefined ? 'undefined' : JSON.stringify(constant); // preventing code breakage due to bad constant hint = hint.indexOf('*/') >= 0 ? '' : ' /* ' + hint + ' */'; } parts.push('constantPool[' + arg.constantIndex + ']' + hint); } } else if (arg instanceof ParsedPushRegisterAction) { registerNumber = 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 */ ActionsDataCompiler.prototype.convertAction = function (item, id, res, indexInBlock, ir, items, hoists) { // const calls = getActionsCalls(); var prevItem = items[indexInBlock - 1]; var flags = item.flags; var result = ''; if (flags === null || flags === void 0 ? void 0 : flags.optimised) { result = " /* ".concat(item.action.actionName, " optimised */\n"); } if (flags === null || flags === void 0 ? void 0 : flags.killed) { return " /* ".concat(item.action.actionName, " killed by optimiser */\n"); } if (!item.action.knownAction) { return " // unknown actionCode ".concat(item.action.actionCode, " at ").concat(item.action.position, "\n"); } switch (item.action.actionCode) { case 153 /* ActionCode.ActionJump */: case 62 /* ActionCode.ActionReturn */: return ''; case 136 /* ActionCode.ActionConstantPool */: res.constantPool = item.action.args[0]; hoists.forward["constPool".concat(id)] = "[".concat(this.convertArgs(item.action.args[0], id, res, ir), "]"); return " constantPool = ectx.constantPool = constPool".concat(id, ";\n"); case 150 /* ActionCode.ActionPush */: return result + ' stack.push(' + this.convertArgs(item.action.args, id, res, ir) + ');\n'; case 135 /* ActionCode.ActionStoreRegister */: { var 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 138 /* ActionCode.ActionWaitForFrame */: case 141 /* ActionCode.ActionWaitForFrame2 */: { var 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 157 /* ActionCode.ActionIf */: return ' if (!!stack.pop()) { position = ' + item.conditionalJumpTo + '; ' + 'checkTimeAfter -= ' + (indexInBlock + 1) + '; break; }\n'; default: { var args = item.action.args ? this.convertArgs(item.action.args, id, res, ir) : ''; if (args && !flags.PlainArgs) { args = '[' + args + ']'; } if (item.action.actionCode === 142 /* ActionCode.ActionDefineFunction2 */) { var name_1 = "defFunArgs".concat(id); hoists.forward[name_1] = args; args = name_1; } result += ' calls.' + item.action.actionName + '(ectx' + (args ? ', ' + args : '') + ');\n'; if (item.action.actionName == 'ActionCallMethod') { if (!prevItem) { result = "// strange oppcode at ".concat(item.action.position, "\n") + result; } if (prevItem && prevItem.action.actionCode == 150 /* ActionCode.ActionPush */) { var args_1 = this.convertArgs(prevItem.action.args, id - 1, res, ir); if (args_1 == '"gotoAndStop"' || args_1 == '"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; } } }; ActionsDataCompiler.prototype.inlineStackOpt = function (index, items, pushStack) { if (!pushStack.length) { return false; } var item = items[index]; var itemFlags = item.flags; var code = item.action.actionCode; var pushItem = items[pushStack[0]]; pushStack.length = 0; var flags = ActionOptMap[code]; if (!flags || !flags.AllowStackToArgs) { return false; } itemFlags.PlainArgs = flags.PlainArgs; var 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) { var index_1 = stackArgs.length - flags.ArgsCount; item.action.args = stackArgs.slice(index_1); stackArgs.length = index_1; pushItem.flags.optimised = true; } else { var 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 (var i = 0; i < delta; i++) { item.action.args.unshift(new CustomOperationAction(true)); } } itemFlags.optimised = true; return true; }; ActionsDataCompiler.prototype.optimiser = function (block) { var items = block.items; var pushStack = []; var lastStackOpPassed = true; for (var i = 0, l = items.length; i < l; i++) { var item = items[i]; var code = item.action.actionCode; item.flags = item.flags || {}; if (code !== 150 /* ActionCode.ActionPush */) { /* optimise push, push, push instruction to one push (arg, arg1, arg2) to many used in obfuscated code */ if (pushStack.length > 1) { var from = pushStack[0]; var to = pushStack[pushStack.length - 1]; var target = items[from].action; for (var i_1 = from + 1; i_1 <= to; i_1++) { target.args = target.args.concat(items[i_1].action.args); items[i_1].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 150 /* ActionCode.ActionPush */: { pushStack.push(i); // last op is pushStack, optimiser sould be passed lastStackOpPassed = true; break; } case 78 /* ActionCode.ActionGetMember */: { // collaps doubled calls to args var selfArgs = item.action.args; // args not inlined, skip if (!(selfArgs === null || selfArgs === void 0 ? void 0 : selfArgs.length)) { break; } var first = selfArgs[0]; // first item should be inline pop, that we shure that opp not statically passed if (!(first instanceof CustomOperationAction) || !first.inlinePop) { break; } var j = i - 1; // skip oppcodes, that is killed for (; j >= 0; j--) { if (!items[j].flags.killed) { break; } } if (j >= 0) { var topOpp = items[j]; // chain var topArgs = topOpp.action.args; if (topOpp.action.actionCode === 78 /* ActionCode.ActionGetMember */ && (topArgs === null || topArgs === void 0 ? void 0 : topArgs.length)) { topOpp.flags.killed = true; item.flags.optimised = true; // prepend arguments from top, drop first, because it pop selfArgs.shift(); for (var i_2 = topArgs.length - 1; i_2 >= 0; i_2--) { selfArgs.unshift(topArgs[i_2]); } } } } } } }; ActionsDataCompiler.prototype.generate = function (ir, debugPath) { var _this = this; if (debugPath === void 0) { debugPath = null; } var blocks = ir.blocks; var res = {}; var hoists = { forward: {}, back: {} }; var debugName = ir.dataId.replace(IS_INVALID_NAME, '_'); var 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'; var beforeHeader = ''; var 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 = ".concat(CHECK_AVM1_HANG_EVERY, "; ectx.context.checkTimeout(); }\n\n") + 'switch(position) {\n'; var uuid = 0; blocks.forEach(function (b) { fn += ' case ' + b.label + ':\n'; _this.optimiser(b); var actLines = b.items.map(function (item, index) { 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 (var key in hoists.forward) { beforeHeader += "var ".concat(key, " = ").concat(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; } }; return ActionsDataCompiler; }()); export { ActionsDataCompiler }; // 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, context) { var bytes = actionsData.bytes; var fn = null; if (bytes.length === 0 || bytes[0] === 0 /* ActionCode.None */) { // Empty/no actions or first command is ActionEnd. fn = actionsNoop; } else if (bytes.length >= 2 && bytes[1] === 0 /* ActionCode.None */) { // Single bytes actions: ActionPlay, ActionStop, ActionStopSounds // Example: 07 00 switch (bytes[0]) { case 6 /* ActionCode.ActionPlay */: fn = actionsPlay; break; case 7 /* ActionCode.ActionStop */: fn = actionsStop; break; case 9 /* ActionCode.ActionStopSounds */: fn = actionsStopSounds; break; } } else if (bytes.length >= 7 && bytes[6] === 0 /* ActionCode.None */ && bytes[0] === 129 /* ActionCode.ActionGotoFrame */ && bytes[1] === 2 && bytes[2] === 0 && bytes[5] === 6 /* ActionCode.ActionPlay */) { // ActionGotoFrame n, ActionPlay // Example: 81 02 00 04 00 06 00 var frameIndex = bytes[3] | (bytes[4] << 8); fn = actionsGotoFrame.bind(null, [frameIndex, true]); } else if (bytes.length >= 6 && bytes[0] === 140 /* ActionCode.ActionGoToLabel */ && bytes[2] === 0 && bytes.length >= bytes[1] + 5 && bytes[bytes[1] + 4] === 0 /* ActionCode.None */ && bytes[bytes[1] + 3] === 6 /* ActionCode.ActionPlay */) { // ActionGoToLabel s, ActonPlay // Example: 8c 03 00 73 31 00 06 00 var stream = new ActionsDataStream(bytes.subarray(3, 3 + bytes[1]), context.swfVersion); var label = stream.readString(); fn = actionsGotoLabel.bind(null, [label, true]); } // TODO debugger pause and breakpoints ? return fn; } function actionsNoop(ectx) { // no operations stub } function actionsPlay(ectx) { getActionsCalls().ActionPlay(ectx); } function actionsStop(ectx) { getActionsCalls().ActionStop(ectx); } function actionsStopSounds(ectx) { getActionsCalls().ActionStopSounds(ectx); } function actionsGotoFrame(args, ectx) { getActionsCalls().ActionGotoFrame(ectx, args); } function actionsGotoLabel(args, ectx) { getActionsCalls().ActionGoToLabel(ectx, args); }