/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * vim: set ts=8 sts=4 et sw=4 tw=99: * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* * JS bytecode descriptors, disassemblers, and (expression) decompilers. */ #include "jsopcodeinlines.h" #include #include #include #include #include "jsapi.h" #include "jsatom.h" #include "jscntxt.h" #include "jscompartment.h" #include "jsfun.h" #include "jsnum.h" #include "jsobj.h" #include "jsprf.h" #include "jsscript.h" #include "jsstr.h" #include "jstypes.h" #include "jsutil.h" #include "asmjs/AsmJSModule.h" #include "frontend/BytecodeCompiler.h" #include "frontend/SourceNotes.h" #include "js/CharacterEncoding.h" #include "vm/Opcodes.h" #include "vm/ScopeObject.h" #include "vm/Shape.h" #include "vm/StringBuffer.h" #include "jscntxtinlines.h" #include "jscompartmentinlines.h" #include "jsobjinlines.h" #include "jsscriptinlines.h" using namespace js; using namespace js::gc; using JS::AutoCheckCannotGC; using js::frontend::IsIdentifier; /* * Index limit must stay within 32 bits. */ JS_STATIC_ASSERT(sizeof(uint32_t) * JS_BITS_PER_BYTE >= INDEX_LIMIT_LOG2 + 1); const JSCodeSpec js_CodeSpec[] = { #define MAKE_CODESPEC(op,val,name,token,length,nuses,ndefs,format) {length,nuses,ndefs,format}, FOR_EACH_OPCODE(MAKE_CODESPEC) #undef MAKE_CODESPEC }; const unsigned js_NumCodeSpecs = JS_ARRAY_LENGTH(js_CodeSpec); /* * Each element of the array is either a source literal associated with JS * bytecode or null. */ static const char * const CodeToken[] = { #define TOKEN(op, val, name, token, ...) token, FOR_EACH_OPCODE(TOKEN) #undef TOKEN }; /* * Array of JS bytecode names used by PC count JSON, DEBUG-only Disassemble * and JIT debug spew. */ const char * const js_CodeName[] = { #define OPNAME(op, val, name, ...) name, FOR_EACH_OPCODE(OPNAME) #undef OPNAME }; /************************************************************************/ #define COUNTS_LEN 16 size_t js::GetVariableBytecodeLength(jsbytecode* pc) { JSOp op = JSOp(*pc); MOZ_ASSERT(js_CodeSpec[op].length == -1); switch (op) { case JSOP_TABLESWITCH: { /* Structure: default-jump case-low case-high case1-jump ... */ pc += JUMP_OFFSET_LEN; int32_t low = GET_JUMP_OFFSET(pc); pc += JUMP_OFFSET_LEN; int32_t high = GET_JUMP_OFFSET(pc); unsigned ncases = unsigned(high - low + 1); return 1 + 3 * JUMP_OFFSET_LEN + ncases * JUMP_OFFSET_LEN; } default: MOZ_CRASH("Unexpected op"); } } unsigned js::StackUses(JSScript* script, jsbytecode* pc) { JSOp op = (JSOp) *pc; const JSCodeSpec& cs = js_CodeSpec[op]; if (cs.nuses >= 0) return cs.nuses; MOZ_ASSERT(js_CodeSpec[op].nuses == -1); switch (op) { case JSOP_POPN: return GET_UINT16(pc); default: /* stack: fun, this, [argc arguments] */ MOZ_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL || op == JSOP_STRICTEVAL || op == JSOP_FUNCALL || op == JSOP_FUNAPPLY); return 2 + GET_ARGC(pc); } } unsigned js::StackDefs(JSScript* script, jsbytecode* pc) { JSOp op = (JSOp) *pc; const JSCodeSpec& cs = js_CodeSpec[op]; MOZ_ASSERT(cs.ndefs >= 0); return cs.ndefs; } static const char * const countBaseNames[] = { "interp" }; JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) == PCCounts::BASE_LIMIT); static const char * const countAccessNames[] = { "infer_mono", "infer_di", "infer_poly", "infer_barrier", "infer_nobarrier", "observe_undefined", "observe_null", "observe_boolean", "observe_int32", "observe_double", "observe_string", "observe_object" }; JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + JS_ARRAY_LENGTH(countAccessNames) == PCCounts::ACCESS_LIMIT); static const char * const countElementNames[] = { "id_int", "id_double", "id_other", "id_unknown", "elem_typed", "elem_packed", "elem_dense", "elem_other" }; JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + JS_ARRAY_LENGTH(countAccessNames) + JS_ARRAY_LENGTH(countElementNames) == PCCounts::ELEM_LIMIT); static const char * const countPropertyNames[] = { "prop_static", "prop_definite", "prop_other" }; JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + JS_ARRAY_LENGTH(countAccessNames) + JS_ARRAY_LENGTH(countPropertyNames) == PCCounts::PROP_LIMIT); static const char * const countArithNames[] = { "arith_int", "arith_double", "arith_other", "arith_unknown", }; JS_STATIC_ASSERT(JS_ARRAY_LENGTH(countBaseNames) + JS_ARRAY_LENGTH(countArithNames) == PCCounts::ARITH_LIMIT); /* static */ const char* PCCounts::countName(JSOp op, size_t which) { MOZ_ASSERT(which < numCounts(op)); if (which < BASE_LIMIT) return countBaseNames[which]; if (accessOp(op)) { if (which < ACCESS_LIMIT) return countAccessNames[which - BASE_LIMIT]; if (elementOp(op)) return countElementNames[which - ACCESS_LIMIT]; if (propertyOp(op)) return countPropertyNames[which - ACCESS_LIMIT]; MOZ_CRASH("bad op"); } if (arithOp(op)) return countArithNames[which - BASE_LIMIT]; MOZ_CRASH("bad op"); } void js::DumpIonScriptCounts(Sprinter* sp, jit::IonScriptCounts* ionCounts) { Sprint(sp, "IonScript [%lu blocks]:\n", ionCounts->numBlocks()); for (size_t i = 0; i < ionCounts->numBlocks(); i++) { const jit::IonBlockCounts& block = ionCounts->block(i); Sprint(sp, "BB #%lu [%05u]", block.id(), block.offset()); if (block.description()) Sprint(sp, " [inlined %s]", block.description()); for (size_t j = 0; j < block.numSuccessors(); j++) Sprint(sp, " -> #%lu", block.successor(j)); Sprint(sp, " :: %llu hits\n", block.hitCount()); Sprint(sp, "%s\n", block.code()); } } void js::DumpPCCounts(JSContext* cx, HandleScript script, Sprinter* sp) { MOZ_ASSERT(script->hasScriptCounts()); #ifdef DEBUG jsbytecode* pc = script->code(); while (pc < script->codeEnd()) { JSOp op = JSOp(*pc); jsbytecode* next = GetNextPc(pc); if (!Disassemble1(cx, script, pc, script->pcToOffset(pc), true, sp)) return; size_t total = PCCounts::numCounts(op); double* raw = script->getPCCounts(pc).rawCounts(); Sprint(sp, " {"); bool printed = false; for (size_t i = 0; i < total; i++) { double val = raw[i]; if (val) { if (printed) Sprint(sp, ", "); Sprint(sp, "\"%s\": %.0f", PCCounts::countName(op, i), val); printed = true; } } Sprint(sp, "}\n"); pc = next; } #endif jit::IonScriptCounts* ionCounts = script->getIonCounts(); while (ionCounts) { DumpIonScriptCounts(sp, ionCounts); ionCounts = ionCounts->previous(); } } void js::DumpCompartmentPCCounts(JSContext* cx) { for (ZoneCellIter i(cx->zone(), gc::FINALIZE_SCRIPT); !i.done(); i.next()) { RootedScript script(cx, i.get()); if (script->compartment() != cx->compartment()) continue; if (script->hasScriptCounts()) { Sprinter sprinter(cx); if (!sprinter.init()) return; fprintf(stdout, "--- SCRIPT %s:%d ---\n", script->filename(), (int) script->lineno()); DumpPCCounts(cx, script, &sprinter); fputs(sprinter.string(), stdout); fprintf(stdout, "--- END SCRIPT %s:%d ---\n", script->filename(), (int) script->lineno()); } } for (unsigned thingKind = FINALIZE_OBJECT0; thingKind < FINALIZE_OBJECT_LIMIT; thingKind++) { for (ZoneCellIter i(cx->zone(), (AllocKind) thingKind); !i.done(); i.next()) { JSObject* obj = i.get(); if (obj->compartment() != cx->compartment()) continue; if (obj->is()) { AsmJSModule& module = obj->as().module(); Sprinter sprinter(cx); if (!sprinter.init()) return; fprintf(stdout, "--- Asm.js Module ---\n"); for (size_t i = 0; i < module.numFunctionCounts(); i++) { jit::IonScriptCounts* counts = module.functionCounts(i); DumpIonScriptCounts(&sprinter, counts); } fputs(sprinter.string(), stdout); fprintf(stdout, "--- END Asm.js Module ---\n"); } } } } ///////////////////////////////////////////////////////////////////// // Bytecode Parser ///////////////////////////////////////////////////////////////////// namespace { class BytecodeParser { class Bytecode { public: Bytecode() { mozilla::PodZero(this); } // Whether this instruction has been analyzed to get its output defines // and stack. bool parsed : 1; // Stack depth before this opcode. uint32_t stackDepth; // Pointer to array of |stackDepth| offsets. An element at position N // in the array is the offset of the opcode that defined the // corresponding stack slot. The top of the stack is at position // |stackDepth - 1|. uint32_t* offsetStack; bool captureOffsetStack(LifoAlloc& alloc, const uint32_t* stack, uint32_t depth) { stackDepth = depth; offsetStack = alloc.newArray(stackDepth); if (stackDepth) { if (!offsetStack) return false; for (uint32_t n = 0; n < stackDepth; n++) offsetStack[n] = stack[n]; } return true; } // When control-flow merges, intersect the stacks, marking slots that // are defined by different offsets with the UINT32_MAX sentinel. // This is sufficient for forward control-flow. It doesn't grok loops // -- for that you would have to iterate to a fixed point -- but there // shouldn't be operands on the stack at a loop back-edge anyway. void mergeOffsetStack(const uint32_t* stack, uint32_t depth) { MOZ_ASSERT(depth == stackDepth); for (uint32_t n = 0; n < stackDepth; n++) if (offsetStack[n] != stack[n]) offsetStack[n] = UINT32_MAX; } }; JSContext* cx_; LifoAllocScope allocScope_; RootedScript script_; Bytecode** codeArray_; public: BytecodeParser(JSContext* cx, JSScript* script) : cx_(cx), allocScope_(&cx->tempLifoAlloc()), script_(cx, script), codeArray_(nullptr) { } bool parse(); #ifdef DEBUG bool isReachable(uint32_t offset) { return maybeCode(offset); } bool isReachable(const jsbytecode* pc) { return maybeCode(pc); } #endif uint32_t stackDepthAtPC(uint32_t offset) { // Sometimes the code generator in debug mode asks about the stack depth // of unreachable code (bug 932180 comment 22). Assume that unreachable // code has no operands on the stack. return getCode(offset).stackDepth; } uint32_t stackDepthAtPC(const jsbytecode* pc) { return stackDepthAtPC(script_->pcToOffset(pc)); } uint32_t offsetForStackOperand(uint32_t offset, int operand) { Bytecode& code = getCode(offset); if (operand < 0) { operand += code.stackDepth; MOZ_ASSERT(operand >= 0); } MOZ_ASSERT(uint32_t(operand) < code.stackDepth); return code.offsetStack[operand]; } jsbytecode* pcForStackOperand(jsbytecode* pc, int operand) { uint32_t offset = offsetForStackOperand(script_->pcToOffset(pc), operand); if (offset == UINT32_MAX) return nullptr; return script_->offsetToPC(offsetForStackOperand(script_->pcToOffset(pc), operand)); } private: LifoAlloc& alloc() { return allocScope_.alloc(); } void reportOOM() { allocScope_.releaseEarly(); ReportOutOfMemory(cx_); } uint32_t numSlots() { return 1 + script_->nfixed() + (script_->functionNonDelazifying() ? script_->functionNonDelazifying()->nargs() : 0); } uint32_t maximumStackDepth() { return script_->nslots() - script_->nfixed(); } Bytecode& getCode(uint32_t offset) { MOZ_ASSERT(offset < script_->length()); MOZ_ASSERT(codeArray_[offset]); return *codeArray_[offset]; } Bytecode& getCode(const jsbytecode* pc) { return getCode(script_->pcToOffset(pc)); } Bytecode* maybeCode(uint32_t offset) { MOZ_ASSERT(offset < script_->length()); return codeArray_[offset]; } Bytecode* maybeCode(const jsbytecode* pc) { return maybeCode(script_->pcToOffset(pc)); } uint32_t simulateOp(JSOp op, uint32_t offset, uint32_t* offsetStack, uint32_t stackDepth); inline bool addJump(uint32_t offset, uint32_t* currentOffset, uint32_t stackDepth, const uint32_t* offsetStack); }; } // anonymous namespace uint32_t BytecodeParser::simulateOp(JSOp op, uint32_t offset, uint32_t* offsetStack, uint32_t stackDepth) { uint32_t nuses = GetUseCount(script_, offset); uint32_t ndefs = GetDefCount(script_, offset); MOZ_ASSERT(stackDepth >= nuses); stackDepth -= nuses; MOZ_ASSERT(stackDepth + ndefs <= maximumStackDepth()); // Mark the current offset as defining its values on the offset stack, // unless it just reshuffles the stack. In that case we want to preserve // the opcode that generated the original value. switch (op) { default: for (uint32_t n = 0; n != ndefs; ++n) offsetStack[stackDepth + n] = offset; break; case JSOP_CASE: /* Keep the switch value. */ MOZ_ASSERT(ndefs == 1); break; case JSOP_DUP: MOZ_ASSERT(ndefs == 2); if (offsetStack) offsetStack[stackDepth + 1] = offsetStack[stackDepth]; break; case JSOP_DUP2: MOZ_ASSERT(ndefs == 4); if (offsetStack) { offsetStack[stackDepth + 2] = offsetStack[stackDepth]; offsetStack[stackDepth + 3] = offsetStack[stackDepth + 1]; } break; case JSOP_DUPAT: { MOZ_ASSERT(ndefs == 1); jsbytecode* pc = script_->offsetToPC(offset); unsigned n = GET_UINT24(pc); MOZ_ASSERT(n < stackDepth); if (offsetStack) offsetStack[stackDepth] = offsetStack[stackDepth - 1 - n]; break; } case JSOP_SWAP: MOZ_ASSERT(ndefs == 2); if (offsetStack) { uint32_t tmp = offsetStack[stackDepth + 1]; offsetStack[stackDepth + 1] = offsetStack[stackDepth]; offsetStack[stackDepth] = tmp; } break; } stackDepth += ndefs; return stackDepth; } bool BytecodeParser::addJump(uint32_t offset, uint32_t* currentOffset, uint32_t stackDepth, const uint32_t* offsetStack) { MOZ_ASSERT(offset < script_->length()); Bytecode*& code = codeArray_[offset]; if (!code) { code = alloc().new_(); if (!code) return false; if (!code->captureOffsetStack(alloc(), offsetStack, stackDepth)) { reportOOM(); return false; } } else { code->mergeOffsetStack(offsetStack, stackDepth); } if (offset < *currentOffset && !code->parsed) { // Backedge in a while/for loop, whose body has not been parsed due // to a lack of fallthrough at the loop head. Roll back the offset // to analyze the body. *currentOffset = offset; } return true; } bool BytecodeParser::parse() { MOZ_ASSERT(!codeArray_); uint32_t length = script_->length(); codeArray_ = alloc().newArray(length); if (!codeArray_) { reportOOM(); return false; } mozilla::PodZero(codeArray_, length); // Fill in stack depth and definitions at initial bytecode. Bytecode* startcode = alloc().new_(); if (!startcode) { reportOOM(); return false; } // Fill in stack depth and definitions at initial bytecode. uint32_t* offsetStack = alloc().newArray(maximumStackDepth()); if (maximumStackDepth() && !offsetStack) { reportOOM(); return false; } startcode->stackDepth = 0; codeArray_[0] = startcode; uint32_t offset, nextOffset = 0; while (nextOffset < length) { offset = nextOffset; Bytecode* code = maybeCode(offset); jsbytecode* pc = script_->offsetToPC(offset); JSOp op = (JSOp)*pc; MOZ_ASSERT(op < JSOP_LIMIT); // Immediate successor of this bytecode. uint32_t successorOffset = offset + GetBytecodeLength(pc); // Next bytecode to analyze. This is either the successor, or is an // earlier bytecode if this bytecode has a loop backedge. nextOffset = successorOffset; if (!code) { // Haven't found a path by which this bytecode is reachable. continue; } if (code->parsed) { // No need to reparse. continue; } code->parsed = true; uint32_t stackDepth = simulateOp(op, offset, offsetStack, code->stackDepth); switch (op) { case JSOP_TABLESWITCH: { uint32_t defaultOffset = offset + GET_JUMP_OFFSET(pc); jsbytecode* pc2 = pc + JUMP_OFFSET_LEN; int32_t low = GET_JUMP_OFFSET(pc2); pc2 += JUMP_OFFSET_LEN; int32_t high = GET_JUMP_OFFSET(pc2); pc2 += JUMP_OFFSET_LEN; if (!addJump(defaultOffset, &nextOffset, stackDepth, offsetStack)) return false; for (int32_t i = low; i <= high; i++) { uint32_t targetOffset = offset + GET_JUMP_OFFSET(pc2); if (targetOffset != offset) { if (!addJump(targetOffset, &nextOffset, stackDepth, offsetStack)) return false; } pc2 += JUMP_OFFSET_LEN; } break; } case JSOP_TRY: { // Everything between a try and corresponding catch or finally is conditional. // Note that there is no problem with code which is skipped by a thrown // exception but is not caught by a later handler in the same function: // no more code will execute, and it does not matter what is defined. JSTryNote* tn = script_->trynotes()->vector; JSTryNote* tnlimit = tn + script_->trynotes()->length; for (; tn < tnlimit; tn++) { uint32_t startOffset = script_->mainOffset() + tn->start; if (startOffset == offset + 1) { uint32_t catchOffset = startOffset + tn->length; if (tn->kind == JSTRY_CATCH || tn->kind == JSTRY_FINALLY) { if (!addJump(catchOffset, &nextOffset, stackDepth, offsetStack)) return false; } } } break; } default: break; } // Check basic jump opcodes, which may or may not have a fallthrough. if (IsJumpOpcode(op)) { // Case instructions do not push the lvalue back when branching. uint32_t newStackDepth = stackDepth; if (op == JSOP_CASE) newStackDepth--; uint32_t targetOffset = offset + GET_JUMP_OFFSET(pc); if (!addJump(targetOffset, &nextOffset, newStackDepth, offsetStack)) return false; } // Handle any fallthrough from this opcode. if (BytecodeFallsThrough(op)) { MOZ_ASSERT(successorOffset < script_->length()); Bytecode*& nextcode = codeArray_[successorOffset]; if (!nextcode) { nextcode = alloc().new_(); if (!nextcode) { reportOOM(); return false; } if (!nextcode->captureOffsetStack(alloc(), offsetStack, stackDepth)) { reportOOM(); return false; } } else { nextcode->mergeOffsetStack(offsetStack, stackDepth); } } } return true; } #ifdef DEBUG bool js::ReconstructStackDepth(JSContext* cx, JSScript* script, jsbytecode* pc, uint32_t* depth, bool* reachablePC) { BytecodeParser parser(cx, script); if (!parser.parse()) return false; *reachablePC = parser.isReachable(pc); if (*reachablePC) *depth = parser.stackDepthAtPC(pc); return true; } /* * If pc != nullptr, include a prefix indicating whether the PC is at the * current line. If showAll is true, include the source note type and the * entry stack depth. */ static bool DisassembleAtPC(JSContext* cx, JSScript* scriptArg, bool lines, jsbytecode* pc, bool showAll, Sprinter* sp) { RootedScript script(cx, scriptArg); BytecodeParser parser(cx, script); jsbytecode* next, *end; unsigned len; if (showAll && !parser.parse()) return false; if (showAll) Sprint(sp, "%s:%u\n", script->filename(), script->lineno()); if (pc != nullptr) sp->put(" "); if (showAll) sp->put("sn stack "); sp->put("loc "); if (lines) sp->put("line"); sp->put(" op\n"); if (pc != nullptr) sp->put(" "); if (showAll) sp->put("-- ----- "); sp->put("----- "); if (lines) sp->put("----"); sp->put(" --\n"); next = script->code(); end = script->codeEnd(); while (next < end) { if (next == script->main()) sp->put("main:\n"); if (pc != nullptr) { if (pc == next) sp->put("--> "); else sp->put(" "); } if (showAll) { jssrcnote* sn = GetSrcNote(cx, script, next); if (sn) { MOZ_ASSERT(!SN_IS_TERMINATOR(sn)); jssrcnote* next = SN_NEXT(sn); while (!SN_IS_TERMINATOR(next) && SN_DELTA(next) == 0) { Sprint(sp, "%02u\n ", SN_TYPE(sn)); sn = next; next = SN_NEXT(sn); } Sprint(sp, "%02u ", SN_TYPE(sn)); } else sp->put(" "); if (parser.isReachable(next)) Sprint(sp, "%05u ", parser.stackDepthAtPC(next)); else Sprint(sp, " "); } len = Disassemble1(cx, script, next, script->pcToOffset(next), lines, sp); if (!len) return false; next += len; } return true; } bool js::Disassemble(JSContext* cx, HandleScript script, bool lines, Sprinter* sp) { return DisassembleAtPC(cx, script, lines, nullptr, false, sp); } static char* QuoteString(Sprinter* sp, JSString* str, char16_t quote); static bool ToDisassemblySource(JSContext* cx, HandleValue v, JSAutoByteString* bytes) { if (v.isString()) { Sprinter sprinter(cx); if (!sprinter.init()) return false; char* nbytes = QuoteString(&sprinter, v.toString(), '"'); if (!nbytes) return false; nbytes = JS_sprintf_append(nullptr, "%s", nbytes); if (!nbytes) return false; bytes->initBytes(nbytes); return true; } JSRuntime* rt = cx->runtime(); if (rt->isHeapBusy() || !rt->gc.isAllocAllowed()) { char* source = JS_sprintf_append(nullptr, ""); if (!source) return false; bytes->initBytes(source); return true; } if (v.isObject()) { JSObject& obj = v.toObject(); if (obj.is()) { Rooted block(cx, &obj.as()); char* source = JS_sprintf_append(nullptr, "depth %d {", block->localOffset()); if (!source) return false; Shape::Range r(cx, block->lastProperty()); while (!r.empty()) { Rooted shape(cx, &r.front()); JSAtom* atom = JSID_IS_INT(shape->propid()) ? cx->names().empty : JSID_TO_ATOM(shape->propid()); JSAutoByteString bytes; if (!AtomToPrintableString(cx, atom, &bytes)) return false; r.popFront(); source = JS_sprintf_append(source, "%s: %d%s", bytes.ptr(), block->shapeToIndex(*shape), !r.empty() ? ", " : ""); if (!source) return false; } source = JS_sprintf_append(source, "}"); if (!source) return false; bytes->initBytes(source); return true; } if (obj.is()) { RootedFunction fun(cx, &obj.as()); JSString* str = JS_DecompileFunction(cx, fun, JS_DONT_PRETTY_PRINT); if (!str) return false; return bytes->encodeLatin1(cx, str); } if (obj.is()) { JSString* source = obj.as().toString(cx); if (!source) return false; return bytes->encodeLatin1(cx, source); } } return !!ValueToPrintable(cx, v, bytes, true); } unsigned js::Disassemble1(JSContext* cx, HandleScript script, jsbytecode* pc, unsigned loc, bool lines, Sprinter* sp) { JSOp op = (JSOp)*pc; if (op >= JSOP_LIMIT) { char numBuf1[12], numBuf2[12]; JS_snprintf(numBuf1, sizeof numBuf1, "%d", op); JS_snprintf(numBuf2, sizeof numBuf2, "%d", JSOP_LIMIT); JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_BYTECODE_TOO_BIG, numBuf1, numBuf2); return 0; } const JSCodeSpec* cs = &js_CodeSpec[op]; ptrdiff_t len = (ptrdiff_t) cs->length; Sprint(sp, "%05u:", loc); if (lines) Sprint(sp, "%4u", PCToLineNumber(script, pc)); Sprint(sp, " %s", js_CodeName[op]); switch (JOF_TYPE(cs->format)) { case JOF_BYTE: // Scan the trynotes to find the associated catch block // and make the try opcode look like a jump instruction // with an offset. This simplifies code coverage analysis // based on this disassembled output. if (op == JSOP_TRY) { TryNoteArray* trynotes = script->trynotes(); uint32_t i; for(i = 0; i < trynotes->length; i++) { JSTryNote note = trynotes->vector[i]; if (note.kind == JSTRY_CATCH && note.start == loc + 1) { Sprint(sp, " %u (%+d)", (unsigned int) (loc+note.length+1), (int) (note.length+1)); break; } } } break; case JOF_JUMP: { ptrdiff_t off = GET_JUMP_OFFSET(pc); Sprint(sp, " %u (%+d)", loc + (int) off, (int) off); break; } case JOF_SCOPECOORD: { RootedValue v(cx, StringValue(ScopeCoordinateName(cx->runtime()->scopeCoordinateNameCache, script, pc))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, v, &bytes)) return 0; ScopeCoordinate sc(pc); Sprint(sp, " %s (hops = %u, slot = %u)", bytes.ptr(), sc.hops(), sc.slot()); break; } case JOF_ATOM: { RootedValue v(cx, StringValue(script->getAtom(GET_UINT32_INDEX(pc)))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, v, &bytes)) return 0; Sprint(sp, " %s", bytes.ptr()); break; } case JOF_DOUBLE: { RootedValue v(cx, script->getConst(GET_UINT32_INDEX(pc))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, v, &bytes)) return 0; Sprint(sp, " %s", bytes.ptr()); break; } case JOF_OBJECT: { /* Don't call obj.toSource if analysis/inference is active. */ if (script->zone()->types.activeAnalysis) { Sprint(sp, " object"); break; } JSObject* obj = script->getObject(GET_UINT32_INDEX(pc)); { JSAutoByteString bytes; RootedValue v(cx, ObjectValue(*obj)); if (!ToDisassemblySource(cx, v, &bytes)) return 0; Sprint(sp, " %s", bytes.ptr()); } break; } case JOF_REGEXP: { JSObject* obj = script->getRegExp(GET_UINT32_INDEX(pc)); JSAutoByteString bytes; RootedValue v(cx, ObjectValue(*obj)); if (!ToDisassemblySource(cx, v, &bytes)) return 0; Sprint(sp, " %s", bytes.ptr()); break; } case JOF_TABLESWITCH: { int32_t i, low, high; ptrdiff_t off = GET_JUMP_OFFSET(pc); jsbytecode* pc2 = pc + JUMP_OFFSET_LEN; low = GET_JUMP_OFFSET(pc2); pc2 += JUMP_OFFSET_LEN; high = GET_JUMP_OFFSET(pc2); pc2 += JUMP_OFFSET_LEN; Sprint(sp, " defaultOffset %d low %d high %d", int(off), low, high); for (i = low; i <= high; i++) { off = GET_JUMP_OFFSET(pc2); Sprint(sp, "\n\t%d: %d", i, int(off)); pc2 += JUMP_OFFSET_LEN; } len = 1 + pc2 - pc; break; } case JOF_QARG: Sprint(sp, " %u", GET_ARGNO(pc)); break; case JOF_LOCAL: Sprint(sp, " %u", GET_LOCALNO(pc)); break; { int i; case JOF_UINT16: i = (int)GET_UINT16(pc); goto print_int; case JOF_UINT24: MOZ_ASSERT(len == 4); i = (int)GET_UINT24(pc); goto print_int; case JOF_UINT8: i = GET_UINT8(pc); goto print_int; case JOF_INT8: i = GET_INT8(pc); goto print_int; case JOF_INT32: MOZ_ASSERT(op == JSOP_INT32); i = GET_INT32(pc); print_int: Sprint(sp, " %d", i); break; } default: { char numBuf[12]; JS_snprintf(numBuf, sizeof numBuf, "%lx", (unsigned long) cs->format); JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_UNKNOWN_FORMAT, numBuf); return 0; } } sp->put("\n"); return len; } #endif /* DEBUG */ /************************************************************************/ const size_t Sprinter::DefaultSize = 64; bool Sprinter::realloc_(size_t newSize) { MOZ_ASSERT(newSize > (size_t) offset); char* newBuf = (char*) js_realloc(base, newSize); if (!newBuf) { reportOutOfMemory(); return false; } base = newBuf; size = newSize; base[size - 1] = 0; return true; } Sprinter::Sprinter(ExclusiveContext* cx) : context(cx), #ifdef DEBUG initialized(false), #endif base(nullptr), size(0), offset(0), reportedOOM(false) { } Sprinter::~Sprinter() { #ifdef DEBUG if (initialized) checkInvariants(); #endif js_free(base); } bool Sprinter::init() { MOZ_ASSERT(!initialized); base = (char*) js_malloc(DefaultSize); if (!base) { reportOutOfMemory(); return false; } #ifdef DEBUG initialized = true; #endif *base = 0; size = DefaultSize; base[size - 1] = 0; return true; } void Sprinter::checkInvariants() const { MOZ_ASSERT(initialized); MOZ_ASSERT((size_t) offset < size); MOZ_ASSERT(base[size - 1] == 0); } const char* Sprinter::string() const { return base; } const char* Sprinter::stringEnd() const { return base + offset; } char* Sprinter::stringAt(ptrdiff_t off) const { MOZ_ASSERT(off >= 0 && (size_t) off < size); return base + off; } char& Sprinter::operator[](size_t off) { MOZ_ASSERT(off < size); return *(base + off); } char* Sprinter::reserve(size_t len) { InvariantChecker ic(this); while (len + 1 > size - offset) { /* Include trailing \0 */ if (!realloc_(size * 2)) return nullptr; } char* sb = base + offset; offset += len; return sb; } ptrdiff_t Sprinter::put(const char* s, size_t len) { InvariantChecker ic(this); const char* oldBase = base; const char* oldEnd = base + size; ptrdiff_t oldOffset = offset; char* bp = reserve(len); if (!bp) return -1; /* s is within the buffer already */ if (s >= oldBase && s < oldEnd) { /* buffer was realloc'ed */ if (base != oldBase) s = stringAt(s - oldBase); /* this is where it lives now */ memmove(bp, s, len); } else { js_memcpy(bp, s, len); } bp[len] = 0; return oldOffset; } ptrdiff_t Sprinter::put(const char* s) { return put(s, strlen(s)); } ptrdiff_t Sprinter::putString(JSString* s) { InvariantChecker ic(this); size_t length = s->length(); size_t size = length; ptrdiff_t oldOffset = offset; char* buffer = reserve(size); if (!buffer) return -1; JSLinearString* linear = s->ensureLinear(context); if (!linear) return -1; AutoCheckCannotGC nogc; if (linear->hasLatin1Chars()) mozilla::PodCopy(reinterpret_cast(buffer), linear->latin1Chars(nogc), length); else DeflateStringToBuffer(nullptr, linear->twoByteChars(nogc), length, buffer, &size); buffer[size] = 0; return oldOffset; } int Sprinter::printf(const char* fmt, ...) { InvariantChecker ic(this); do { va_list va; va_start(va, fmt); int i = vsnprintf(base + offset, size - offset, fmt, va); va_end(va); if (i > -1 && (size_t) i < size - offset) { offset += i; return i; } } while (realloc_(size * 2)); return -1; } ptrdiff_t Sprinter::getOffset() const { return offset; } void Sprinter::reportOutOfMemory() { if (reportedOOM) return; if (context) ReportOutOfMemory(context); reportedOOM = true; } bool Sprinter::hadOutOfMemory() const { return reportedOOM; } ptrdiff_t js::Sprint(Sprinter* sp, const char* format, ...) { va_list ap; char* bp; ptrdiff_t offset; va_start(ap, format); bp = JS_vsmprintf(format, ap); /* XXX vsaprintf */ va_end(ap); if (!bp) { sp->reportOutOfMemory(); return -1; } offset = sp->put(bp); js_free(bp); return offset; } const char js_EscapeMap[] = { '\b', 'b', '\f', 'f', '\n', 'n', '\r', 'r', '\t', 't', '\v', 'v', '"', '"', '\'', '\'', '\\', '\\', '\0' }; template static char* QuoteString(Sprinter* sp, const CharT* s, size_t length, char16_t quote) { /* Sample off first for later return value pointer computation. */ ptrdiff_t offset = sp->getOffset(); if (quote && Sprint(sp, "%c", char(quote)) < 0) return nullptr; const CharT* end = s + length; /* Loop control variables: end points at end of string sentinel. */ for (const CharT* t = s; t < end; s = ++t) { /* Move t forward from s past un-quote-worthy characters. */ char16_t c = *t; while (c < 127 && isprint(c) && c != quote && c != '\\' && c != '\t') { c = *++t; if (t == end) break; } { ptrdiff_t len = t - s; ptrdiff_t base = sp->getOffset(); if (!sp->reserve(len)) return nullptr; for (ptrdiff_t i = 0; i < len; ++i) (*sp)[base + i] = char(*s++); (*sp)[base + len] = 0; } if (t == end) break; /* Use js_EscapeMap, \u, or \x only if necessary. */ const char* escape; if (!(c >> 8) && c != 0 && (escape = strchr(js_EscapeMap, int(c))) != nullptr) { if (Sprint(sp, "\\%c", escape[1]) < 0) return nullptr; } else { /* * Use \x only if the high byte is 0 and we're in a quoted string, * because ECMA-262 allows only \u, not \x, in Unicode identifiers * (see bug 621814). */ if (Sprint(sp, (quote && !(c >> 8)) ? "\\x%02X" : "\\u%04X", c) < 0) return nullptr; } } /* Sprint the closing quote and return the quoted string. */ if (quote && Sprint(sp, "%c", char(quote)) < 0) return nullptr; /* * If we haven't Sprint'd anything yet, Sprint an empty string so that * the return below gives a valid result. */ if (offset == sp->getOffset() && Sprint(sp, "") < 0) return nullptr; return sp->stringAt(offset); } static char* QuoteString(Sprinter* sp, JSString* str, char16_t quote) { JSLinearString* linear = str->ensureLinear(sp->context); if (!linear) return nullptr; AutoCheckCannotGC nogc; return linear->hasLatin1Chars() ? QuoteString(sp, linear->latin1Chars(nogc), linear->length(), quote) : QuoteString(sp, linear->twoByteChars(nogc), linear->length(), quote); } JSString* js::QuoteString(ExclusiveContext* cx, JSString* str, char16_t quote) { Sprinter sprinter(cx); if (!sprinter.init()) return nullptr; char* bytes = QuoteString(&sprinter, str, quote); if (!bytes) return nullptr; return NewStringCopyZ(cx, bytes); } /************************************************************************/ namespace { /* * The expression decompiler is invoked by error handling code to produce a * string representation of the erroring expression. As it's only a debugging * tool, it only supports basic expressions. For anything complicated, it simply * puts "(intermediate value)" into the error result. * * Here's the basic algorithm: * * 1. Find the stack location of the value whose expression we wish to * decompile. The error handler can explicitly pass this as an * argument. Otherwise, we search backwards down the stack for the offending * value. * * 2. Instantiate and run a BytecodeParser for the current frame. This creates a * stack of pcs parallel to the interpreter stack; given an interpreter stack * location, the corresponding pc stack location contains the opcode that pushed * the value in the interpreter. Now, with the result of step 1, we have the * opcode responsible for pushing the value we want to decompile. * * 3. Pass the opcode to decompilePC. decompilePC is the main decompiler * routine, responsible for a string representation of the expression that * generated a certain stack location. decompilePC looks at one opcode and * returns the JS source equivalent of that opcode. * * 4. Expressions can, of course, contain subexpressions. For example, the * literals "4" and "5" are subexpressions of the addition operator in "4 + * 5". If we need to decompile a subexpression, we call decompilePC (step 2) * recursively on the operands' pcs. The result is a depth-first traversal of * the expression tree. * */ struct ExpressionDecompiler { JSContext* cx; RootedScript script; RootedFunction fun; BytecodeParser parser; Sprinter sprinter; ExpressionDecompiler(JSContext* cx, JSScript* script, JSFunction* fun) : cx(cx), script(cx, script), fun(cx, fun), parser(cx, script), sprinter(cx) {} bool init(); bool decompilePCForStackOperand(jsbytecode* pc, int i); bool decompilePC(jsbytecode* pc); JSAtom* getLocal(uint32_t local, jsbytecode* pc); JSAtom* getArg(unsigned slot); JSAtom* loadAtom(jsbytecode* pc); bool quote(JSString* s, uint32_t quote); bool write(const char* s); bool write(JSString* str); bool getOutput(char** out); }; bool ExpressionDecompiler::decompilePCForStackOperand(jsbytecode* pc, int i) { pc = parser.pcForStackOperand(pc, i); if (!pc) return write("(intermediate value)"); return decompilePC(pc); } bool ExpressionDecompiler::decompilePC(jsbytecode* pc) { MOZ_ASSERT(script->containsPC(pc)); JSOp op = (JSOp)*pc; if (const char* token = CodeToken[op]) { // Handle simple cases of binary and unary operators. switch (js_CodeSpec[op].nuses) { case 2: { jssrcnote* sn = GetSrcNote(cx, script, pc); if (!sn || SN_TYPE(sn) != SRC_ASSIGNOP) return write("(") && decompilePCForStackOperand(pc, -2) && write(" ") && write(token) && write(" ") && decompilePCForStackOperand(pc, -1) && write(")"); break; } case 1: return write(token) && write("(") && decompilePCForStackOperand(pc, -1) && write(")"); default: break; } } switch (op) { case JSOP_GETGNAME: case JSOP_GETNAME: case JSOP_GETINTRINSIC: return write(loadAtom(pc)); case JSOP_GETARG: { unsigned slot = GET_ARGNO(pc); JSAtom* atom = getArg(slot); return write(atom); } case JSOP_GETLOCAL: { uint32_t i = GET_LOCALNO(pc); if (JSAtom* atom = getLocal(i, pc)) return write(atom); return write("(intermediate value)"); } case JSOP_GETALIASEDVAR: { JSAtom* atom = ScopeCoordinateName(cx->runtime()->scopeCoordinateNameCache, script, pc); MOZ_ASSERT(atom); return write(atom); } case JSOP_LENGTH: case JSOP_GETPROP: case JSOP_CALLPROP: { RootedAtom prop(cx, (op == JSOP_LENGTH) ? cx->names().length : loadAtom(pc)); if (!decompilePCForStackOperand(pc, -1)) return false; if (IsIdentifier(prop)) { return write(".") && quote(prop, '\0'); } return write("[") && quote(prop, '\'') && write("]"); } case JSOP_GETELEM: case JSOP_CALLELEM: return decompilePCForStackOperand(pc, -2) && write("[") && decompilePCForStackOperand(pc, -1) && write("]"); case JSOP_NULL: return write(js_null_str); case JSOP_TRUE: return write(js_true_str); case JSOP_FALSE: return write(js_false_str); case JSOP_ZERO: case JSOP_ONE: case JSOP_INT8: case JSOP_UINT16: case JSOP_UINT24: case JSOP_INT32: return sprinter.printf("%d", GetBytecodeInteger(pc)) >= 0; case JSOP_STRING: return quote(loadAtom(pc), '"'); case JSOP_SYMBOL: { unsigned i = uint8_t(pc[1]); MOZ_ASSERT(i < JS::WellKnownSymbolLimit); if (i < JS::WellKnownSymbolLimit) return write(cx->names().wellKnownSymbolDescriptions()[i]); break; } case JSOP_UNDEFINED: return write(js_undefined_str); case JSOP_THIS: // |this| could convert to a very long object initialiser, so cite it by // its keyword name. return write(js_this_str); case JSOP_CALL: case JSOP_FUNCALL: return decompilePCForStackOperand(pc, -int32_t(GET_ARGC(pc) + 2)) && write("(...)"); case JSOP_SPREADCALL: return decompilePCForStackOperand(pc, -int32_t(3)) && write("(...)"); case JSOP_NEWARRAY: return write("[]"); case JSOP_REGEXP: { RootedObject obj(cx, script->getRegExp(GET_UINT32_INDEX(pc))); JSString* str = obj->as().toString(cx); if (!str) return false; return write(str); } case JSOP_NEWARRAY_COPYONWRITE: { RootedObject obj(cx, script->getObject(GET_UINT32_INDEX(pc))); Handle aobj = obj.as(); if (!write("[")) return false; for (size_t i = 0; i < aobj->getDenseInitializedLength(); i++) { if (i > 0 && !write(", ")) return false; RootedValue v(cx, aobj->getDenseElement(i)); MOZ_RELEASE_ASSERT(v.isPrimitive() && !v.isMagic()); JSString* str = ValueToSource(cx, v); if (!str || !write(str)) return false; } return write("]"); } case JSOP_OBJECT: { JSObject* obj = script->getObject(GET_UINT32_INDEX(pc)); RootedValue objv(cx, ObjectValue(*obj)); JSString* str = ValueToSource(cx, objv); if (!str) return false; return write(str); } default: break; } return write("(intermediate value)"); } bool ExpressionDecompiler::init() { assertSameCompartment(cx, script); if (!sprinter.init()) return false; if (!parser.parse()) return false; return true; } bool ExpressionDecompiler::write(const char* s) { return sprinter.put(s) >= 0; } bool ExpressionDecompiler::write(JSString* str) { return sprinter.putString(str) >= 0; } bool ExpressionDecompiler::quote(JSString* s, uint32_t quote) { return QuoteString(&sprinter, s, quote) != nullptr; } JSAtom* ExpressionDecompiler::loadAtom(jsbytecode* pc) { return script->getAtom(GET_UINT32_INDEX(pc)); } JSAtom* ExpressionDecompiler::getArg(unsigned slot) { MOZ_ASSERT(fun); MOZ_ASSERT(slot < script->bindings.numArgs()); for (BindingIter bi(script); bi; bi++) { MOZ_ASSERT(bi->kind() == Binding::ARGUMENT); if (bi.argIndex() == slot) return bi->name(); } MOZ_CRASH("No binding"); } JSAtom* ExpressionDecompiler::getLocal(uint32_t local, jsbytecode* pc) { MOZ_ASSERT(local < script->nfixed()); if (local < script->nbodyfixed()) { for (BindingIter bi(script); bi; bi++) { if (bi->kind() != Binding::ARGUMENT && !bi->aliased() && bi.frameIndex() == local) return bi->name(); } MOZ_CRASH("No binding"); } for (NestedScopeObject* chain = script->getStaticBlockScope(pc); chain; chain = chain->enclosingNestedScope()) { if (!chain->is()) continue; StaticBlockObject& block = chain->as(); if (local < block.localOffset()) continue; local -= block.localOffset(); if (local >= block.numVariables()) return nullptr; for (Shape::Range r(block.lastProperty()); !r.empty(); r.popFront()) { const Shape& shape = r.front(); if (block.shapeToIndex(shape) == local) return JSID_TO_ATOM(shape.propid()); } break; } return nullptr; } bool ExpressionDecompiler::getOutput(char** res) { ptrdiff_t len = sprinter.stringEnd() - sprinter.stringAt(0); *res = cx->pod_malloc(len + 1); if (!*res) return false; js_memcpy(*res, sprinter.stringAt(0), len); (*res)[len] = 0; return true; } } // anonymous namespace static bool FindStartPC(JSContext* cx, const FrameIter& iter, int spindex, int skipStackHits, Value v, jsbytecode** valuepc) { jsbytecode* current = *valuepc; if (spindex == JSDVG_IGNORE_STACK) return true; /* * FIXME: Fall back if iter.isIon(), since the stack snapshot may be for the * previous pc (see bug 831120). */ if (iter.isIon()) return true; *valuepc = nullptr; BytecodeParser parser(cx, iter.script()); if (!parser.parse()) return false; if (spindex < 0 && spindex + int(parser.stackDepthAtPC(current)) < 0) spindex = JSDVG_SEARCH_STACK; if (spindex == JSDVG_SEARCH_STACK) { size_t index = iter.numFrameSlots(); MOZ_ASSERT(index >= size_t(parser.stackDepthAtPC(current))); // We search from fp->sp to base to find the most recently calculated // value matching v under assumption that it is the value that caused // the exception. int stackHits = 0; Value s; do { if (!index) return true; s = iter.frameSlotValue(--index); } while (s != v || stackHits++ != skipStackHits); // If the current PC has fewer values on the stack than the index we are // looking for, the blamed value must be one pushed by the current // bytecode, so restore *valuepc. jsbytecode* pc = nullptr; if (index < size_t(parser.stackDepthAtPC(current))) pc = parser.pcForStackOperand(current, index); *valuepc = pc ? pc : current; } else { jsbytecode* pc = parser.pcForStackOperand(current, spindex); *valuepc = pc ? pc : current; } return true; } static bool DecompileExpressionFromStack(JSContext* cx, int spindex, int skipStackHits, HandleValue v, char** res) { MOZ_ASSERT(spindex < 0 || spindex == JSDVG_IGNORE_STACK || spindex == JSDVG_SEARCH_STACK); *res = nullptr; #ifdef JS_MORE_DETERMINISTIC /* * Give up if we need deterministic behavior for differential testing. * IonMonkey doesn't use InterpreterFrames and this ensures we get the same * error messages. */ return true; #endif FrameIter frameIter(cx); if (frameIter.done() || !frameIter.hasScript()) return true; RootedScript script(cx, frameIter.script()); AutoCompartment ac(cx, &script->global()); jsbytecode* valuepc = frameIter.pc(); RootedFunction fun(cx, frameIter.isFunctionFrame() ? frameIter.calleeTemplate() : nullptr); MOZ_ASSERT(script->containsPC(valuepc)); // Give up if in prologue. if (valuepc < script->main()) return true; if (!FindStartPC(cx, frameIter, spindex, skipStackHits, v, &valuepc)) return false; if (!valuepc) return true; ExpressionDecompiler ed(cx, script, fun); if (!ed.init()) return false; if (!ed.decompilePC(valuepc)) return false; return ed.getOutput(res); } char* js::DecompileValueGenerator(JSContext* cx, int spindex, HandleValue v, HandleString fallbackArg, int skipStackHits) { RootedString fallback(cx, fallbackArg); { char* result; if (!DecompileExpressionFromStack(cx, spindex, skipStackHits, v, &result)) return nullptr; if (result) { if (strcmp(result, "(intermediate value)")) return result; js_free(result); } } if (!fallback) { if (v.isUndefined()) return JS_strdup(cx, js_undefined_str); // Prevent users from seeing "(void 0)" fallback = ValueToSource(cx, v); if (!fallback) return nullptr; } return JS_EncodeString(cx, fallback); } static bool DecompileArgumentFromStack(JSContext* cx, int formalIndex, char** res) { MOZ_ASSERT(formalIndex >= 0); *res = nullptr; #ifdef JS_MORE_DETERMINISTIC /* See note in DecompileExpressionFromStack. */ return true; #endif /* * Settle on the nearest script frame, which should be the builtin that * called the intrinsic. */ FrameIter frameIter(cx); MOZ_ASSERT(!frameIter.done()); /* * Get the second-to-top frame, the caller of the builtin that called the * intrinsic. */ ++frameIter; if (frameIter.done() || !frameIter.hasScript()) return true; RootedScript script(cx, frameIter.script()); AutoCompartment ac(cx, &script->global()); jsbytecode* current = frameIter.pc(); RootedFunction fun(cx, frameIter.isFunctionFrame() ? frameIter.calleeTemplate() : nullptr); MOZ_ASSERT(script->containsPC(current)); if (current < script->main()) return true; /* Don't handle getters, setters or calls from fun.call/fun.apply. */ if (JSOp(*current) != JSOP_CALL || static_cast(formalIndex) >= GET_ARGC(current)) return true; BytecodeParser parser(cx, script); if (!parser.parse()) return false; int formalStackIndex = parser.stackDepthAtPC(current) - GET_ARGC(current) + formalIndex; MOZ_ASSERT(formalStackIndex >= 0); if (uint32_t(formalStackIndex) >= parser.stackDepthAtPC(current)) return true; ExpressionDecompiler ed(cx, script, fun); if (!ed.init()) return false; if (!ed.decompilePCForStackOperand(current, formalStackIndex)) return false; return ed.getOutput(res); } char* js::DecompileArgument(JSContext* cx, int formalIndex, HandleValue v) { { char* result; if (!DecompileArgumentFromStack(cx, formalIndex, &result)) return nullptr; if (result) { if (strcmp(result, "(intermediate value)")) return result; js_free(result); } } if (v.isUndefined()) return JS_strdup(cx, js_undefined_str); // Prevent users from seeing "(void 0)" RootedString fallback(cx, ValueToSource(cx, v)); if (!fallback) return nullptr; return JS_EncodeString(cx, fallback); } bool js::CallResultEscapes(jsbytecode* pc) { /* * If we see any of these sequences, the result is unused: * - call / pop * * If we see any of these sequences, the result is only tested for nullness: * - call / ifeq * - call / not / ifeq */ if (*pc == JSOP_CALL) pc += JSOP_CALL_LENGTH; else if (*pc == JSOP_SPREADCALL) pc += JSOP_SPREADCALL_LENGTH; else return true; if (*pc == JSOP_POP) return false; if (*pc == JSOP_NOT) pc += JSOP_NOT_LENGTH; return *pc != JSOP_IFEQ; } extern bool js::IsValidBytecodeOffset(JSContext* cx, JSScript* script, size_t offset) { // This could be faster (by following jump instructions if the target is <= offset). for (BytecodeRange r(cx, script); !r.empty(); r.popFront()) { size_t here = r.frontOffset(); if (here >= offset) return here == offset; } return false; } /* * There are three possible PCCount profiling states: * * 1. None: Neither scripts nor the runtime have count information. * 2. Profile: Active scripts have count information, the runtime does not. * 3. Query: Scripts do not have count information, the runtime does. * * When starting to profile scripts, counting begins immediately, with all JIT * code discarded and recompiled with counts as necessary. Active interpreter * frames will not begin profiling until they begin executing another script * (via a call or return). * * The below API functions manage transitions to new states, according * to the table below. * * Old State * ------------------------- * Function None Profile Query * -------- * StartPCCountProfiling Profile Profile Profile * StopPCCountProfiling None Query Query * PurgePCCounts None None None */ static void ReleaseScriptCounts(FreeOp* fop) { JSRuntime* rt = fop->runtime(); MOZ_ASSERT(rt->scriptAndCountsVector); ScriptAndCountsVector& vec = *rt->scriptAndCountsVector; for (size_t i = 0; i < vec.length(); i++) vec[i].scriptCounts.destroy(fop); fop->delete_(rt->scriptAndCountsVector); rt->scriptAndCountsVector = nullptr; } JS_FRIEND_API(void) js::StartPCCountProfiling(JSContext* cx) { JSRuntime* rt = cx->runtime(); if (rt->profilingScripts) return; if (rt->scriptAndCountsVector) ReleaseScriptCounts(rt->defaultFreeOp()); ReleaseAllJITCode(rt->defaultFreeOp()); rt->profilingScripts = true; } JS_FRIEND_API(void) js::StopPCCountProfiling(JSContext* cx) { JSRuntime* rt = cx->runtime(); if (!rt->profilingScripts) return; MOZ_ASSERT(!rt->scriptAndCountsVector); ReleaseAllJITCode(rt->defaultFreeOp()); ScriptAndCountsVector* vec = cx->new_(SystemAllocPolicy()); if (!vec) return; for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) { for (ZoneCellIter i(zone, FINALIZE_SCRIPT); !i.done(); i.next()) { JSScript* script = i.get(); if (script->hasScriptCounts() && script->types()) { ScriptAndCounts sac; sac.script = script; sac.scriptCounts.set(script->releaseScriptCounts()); if (!vec->append(sac)) sac.scriptCounts.destroy(rt->defaultFreeOp()); } } } rt->profilingScripts = false; rt->scriptAndCountsVector = vec; } JS_FRIEND_API(void) js::PurgePCCounts(JSContext* cx) { JSRuntime* rt = cx->runtime(); if (!rt->scriptAndCountsVector) return; MOZ_ASSERT(!rt->profilingScripts); ReleaseScriptCounts(rt->defaultFreeOp()); } JS_FRIEND_API(size_t) js::GetPCCountScriptCount(JSContext* cx) { JSRuntime* rt = cx->runtime(); if (!rt->scriptAndCountsVector) return 0; return rt->scriptAndCountsVector->length(); } enum MaybeComma {NO_COMMA, COMMA}; static void AppendJSONProperty(StringBuffer& buf, const char* name, MaybeComma comma = COMMA) { if (comma) buf.append(','); buf.append('\"'); buf.append(name, strlen(name)); buf.append("\":", 2); } static void AppendArrayJSONProperties(JSContext* cx, StringBuffer& buf, double* values, const char * const* names, unsigned count, MaybeComma& comma) { for (unsigned i = 0; i < count; i++) { if (values[i]) { AppendJSONProperty(buf, names[i], comma); comma = COMMA; NumberValueToStringBuffer(cx, DoubleValue(values[i]), buf); } } } JS_FRIEND_API(JSString*) js::GetPCCountScriptSummary(JSContext* cx, size_t index) { JSRuntime* rt = cx->runtime(); if (!rt->scriptAndCountsVector || index >= rt->scriptAndCountsVector->length()) { JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_BUFFER_TOO_SMALL); return nullptr; } const ScriptAndCounts& sac = (*rt->scriptAndCountsVector)[index]; RootedScript script(cx, sac.script); /* * OOM on buffer appends here will not be caught immediately, but since * StringBuffer uses a TempAllocPolicy will trigger an exception on the * context if they occur, which we'll catch before returning. */ StringBuffer buf(cx); buf.append('{'); AppendJSONProperty(buf, "file", NO_COMMA); JSString* str = JS_NewStringCopyZ(cx, script->filename()); if (!str || !(str = StringToSource(cx, str))) return nullptr; buf.append(str); AppendJSONProperty(buf, "line"); NumberValueToStringBuffer(cx, Int32Value(script->lineno()), buf); if (script->functionNonDelazifying()) { JSAtom* atom = script->functionNonDelazifying()->displayAtom(); if (atom) { AppendJSONProperty(buf, "name"); if (!(str = StringToSource(cx, atom))) return nullptr; buf.append(str); } } double baseTotals[PCCounts::BASE_LIMIT] = {0.0}; double accessTotals[PCCounts::ACCESS_LIMIT - PCCounts::BASE_LIMIT] = {0.0}; double elementTotals[PCCounts::ELEM_LIMIT - PCCounts::ACCESS_LIMIT] = {0.0}; double propertyTotals[PCCounts::PROP_LIMIT - PCCounts::ACCESS_LIMIT] = {0.0}; double arithTotals[PCCounts::ARITH_LIMIT - PCCounts::BASE_LIMIT] = {0.0}; for (unsigned i = 0; i < script->length(); i++) { PCCounts& counts = sac.getPCCounts(script->offsetToPC(i)); if (!counts) continue; JSOp op = (JSOp)script->code()[i]; unsigned numCounts = PCCounts::numCounts(op); for (unsigned j = 0; j < numCounts; j++) { double value = counts.get(j); if (j < PCCounts::BASE_LIMIT) { baseTotals[j] += value; } else if (PCCounts::accessOp(op)) { if (j < PCCounts::ACCESS_LIMIT) accessTotals[j - PCCounts::BASE_LIMIT] += value; else if (PCCounts::elementOp(op)) elementTotals[j - PCCounts::ACCESS_LIMIT] += value; else if (PCCounts::propertyOp(op)) propertyTotals[j - PCCounts::ACCESS_LIMIT] += value; else MOZ_CRASH("Bad opcode"); } else if (PCCounts::arithOp(op)) { arithTotals[j - PCCounts::BASE_LIMIT] += value; } else { MOZ_CRASH("Bad opcode"); } } } AppendJSONProperty(buf, "totals"); buf.append('{'); MaybeComma comma = NO_COMMA; AppendArrayJSONProperties(cx, buf, baseTotals, countBaseNames, JS_ARRAY_LENGTH(baseTotals), comma); AppendArrayJSONProperties(cx, buf, accessTotals, countAccessNames, JS_ARRAY_LENGTH(accessTotals), comma); AppendArrayJSONProperties(cx, buf, elementTotals, countElementNames, JS_ARRAY_LENGTH(elementTotals), comma); AppendArrayJSONProperties(cx, buf, propertyTotals, countPropertyNames, JS_ARRAY_LENGTH(propertyTotals), comma); AppendArrayJSONProperties(cx, buf, arithTotals, countArithNames, JS_ARRAY_LENGTH(arithTotals), comma); uint64_t ionActivity = 0; jit::IonScriptCounts* ionCounts = sac.getIonCounts(); while (ionCounts) { for (size_t i = 0; i < ionCounts->numBlocks(); i++) ionActivity += ionCounts->block(i).hitCount(); ionCounts = ionCounts->previous(); } if (ionActivity) { AppendJSONProperty(buf, "ion", comma); NumberValueToStringBuffer(cx, DoubleValue(ionActivity), buf); } buf.append('}'); buf.append('}'); if (cx->isExceptionPending()) return nullptr; return buf.finishString(); } static bool GetPCCountJSON(JSContext* cx, const ScriptAndCounts& sac, StringBuffer& buf) { RootedScript script(cx, sac.script); buf.append('{'); AppendJSONProperty(buf, "text", NO_COMMA); JSString* str = JS_DecompileScript(cx, script, nullptr, 0); if (!str || !(str = StringToSource(cx, str))) return false; buf.append(str); AppendJSONProperty(buf, "line"); NumberValueToStringBuffer(cx, Int32Value(script->lineno()), buf); AppendJSONProperty(buf, "opcodes"); buf.append('['); bool comma = false; SrcNoteLineScanner scanner(script->notes(), script->lineno()); for (jsbytecode* pc = script->code(); pc < script->codeEnd(); pc += GetBytecodeLength(pc)) { size_t offset = script->pcToOffset(pc); JSOp op = (JSOp) *pc; if (comma) buf.append(','); comma = true; buf.append('{'); AppendJSONProperty(buf, "id", NO_COMMA); NumberValueToStringBuffer(cx, Int32Value(offset), buf); scanner.advanceTo(offset); AppendJSONProperty(buf, "line"); NumberValueToStringBuffer(cx, Int32Value(scanner.getLine()), buf); { const char* name = js_CodeName[op]; AppendJSONProperty(buf, "name"); buf.append('\"'); buf.append(name, strlen(name)); buf.append('\"'); } { ExpressionDecompiler ed(cx, script, script->functionDelazifying()); if (!ed.init()) return false; if (!ed.decompilePC(pc)) return false; char* text; if (!ed.getOutput(&text)) return false; AppendJSONProperty(buf, "text"); JSString* str = JS_NewStringCopyZ(cx, text); js_free(text); if (!str || !(str = StringToSource(cx, str))) return false; buf.append(str); } PCCounts& counts = sac.getPCCounts(pc); unsigned numCounts = PCCounts::numCounts(op); AppendJSONProperty(buf, "counts"); buf.append('{'); MaybeComma comma = NO_COMMA; for (unsigned i = 0; i < numCounts; i++) { double value = counts.get(i); if (value > 0) { AppendJSONProperty(buf, PCCounts::countName(op, i), comma); comma = COMMA; NumberValueToStringBuffer(cx, DoubleValue(value), buf); } } buf.append('}'); buf.append('}'); } buf.append(']'); jit::IonScriptCounts* ionCounts = sac.getIonCounts(); if (ionCounts) { AppendJSONProperty(buf, "ion"); buf.append('['); bool comma = false; while (ionCounts) { if (comma) buf.append(','); comma = true; buf.append('['); for (size_t i = 0; i < ionCounts->numBlocks(); i++) { if (i) buf.append(','); const jit::IonBlockCounts& block = ionCounts->block(i); buf.append('{'); AppendJSONProperty(buf, "id", NO_COMMA); NumberValueToStringBuffer(cx, Int32Value(block.id()), buf); AppendJSONProperty(buf, "offset"); NumberValueToStringBuffer(cx, Int32Value(block.offset()), buf); AppendJSONProperty(buf, "successors"); buf.append('['); for (size_t j = 0; j < block.numSuccessors(); j++) { if (j) buf.append(','); NumberValueToStringBuffer(cx, Int32Value(block.successor(j)), buf); } buf.append(']'); AppendJSONProperty(buf, "hits"); NumberValueToStringBuffer(cx, DoubleValue(block.hitCount()), buf); AppendJSONProperty(buf, "code"); JSString* str = JS_NewStringCopyZ(cx, block.code()); if (!str || !(str = StringToSource(cx, str))) return false; buf.append(str); buf.append('}'); } buf.append(']'); ionCounts = ionCounts->previous(); } buf.append(']'); } buf.append('}'); return !cx->isExceptionPending(); } JS_FRIEND_API(JSString*) js::GetPCCountScriptContents(JSContext* cx, size_t index) { JSRuntime* rt = cx->runtime(); if (!rt->scriptAndCountsVector || index >= rt->scriptAndCountsVector->length()) { JS_ReportErrorNumber(cx, GetErrorMessage, nullptr, JSMSG_BUFFER_TOO_SMALL); return nullptr; } const ScriptAndCounts& sac = (*rt->scriptAndCountsVector)[index]; JSScript* script = sac.script; StringBuffer buf(cx); { AutoCompartment ac(cx, &script->global()); if (!GetPCCountJSON(cx, sac, buf)) return nullptr; } return buf.finishString(); }