/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- * 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" #define __STDC_FORMAT_MACROS #include "mozilla/Attributes.h" #include "mozilla/SizePrintfMacros.h" #include "mozilla/Sprintf.h" #include #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 "frontend/BytecodeCompiler.h" #include "frontend/SourceNotes.h" #include "gc/GCInternals.h" #include "js/CharacterEncoding.h" #include "vm/CodeCoverage.h" #include "vm/EnvironmentObject.h" #include "vm/Opcodes.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(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 }; /************************************************************************/ static bool DecompileArgumentFromStack(JSContext* cx, int formalIndex, char** res); size_t js::GetVariableBytecodeLength(jsbytecode* pc) { JSOp op = JSOp(*pc); MOZ_ASSERT(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 = CodeSpec[op]; if (cs.nuses >= 0) return cs.nuses; MOZ_ASSERT(CodeSpec[op].nuses == -1); switch (op) { case JSOP_POPN: return GET_UINT16(pc); case JSOP_NEW: case JSOP_SUPERCALL: return 2 + GET_ARGC(pc) + 1; default: /* stack: fun, this, [argc arguments] */ MOZ_ASSERT(op == JSOP_CALL || op == JSOP_CALL_IGNORES_RV || op == JSOP_EVAL || op == JSOP_CALLITER || 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 = CodeSpec[op]; MOZ_ASSERT(cs.ndefs >= 0); return cs.ndefs; } const char * PCCounts::numExecName = "interp"; [[nodiscard]] static bool DumpIonScriptCounts(Sprinter* sp, HandleScript script, jit::IonScriptCounts* ionCounts) { if (!sp->jsprintf("IonScript [%" PRIuSIZE " blocks]:\n", ionCounts->numBlocks())) return false; for (size_t i = 0; i < ionCounts->numBlocks(); i++) { const jit::IonBlockCounts& block = ionCounts->block(i); unsigned lineNumber = 0, columnNumber = 0; lineNumber = PCToLineNumber(script, script->offsetToPC(block.offset()), &columnNumber); if (!sp->jsprintf("BB #%" PRIu32 " [%05u,%u,%u]", block.id(), block.offset(), lineNumber, columnNumber)) { return false; } if (block.description()) { if (!sp->jsprintf(" [inlined %s]", block.description())) return false; } for (size_t j = 0; j < block.numSuccessors(); j++) { if (!sp->jsprintf(" -> #%" PRIu32, block.successor(j))) return false; } if (!sp->jsprintf(" :: %" PRIu64 " hits\n", block.hitCount())) return false; if (!sp->jsprintf("%s\n", block.code())) return false; } return true; } [[nodiscard]] static bool DumpPCCounts(JSContext* cx, HandleScript script, Sprinter* sp) { MOZ_ASSERT(script->hasScriptCounts()); #ifdef DEBUG jsbytecode* pc = script->code(); while (pc < script->codeEnd()) { jsbytecode* next = GetNextPc(pc); if (!Disassemble1(cx, script, pc, script->pcToOffset(pc), true, sp)) return false; if (sp->put(" {") < 0) return false; PCCounts* counts = script->maybeGetPCCounts(pc); if (double val = counts ? counts->numExec() : 0.0) { if (!sp->jsprintf("\"%s\": %.0f", PCCounts::numExecName, val)) return false; } if (sp->put("}\n") < 0) return false; pc = next; } #endif jit::IonScriptCounts* ionCounts = script->getIonCounts(); while (ionCounts) { if (!DumpIonScriptCounts(sp, script, ionCounts)) return false; ionCounts = ionCounts->previous(); } return true; } bool js::DumpCompartmentPCCounts(JSContext* cx) { Rooted> scripts(cx, GCVector(cx)); for (auto iter = cx->zone()->cellIter(); !iter.done(); iter.next()) { JSScript* script = iter; if (script->compartment() != cx->compartment()) continue; if (script->hasScriptCounts()) { if (!scripts.append(script)) return false; } } for (uint32_t i = 0; i < scripts.length(); i++) { HandleScript script = scripts[i]; Sprinter sprinter(cx); if (!sprinter.init()) return false; fprintf(stdout, "--- SCRIPT %s:%" PRIuSIZE " ---\n", script->filename(), script->lineno()); if (!DumpPCCounts(cx, script, &sprinter)) return false; fputs(sprinter.string(), stdout); fprintf(stdout, "--- END SCRIPT %s:%" PRIuSIZE " ---\n", script->filename(), script->lineno()); } return true; } ///////////////////////////////////////////////////////////////////// // 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 (!offsetStack) return false; if (stackDepth) { 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 UnknownOffset 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 (stack[n] == SpecialOffsets::IgnoreOffset) continue; if (offsetStack[n] == SpecialOffsets::IgnoreOffset) offsetStack[n] = stack[n]; if (offsetStack[n] != stack[n]) offsetStack[n] = SpecialOffsets::UnknownOffset; } } }; JSContext* cx_; LifoAllocScope allocScope_; RootedScript script_; Bytecode** codeArray_; // Use a struct instead of an enum class to avoid casting the enumerated // value. struct SpecialOffsets { static const uint32_t UnknownOffset = UINT32_MAX; static const uint32_t IgnoreOffset = UINT32_MAX - 1; static const uint32_t FirstSpecialOffset = IgnoreOffset; }; 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 >= SpecialOffsets::FirstSpecialOffset) return nullptr; return script_->offsetToPC(offset); } 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 recordBytecode(uint32_t offset, const 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_NOP_DESTRUCTURING: // Poison the last offset to not obfuscate the error message. offsetStack[stackDepth - 1] = SpecialOffsets::IgnoreOffset; 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; case JSOP_PICK: { jsbytecode* pc = script_->offsetToPC(offset); unsigned n = GET_UINT8(pc); MOZ_ASSERT(ndefs == n + 1); if (offsetStack) { uint32_t top = stackDepth + n; uint32_t tmp = offsetStack[stackDepth]; for (uint32_t i = stackDepth; i < top; i++) offsetStack[i] = offsetStack[i + 1]; offsetStack[top] = tmp; } break; } case JSOP_UNPICK: { jsbytecode* pc = script_->offsetToPC(offset); unsigned n = GET_UINT8(pc); MOZ_ASSERT(ndefs == n + 1); if (offsetStack) { uint32_t top = stackDepth + n; uint32_t tmp = offsetStack[top]; for (uint32_t i = top; i > stackDepth; i--) offsetStack[i] = offsetStack[i - 1]; offsetStack[stackDepth] = tmp; } break; } } stackDepth += ndefs; return stackDepth; } bool BytecodeParser::recordBytecode(uint32_t offset, const uint32_t* offsetStack, uint32_t stackDepth) { MOZ_ASSERT(offset < script_->length()); Bytecode*& code = codeArray_[offset]; if (!code) { code = alloc().new_(); if (!code || !code->captureOffsetStack(alloc(), offsetStack, stackDepth)) { reportOOM(); return false; } } else { code->mergeOffsetStack(offsetStack, stackDepth); } return true; } bool BytecodeParser::addJump(uint32_t offset, uint32_t* currentOffset, uint32_t stackDepth, const uint32_t* offsetStack) { if (!recordBytecode(offset, offsetStack, stackDepth)) return false; Bytecode*& code = codeArray_[offset]; 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; } // On a jump target, we reload the offsetStack saved for the current // bytecode, as it contains either the original offset stack, or the // merged offset stack. if (BytecodeIsJumpTarget(op)) { for (uint32_t n = 0; n < code->stackDepth; ++n) offsetStack[n] = code->offsetStack[n]; } 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)) { if (!recordBytecode(successorOffset, offsetStack, stackDepth)) return false; } } 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. */ [[nodiscard]] static bool DisassembleAtPC(JSContext* cx, JSScript* scriptArg, bool lines, jsbytecode* pc, bool showAll, Sprinter* sp) { RootedScript script(cx, scriptArg); BytecodeParser parser(cx, script); if (showAll) { if (!parser.parse()) return false; if (!sp->jsprintf("%s:%u\n", script->filename(), unsigned(script->lineno()))) return false; } if (pc != nullptr) { if (sp->put(" ") < 0) return false; } if (showAll) { if (sp->put("sn stack ") < 0) return false; } if (sp->put("loc ") < 0) return false; if (lines) { if (sp->put("line") < 0) return false; } if (sp->put(" op\n") < 0) return false; if (pc != nullptr) { if (sp->put(" ") < 0) return false; } if (showAll) { if (sp->put("-- ----- ") < 0) return false; } if (sp->put("----- ") < 0) return false; if (lines) { if (sp->put("----") < 0) return false; } if (sp->put(" --\n") < 0) return false; jsbytecode* next = script->code(); jsbytecode* end = script->codeEnd(); while (next < end) { if (next == script->main()) { if (sp->put("main:\n") < 0) return false; } if (pc != nullptr) { if (sp->put(pc == next ? "--> " : " ") < 0) return false; } 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) { if (!sp->jsprintf("%02u\n ", SN_TYPE(sn))) return false; sn = next; next = SN_NEXT(sn); } if (!sp->jsprintf("%02u ", SN_TYPE(sn))) return false; } else { if (sp->put(" ") < 0) return false; } if (parser.isReachable(next)) { if (!sp->jsprintf("%05u ", parser.stackDepthAtPC(next))) return false; } else { if (sp->put(" ") < 0) return false; } } unsigned 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); } JS_FRIEND_API(bool) js::DumpPC(JSContext* cx, FILE* fp) { gc::AutoSuppressGC suppressGC(cx); Sprinter sprinter(cx); if (!sprinter.init()) return false; ScriptFrameIter iter(cx); if (iter.done()) { fprintf(fp, "Empty stack.\n"); return true; } RootedScript script(cx, iter.script()); bool ok = DisassembleAtPC(cx, script, true, iter.pc(), false, &sprinter); fprintf(fp, "%s", sprinter.string()); return ok; } JS_FRIEND_API(bool) js::DumpScript(JSContext* cx, JSScript* scriptArg, FILE* fp) { gc::AutoSuppressGC suppressGC(cx); Sprinter sprinter(cx); if (!sprinter.init()) return false; RootedScript script(cx, scriptArg); bool ok = Disassemble(cx, script, true, &sprinter); fprintf(fp, "%s", sprinter.string()); return ok; } 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) { ReportOutOfMemory(cx); 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) { ReportOutOfMemory(cx); return false; } bytes->initBytes(source); return true; } if (v.isObject()) { JSObject& obj = v.toObject(); if (obj.is()) { RootedFunction fun(cx, &obj.as()); JSString* str = JS_DecompileFunction(cx, fun); 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); } static bool ToDisassemblySource(JSContext* cx, HandleScope scope, JSAutoByteString* bytes) { char* source = JS_sprintf_append(nullptr, "%s {", ScopeKindString(scope->kind())); if (!source) { ReportOutOfMemory(cx); return false; } for (Rooted bi(cx, BindingIter(scope)); bi; bi++) { JSAutoByteString nameBytes; if (!AtomToPrintableString(cx, bi.name(), &nameBytes)) return false; source = JS_sprintf_append(source, "%s: ", nameBytes.ptr()); if (!source) { ReportOutOfMemory(cx); return false; } BindingLocation loc = bi.location(); switch (loc.kind()) { case BindingLocation::Kind::Global: source = JS_sprintf_append(source, "global"); break; case BindingLocation::Kind::Frame: source = JS_sprintf_append(source, "frame slot %u", loc.slot()); break; case BindingLocation::Kind::Environment: source = JS_sprintf_append(source, "env slot %u", loc.slot()); break; case BindingLocation::Kind::Argument: source = JS_sprintf_append(source, "arg slot %u", loc.slot()); break; case BindingLocation::Kind::NamedLambdaCallee: source = JS_sprintf_append(source, "named lambda callee"); break; case BindingLocation::Kind::Import: source = JS_sprintf_append(source, "import"); break; } if (!source) { ReportOutOfMemory(cx); return false; } if (!bi.isLast()) { source = JS_sprintf_append(source, ", "); if (!source) { ReportOutOfMemory(cx); return false; } } } source = JS_sprintf_append(source, "}"); if (!source) { ReportOutOfMemory(cx); return false; } bytes->initBytes(source); return 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]; SprintfLiteral(numBuf1, "%d", op); SprintfLiteral(numBuf2, "%d", JSOP_LIMIT); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_BYTECODE_TOO_BIG, numBuf1, numBuf2); return 0; } const JSCodeSpec* cs = &CodeSpec[op]; ptrdiff_t len = (ptrdiff_t) cs->length; if (!sp->jsprintf("%05u:", loc)) return 0; if (lines) { if (!sp->jsprintf("%4u", PCToLineNumber(script, pc))) return 0; } if (!sp->jsprintf(" %s", CodeName[op])) return 0; int i; 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) { if (!sp->jsprintf(" %u (%+d)", unsigned(loc + note.length + 1), int(note.length + 1))) { return 0; } break; } } } break; case JOF_JUMP: { ptrdiff_t off = GET_JUMP_OFFSET(pc); if (!sp->jsprintf(" %u (%+d)", unsigned(loc + int(off)), int(off))) return 0; break; } case JOF_SCOPE: { RootedScope scope(cx, script->getScope(GET_UINT32_INDEX(pc))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, scope, &bytes)) return 0; if (!sp->jsprintf(" %s", bytes.ptr())) return 0; break; } case JOF_ENVCOORD: { RootedValue v(cx, StringValue(EnvironmentCoordinateName(cx->caches.envCoordinateNameCache, script, pc))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, v, &bytes)) return 0; EnvironmentCoordinate ec(pc); if (!sp->jsprintf(" %s (hops = %u, slot = %u)", bytes.ptr(), ec.hops(), ec.slot())) return 0; break; } case JOF_ATOM: { RootedValue v(cx, StringValue(script->getAtom(GET_UINT32_INDEX(pc)))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, v, &bytes)) return 0; if (!sp->jsprintf(" %s", bytes.ptr())) return 0; break; } case JOF_BIGINT: // Fallthrough. case JOF_DOUBLE: { RootedValue v(cx, script->getConst(GET_UINT32_INDEX(pc))); JSAutoByteString bytes; if (!ToDisassemblySource(cx, v, &bytes)) return 0; if (!sp->jsprintf(" %s", bytes.ptr())) return 0; break; } case JOF_OBJECT: { /* Don't call obj.toSource if analysis/inference is active. */ if (script->zone()->types.activeAnalysis) { if (!sp->jsprintf(" object")) return 0; break; } JSObject* obj = script->getObject(GET_UINT32_INDEX(pc)); { JSAutoByteString bytes; RootedValue v(cx, ObjectValue(*obj)); if (!ToDisassemblySource(cx, v, &bytes)) return 0; if (!sp->jsprintf(" %s", bytes.ptr())) return 0; } break; } case JOF_REGEXP: { js::RegExpObject* obj = script->getRegExp(pc); JSAutoByteString bytes; RootedValue v(cx, ObjectValue(*obj)); if (!ToDisassemblySource(cx, v, &bytes)) return 0; if (!sp->jsprintf(" %s", bytes.ptr())) return 0; 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; if (!sp->jsprintf(" defaultOffset %d low %d high %d", int(off), low, high)) return 0; for (i = low; i <= high; i++) { off = GET_JUMP_OFFSET(pc2); if (!sp->jsprintf("\n\t%d: %d", i, int(off))) return 0; pc2 += JUMP_OFFSET_LEN; } len = 1 + pc2 - pc; break; } case JOF_QARG: if (!sp->jsprintf(" %u", GET_ARGNO(pc))) return 0; break; case JOF_LOCAL: if (!sp->jsprintf(" %u", GET_LOCALNO(pc))) return 0; break; case JOF_UINT32: if (!sp->jsprintf(" %u", GET_UINT32(pc))) return 0; break; 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: if (!sp->jsprintf(" %d", i)) return 0; break; default: { char numBuf[12]; SprintfLiteral(numBuf, "%x", cs->format); JS_ReportErrorNumberASCII(cx, GetErrorMessage, nullptr, JSMSG_UNKNOWN_FORMAT, numBuf); return 0; } } sp->put("\n"); return len; } #endif /* DEBUG */ 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; BytecodeParser parser; Sprinter sprinter; ExpressionDecompiler(JSContext* cx, JSScript* script) : cx(cx), script(cx, script), parser(cx, script), sprinter(cx) {} bool init(); bool decompilePCForStackOperand(jsbytecode* pc, int i); bool decompilePC(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 (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); // For self-hosted scripts that are called from non-self-hosted code, // decompiling the parameter name in the self-hosted script is // unhelpful. Decompile the argument name instead. if (script->selfHosted()) { char* result; if (!DecompileArgumentFromStack(cx, slot, &result)) return false; // Note that decompiling the argument in the parent frame might // not succeed. if (result) { bool ok = write(result); js_free(result); return ok; } } JSAtom* atom = getArg(slot); if (!atom) return false; return write(atom); } case JSOP_GETLOCAL: { JSAtom* atom = FrameSlotName(script, pc); MOZ_ASSERT(atom); return write(atom); } case JSOP_GETALIASEDVAR: { JSAtom* atom = EnvironmentCoordinateName(cx->caches.envCoordinateNameCache, 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_GETPROP_SUPER: { RootedAtom prop(cx, loadAtom(pc)); return write("super.") && quote(prop, '\0'); } case JSOP_SETELEM: case JSOP_STRICTSETELEM: // NOTE: We don't show the right hand side of the operation because // it's used in error messages like: "a[0] is not readable". // // We could though. return decompilePCForStackOperand(pc, -3) && write("[") && decompilePCForStackOperand(pc, -2) && write("]"); case JSOP_GETELEM: case JSOP_CALLELEM: return decompilePCForStackOperand(pc, -2) && write("[") && decompilePCForStackOperand(pc, -1) && write("]"); case JSOP_GETELEM_SUPER: return write("super[") && decompilePCForStackOperand(pc, -2) && 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_GLOBALTHIS: // |this| could convert to a very long object initialiser, so cite it by // its keyword name. return write(js_this_str); case JSOP_NEWTARGET: return write("new.target"); case JSOP_CALL: case JSOP_CALL_IGNORES_RV: case JSOP_CALLITER: case JSOP_FUNCALL: return decompilePCForStackOperand(pc, -int32_t(GET_ARGC(pc) + 2)) && write("(...)"); case JSOP_SPREADCALL: return decompilePCForStackOperand(pc, -3) && write("(...)"); case JSOP_NEWARRAY: return write("[]"); case JSOP_REGEXP: { RootedObject obj(cx, script->getObject(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); } case JSOP_CHECKISOBJ: return decompilePCForStackOperand(pc, -1); case JSOP_VOID: return write("void ") && decompilePCForStackOperand(pc, -1); 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) { if (str == cx->names().dotThis) return write("this"); 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(script->functionNonDelazifying()); MOZ_ASSERT(slot < script->numArgs()); for (PositionalFormalParameterIter fi(script); fi; fi++) { if (fi.argumentSlot() == slot) { if (!fi.isDestructured()) return fi.name(); // Destructured arguments have no single binding name. static const char destructuredParam[] = "(destructured parameter)"; return Atomize(cx, destructuredParam, strlen(destructuredParam)); } } MOZ_CRASH("No binding"); } 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, const Value& v, jsbytecode** valuepc) { jsbytecode* current = *valuepc; *valuepc = nullptr; 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; 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(); // The decompiler may be called from inside functions that are not // called from script, but via the C++ API directly, such as // Invoke. In that case, the youngest script frame may have a // completely unrelated pc and stack depth, so we give up. if (index < size_t(parser.stackDepthAtPC(current))) return true; // 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. if (index < size_t(parser.stackDepthAtPC(current))) *valuepc = parser.pcForStackOperand(current, index); else *valuepc = current; } else { *valuepc = parser.pcForStackOperand(current, spindex); } 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() || frameIter.compartment() != cx->compartment() || frameIter.inPrologue()) { return true; } RootedScript script(cx, frameIter.script()); jsbytecode* valuepc = frameIter.pc(); MOZ_ASSERT(script->containsPC(valuepc)); if (!FindStartPC(cx, frameIter, spindex, skipStackHits, v, &valuepc)) return false; if (!valuepc) return true; ExpressionDecompiler ed(cx, script); if (!ed.init()) return false; if (!ed.decompilePC(valuepc)) return false; return ed.getOutput(res); } UniqueChars 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 UniqueChars(result); js_free(result); } } if (!fallback) { if (v.isUndefined()) return UniqueChars(JS_strdup(cx, js_undefined_str)); // Prevent users from seeing "(void 0)" fallback = ValueToSource(cx, v); if (!fallback) return UniqueChars(nullptr); } return UniqueChars(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()); MOZ_ASSERT(frameIter.script()->selfHosted()); /* * Get the second-to-top frame, the non-self-hosted caller of the builtin * that called the intrinsic. */ ++frameIter; if (frameIter.done() || !frameIter.hasScript() || frameIter.script()->selfHosted() || frameIter.compartment() != cx->compartment()) { return true; } RootedScript script(cx, frameIter.script()); jsbytecode* current = frameIter.pc(); MOZ_ASSERT(script->containsPC(current)); if (current < script->main()) return true; /* Don't handle getters, setters or calls from fun.call/fun.apply. */ JSOp op = JSOp(*current); if (op != JSOP_CALL && op != JSOP_CALL_IGNORES_RV && op != JSOP_NEW) return true; if (static_cast(formalIndex) >= GET_ARGC(current)) return true; BytecodeParser parser(cx, script); if (!parser.parse()) return false; bool pushedNewTarget = op == JSOP_NEW; int formalStackIndex = parser.stackDepthAtPC(current) - GET_ARGC(current) - pushedNewTarget + formalIndex; MOZ_ASSERT(formalStackIndex >= 0); if (uint32_t(formalStackIndex) >= parser.stackDepthAtPC(current)) return true; ExpressionDecompiler ed(cx, script); 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_CALL_IGNORES_RV) pc += JSOP_CALL_IGNORES_RV_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); 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()); auto* vec = cx->new_>(cx, ScriptAndCountsVector(SystemAllocPolicy())); if (!vec) return; for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) { for (auto script = zone->cellIter(); !script.done(); script.next()) { if (script->hasScriptCounts() && script->types()) { if (!vec->append(script)) return; } } } 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}; [[nodiscard]] static bool AppendJSONProperty(StringBuffer& buf, const char* name, MaybeComma comma = COMMA) { if (comma && !buf.append(',')) return false; return buf.append('\"') && buf.append(name, strlen(name)) && buf.append("\":", 2); } JS_FRIEND_API(JSString*) js::GetPCCountScriptSummary(JSContext* cx, size_t index) { JSRuntime* rt = cx->runtime(); if (!rt->scriptAndCountsVector || index >= rt->scriptAndCountsVector->length()) { JS_ReportErrorNumberASCII(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); if (!buf.append('{')) return nullptr; if (!AppendJSONProperty(buf, "file", NO_COMMA)) return nullptr; JSString* str = JS_NewStringCopyZ(cx, script->filename()); if (!str || !(str = StringToSource(cx, str))) return nullptr; if (!buf.append(str)) return nullptr; if (!AppendJSONProperty(buf, "line")) return nullptr; if (!NumberValueToStringBuffer(cx, Int32Value(script->lineno()), buf)) { return nullptr; } if (script->functionNonDelazifying()) { JSAtom* atom = script->functionNonDelazifying()->displayAtom(); if (atom) { if (!AppendJSONProperty(buf, "name")) return nullptr; if (!(str = StringToSource(cx, atom))) return nullptr; if (!buf.append(str)) return nullptr; } } uint64_t total = 0; jsbytecode* codeEnd = script->codeEnd(); for (jsbytecode* pc = script->code(); pc < codeEnd; pc = GetNextPc(pc)) { const PCCounts* counts = sac.maybeGetPCCounts(pc); if (!counts) continue; total += counts->numExec(); } if (!AppendJSONProperty(buf, "totals")) return nullptr; if (!buf.append('{')) return nullptr; if (!AppendJSONProperty(buf, PCCounts::numExecName, NO_COMMA)) return nullptr; if (!NumberValueToStringBuffer(cx, DoubleValue(total), buf)) return nullptr; 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) { if (!AppendJSONProperty(buf, "ion", COMMA)) return nullptr; if (!NumberValueToStringBuffer(cx, DoubleValue(ionActivity), buf)) return nullptr; } if (!buf.append('}')) return nullptr; if (!buf.append('}')) return nullptr; MOZ_ASSERT(!cx->isExceptionPending()); return buf.finishString(); } static bool GetPCCountJSON(JSContext* cx, const ScriptAndCounts& sac, StringBuffer& buf) { RootedScript script(cx, sac.script); if (!buf.append('{')) return false; if (!AppendJSONProperty(buf, "text", NO_COMMA)) return false; JSString* str = JS_DecompileScript(cx, script); if (!str || !(str = StringToSource(cx, str))) return false; if (!buf.append(str)) return false; if (!AppendJSONProperty(buf, "line")) return false; if (!NumberValueToStringBuffer(cx, Int32Value(script->lineno()), buf)) return false; if (!AppendJSONProperty(buf, "opcodes")) return false; if (!buf.append('[')) return false; bool comma = false; SrcNoteLineScanner scanner(script->notes(), script->lineno()); uint64_t hits = 0; jsbytecode* end = script->codeEnd(); for (jsbytecode* pc = script->code(); pc < end; pc = GetNextPc(pc)) { size_t offset = script->pcToOffset(pc); JSOp op = JSOp(*pc); // If the current instruction is a jump target, // then update the number of hits. const PCCounts* counts = sac.maybeGetPCCounts(pc); if (counts) hits = counts->numExec(); if (comma && !buf.append(',')) return false; comma = true; if (!buf.append('{')) return false; if (!AppendJSONProperty(buf, "id", NO_COMMA)) return false; if (!NumberValueToStringBuffer(cx, Int32Value(offset), buf)) return false; scanner.advanceTo(offset); if (!AppendJSONProperty(buf, "line")) return false; if (!NumberValueToStringBuffer(cx, Int32Value(scanner.getLine()), buf)) return false; { const char* name = CodeName[op]; if (!AppendJSONProperty(buf, "name")) return false; if (!buf.append('\"')) return false; if (!buf.append(name, strlen(name))) return false; if (!buf.append('\"')) return false; } { ExpressionDecompiler ed(cx, script); if (!ed.init()) return false; if (!ed.decompilePC(pc)) return false; char* text; if (!ed.getOutput(&text)) return false; JSString* str = JS_NewStringCopyZ(cx, text); js_free(text); if (!AppendJSONProperty(buf, "text")) return false; if (!str || !(str = StringToSource(cx, str))) return false; if (!buf.append(str)) return false; } if (!AppendJSONProperty(buf, "counts")) return false; if (!buf.append('{')) return false; if (hits > 0) { if (!AppendJSONProperty(buf, PCCounts::numExecName, NO_COMMA)) return false; if (!NumberValueToStringBuffer(cx, DoubleValue(hits), buf)) return false; } if (!buf.append('}')) return false; if (!buf.append('}')) return false; // If the current instruction has thrown, // then decrement the hit counts with the number of throws. counts = sac.maybeGetThrowCounts(pc); if (counts) hits -= counts->numExec(); } if (!buf.append(']')) return false; jit::IonScriptCounts* ionCounts = sac.getIonCounts(); if (ionCounts) { if (!AppendJSONProperty(buf, "ion")) return false; if (!buf.append('[')) return false; bool comma = false; while (ionCounts) { if (comma && !buf.append(',')) return false; comma = true; if (!buf.append('[')) return false; for (size_t i = 0; i < ionCounts->numBlocks(); i++) { if (i && !buf.append(',')) return false; const jit::IonBlockCounts& block = ionCounts->block(i); if (!buf.append('{')) return false; if (!AppendJSONProperty(buf, "id", NO_COMMA)) return false; if (!NumberValueToStringBuffer(cx, Int32Value(block.id()), buf)) return false; if (!AppendJSONProperty(buf, "offset")) return false; if (!NumberValueToStringBuffer(cx, Int32Value(block.offset()), buf)) return false; if (!AppendJSONProperty(buf, "successors")) return false; if (!buf.append('[')) return false; for (size_t j = 0; j < block.numSuccessors(); j++) { if (j && !buf.append(',')) return false; if (!NumberValueToStringBuffer(cx, Int32Value(block.successor(j)), buf)) return false; } if (!buf.append(']')) return false; if (!AppendJSONProperty(buf, "hits")) return false; if (!NumberValueToStringBuffer(cx, DoubleValue(block.hitCount()), buf)) return false; if (!AppendJSONProperty(buf, "code")) return false; JSString* str = JS_NewStringCopyZ(cx, block.code()); if (!str || !(str = StringToSource(cx, str))) return false; if (!buf.append(str)) return false; if (!buf.append('}')) return false; } if (!buf.append(']')) return false; ionCounts = ionCounts->previous(); } if (!buf.append(']')) return false; } if (!buf.append('}')) return false; MOZ_ASSERT(!cx->isExceptionPending()); return true; } JS_FRIEND_API(JSString*) js::GetPCCountScriptContents(JSContext* cx, size_t index) { JSRuntime* rt = cx->runtime(); if (!rt->scriptAndCountsVector || index >= rt->scriptAndCountsVector->length()) { JS_ReportErrorNumberASCII(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(); } static bool GenerateLcovInfo(JSContext* cx, JSCompartment* comp, GenericPrinter& out) { JSRuntime* rt = cx->runtime(); // Collect the list of scripts which are part of the current compartment. { js::gc::AutoPrepareForTracing apft(cx, SkipAtoms); } Rooted topScripts(cx, ScriptVector(cx)); for (ZonesIter zone(rt, SkipAtoms); !zone.done(); zone.next()) { for (auto script = zone->cellIter(); !script.done(); script.next()) { if (script->compartment() != comp || !script->isTopLevel() || !script->filename()) { continue; } if (!topScripts.append(script)) return false; } } if (topScripts.length() == 0) return true; // Collect code coverage info for one compartment. coverage::LCovCompartment compCover; for (JSScript* topLevel: topScripts) { RootedScript topScript(cx, topLevel); compCover.collectSourceFile(comp, &topScript->scriptSourceUnwrap()); // We found the top-level script, visit all the functions reachable // from the top-level function, and delazify them. Rooted queue(cx, ScriptVector(cx)); if (!queue.append(topLevel)) return false; RootedScript script(cx); RootedFunction fun(cx); do { script = queue.popCopy(); compCover.collectCodeCoverageInfo(comp, script->sourceObject(), script); // Iterate from the last to the first object in order to have // the functions them visited in the opposite order when popping // elements from the stack of remaining scripts, such that the // functions are more-less listed with increasing line numbers. if (!script->hasObjects()) continue; size_t idx = script->objects()->length; while (idx--) { JSObject* obj = script->getObject(idx); // Only continue on JSFunction objects. if (!obj->is()) continue; fun = &obj->as(); // Let's skip wasm for now. if (!fun->isInterpreted()) continue; // Queue the script in the list of script associated to the // current source. JSScript* childScript = JSFunction::getOrCreateScript(cx, fun); if (!childScript || !queue.append(childScript)) return false; } } while (!queue.empty()); } bool isEmpty = true; compCover.exportInto(out, &isEmpty); if (out.hadOutOfMemory()) return false; return true; } JS_FRIEND_API(char*) js::GetCodeCoverageSummary(JSContext* cx, size_t* length) { Sprinter out(cx); if (!out.init()) return nullptr; if (!GenerateLcovInfo(cx, cx->compartment(), out)) { JS_ReportOutOfMemory(cx); return nullptr; } if (out.hadOutOfMemory()) { JS_ReportOutOfMemory(cx); return nullptr; } ptrdiff_t len = out.stringEnd() - out.string(); char* res = cx->pod_malloc(len + 1); if (!res) { JS_ReportOutOfMemory(cx); return nullptr; } js_memcpy(res, out.string(), len); res[len] = 0; if (length) *length = len; return res; }