/* -*- 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 "mozilla/Util.h" #include #include #include #include #include "jsanalyze.h" #include "jsapi.h" #include "jsatom.h" #include "jsautooplen.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 "js/CharacterEncoding.h" #include "vm/ScopeObject.h" #include "vm/Shape.h" #include "vm/StringBuffer.h" #include "jscntxtinlines.h" #include "jscompartmentinlines.h" #include "jsinferinlines.h" #include "jsscriptinlines.h" using namespace js; using namespace js::gc; using js::frontend::IsIdentifier; using mozilla::ArrayLength; /* * Index limit must stay within 32 bits. */ JS_STATIC_ASSERT(sizeof(uint32_t) * JS_BITS_PER_BYTE >= INDEX_LIMIT_LOG2 + 1); /* Verify JSOP_XXX_LENGTH constant definitions. */ #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ JS_STATIC_ASSERT(op##_LENGTH == length); #include "jsopcode.tbl" #undef OPDEF const JSCodeSpec js_CodeSpec[] = { #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ {length,nuses,ndefs,format}, #include "jsopcode.tbl" #undef OPDEF }; 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 OPDEF(op,val,name,token,length,nuses,ndefs,format) \ token, #include "jsopcode.tbl" #undef OPDEF }; /* * Array of JS bytecode names used by PC count JSON, DEBUG-only js_Disassemble * and JIT debug spew. */ const char * const js_CodeName[] = { #define OPDEF(op,val,name,token,length,nuses,ndefs,format) \ name, #include "jsopcode.tbl" #undef OPDEF }; /************************************************************************/ #define COUNTS_LEN 16 size_t js_GetVariableBytecodeLength(jsbytecode *pc) { JSOp op = JSOp(*pc); JS_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_ASSUME_UNREACHABLE("Unexpected op"); } } static uint32_t NumBlockSlots(JSScript *script, jsbytecode *pc) { JS_ASSERT(*pc == JSOP_ENTERBLOCK || *pc == JSOP_ENTERLET0 || *pc == JSOP_ENTERLET1); JS_STATIC_ASSERT(JSOP_ENTERBLOCK_LENGTH == JSOP_ENTERLET0_LENGTH); JS_STATIC_ASSERT(JSOP_ENTERBLOCK_LENGTH == JSOP_ENTERLET1_LENGTH); return script->getObject(GET_UINT32_INDEX(pc))->as().slotCount(); } unsigned js::StackUses(JSScript *script, jsbytecode *pc) { JSOp op = (JSOp) *pc; const JSCodeSpec &cs = js_CodeSpec[op]; if (cs.nuses >= 0) return cs.nuses; JS_ASSERT(js_CodeSpec[op].nuses == -1); switch (op) { case JSOP_POPN: return GET_UINT16(pc); case JSOP_LEAVEBLOCK: return GET_UINT16(pc); case JSOP_LEAVEBLOCKEXPR: return GET_UINT16(pc) + 1; case JSOP_ENTERLET0: return NumBlockSlots(script, pc); case JSOP_ENTERLET1: return NumBlockSlots(script, pc) + 1; default: /* stack: fun, this, [argc arguments] */ JS_ASSERT(op == JSOP_NEW || op == JSOP_CALL || op == JSOP_EVAL || 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]; if (cs.ndefs >= 0) return cs.ndefs; uint32_t n = NumBlockSlots(script, pc); return op == JSOP_ENTERLET1 ? n + 1 : n; } 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) { JS_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_ASSUME_UNREACHABLE("bad op"); } if (arithOp(op)) return countArithNames[which - BASE_LIMIT]; MOZ_ASSUME_UNREACHABLE("bad op"); } #ifdef JS_ION 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); if (block.hitCount() < 10) continue; Sprint(sp, "BB #%lu [%05u]", block.id(), block.offset()); for (size_t j = 0; j < block.numSuccessors(); j++) Sprint(sp, " -> #%lu", block.successor(j)); Sprint(sp, " :: %llu hits %u instruction bytes %u spill bytes\n", block.hitCount(), block.instructionBytes(), block.spillBytes()); Sprint(sp, "%s\n", block.code()); } } #endif void js_DumpPCCounts(JSContext *cx, HandleScript script, js::Sprinter *sp) { JS_ASSERT(script->hasScriptCounts); #ifdef DEBUG jsbytecode *pc = script->code; while (pc < script->code + script->length) { JSOp op = JSOp(*pc); int len = js_CodeSpec[op].length; jsbytecode *next = (len != -1) ? pc + len : pc + js_GetVariableBytecodeLength(pc); if (!js_Disassemble1(cx, script, pc, pc - script->code, 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 #ifdef JS_ION jit::IonScriptCounts *ionCounts = script->getIonCounts(); while (ionCounts) { DumpIonScriptCounts(sp, ionCounts); ionCounts = ionCounts->previous(); } #endif } #ifdef DEBUG /* * If pc != NULL, 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. */ JS_FRIEND_API(bool) js_DisassembleAtPC(JSContext *cx, JSScript *scriptArg, bool lines, jsbytecode *pc, bool showAll, Sprinter *sp) { RootedScript script(cx, scriptArg); jsbytecode *next, *end; unsigned len; if (showAll) Sprint(sp, "%s:%u\n", script->filename(), script->lineno); if (pc != NULL) sp->put(" "); if (showAll) sp->put("sn stack "); sp->put("loc "); if (lines) sp->put("line"); sp->put(" op\n"); if (pc != NULL) sp->put(" "); if (showAll) sp->put("-- ----- "); sp->put("----- "); if (lines) sp->put("----"); sp->put(" --\n"); next = script->code; end = next + script->length; while (next < end) { if (next == script->main()) sp->put("main:\n"); if (pc != NULL) { if (pc == next) sp->put("--> "); else sp->put(" "); } if (showAll) { jssrcnote *sn = js_GetSrcNote(cx, script, next); if (sn) { JS_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 (script->hasAnalysis() && script->analysis()->maybeCode(next)) Sprint(sp, "%05u ", script->analysis()->getCode(next).stackDepth); else sp->put(" "); } len = js_Disassemble1(cx, script, next, next - script->code, lines, sp); if (!len) return false; next += len; } return true; } bool js_Disassemble(JSContext *cx, HandleScript script, bool lines, Sprinter *sp) { return js_DisassembleAtPC(cx, script, lines, NULL, false, sp); } JS_FRIEND_API(bool) js_DumpPC(JSContext *cx) { js::gc::AutoSuppressGC suppressGC(cx); Sprinter sprinter(cx); if (!sprinter.init()) return false; ScriptFrameIter iter(cx); RootedScript script(cx, iter.script()); bool ok = js_DisassembleAtPC(cx, script, true, iter.pc(), false, &sprinter); fprintf(stdout, "%s", sprinter.string()); return ok; } JS_FRIEND_API(bool) js_DumpScript(JSContext *cx, JSScript *scriptArg) { js::gc::AutoSuppressGC suppressGC(cx); Sprinter sprinter(cx); if (!sprinter.init()) return false; RootedScript script(cx, scriptArg); bool ok = js_Disassemble(cx, script, true, &sprinter); fprintf(stdout, "%s", sprinter.string()); return ok; } /* * Useful to debug ReconstructPCStack. */ JS_FRIEND_API(bool) js_DumpScriptDepth(JSContext *cx, JSScript *scriptArg, jsbytecode *pc) { js::gc::AutoSuppressGC suppressGC(cx); Sprinter sprinter(cx); if (!sprinter.init()) return false; RootedScript script(cx, scriptArg); bool ok = js_DisassembleAtPC(cx, script, true, pc, true, &sprinter); fprintf(stdout, "%s", sprinter.string()); return ok; } static char * QuoteString(Sprinter *sp, JSString *str, uint32_t quote); static bool ToDisassemblySource(JSContext *cx, jsval v, JSAutoByteString *bytes) { if (JSVAL_IS_STRING(v)) { Sprinter sprinter(cx); if (!sprinter.init()) return false; char *nbytes = QuoteString(&sprinter, JSVAL_TO_STRING(v), '"'); if (!nbytes) return false; nbytes = JS_sprintf_append(NULL, "%s", nbytes); if (!nbytes) return false; bytes->initBytes(nbytes); return true; } if (cx->runtime()->isHeapBusy() || cx->runtime()->noGCOrAllocationCheck) { char *source = JS_sprintf_append(NULL, ""); if (!source) return false; bytes->initBytes(source); return true; } if (!JSVAL_IS_PRIMITIVE(v)) { JSObject *obj = JSVAL_TO_OBJECT(v); if (obj->is()) { char *source = JS_sprintf_append(NULL, "depth %d {", obj->as().stackDepth()); if (!source) return false; Shape::Range r(cx, obj->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(), shape->shortid(), !r.empty() ? ", " : ""); if (!source) return false; } source = JS_sprintf_append(source, "}"); if (!source) return false; bytes->initBytes(source); return true; } if (obj->is()) { JSString *str = JS_DecompileFunction(cx, &obj->as(), 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; JS::Anchor anchor(source); return bytes->encodeLatin1(cx, source); } } return !!js_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, js_GetErrorMessage, NULL, 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", JS_PCToLineNumber(cx, 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: { Value v = StringValue(ScopeCoordinateName(cx, 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: { Value v = 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: { Value v = 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->compartment()->activeAnalysis) { Sprint(sp, " object"); break; } JSObject *obj = script->getObject(GET_UINT32_INDEX(pc)); { JSAutoByteString bytes; if (!ToDisassemblySource(cx, ObjectValue(*obj), &bytes)) return 0; Sprint(sp, " %s", bytes.ptr()); } break; } case JOF_REGEXP: { JSObject *obj = script->getRegExp(GET_UINT32_INDEX(pc)); JSAutoByteString bytes; if (!ToDisassemblySource(cx, ObjectValue(*obj), &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_SLOTNO(pc)); break; case JOF_SLOTOBJECT: { Sprint(sp, " %u", GET_SLOTNO(pc)); JSObject *obj = script->getObject(GET_UINT32_INDEX(pc + SLOTNO_LEN)); JSAutoByteString bytes; if (!ToDisassemblySource(cx, ObjectValue(*obj), &bytes)) return 0; Sprint(sp, " %s", bytes.ptr()); break; } { int i; case JOF_UINT16PAIR: i = (int)GET_UINT16(pc); Sprint(sp, " %d", i); pc += UINT16_LEN; /* FALL THROUGH */ case JOF_UINT16: i = (int)GET_UINT16(pc); goto print_int; case JOF_UINT24: JS_ASSERT(op == JSOP_UINT24 || op == JSOP_NEWARRAY || op == JSOP_INITELEM_ARRAY); 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: JS_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, js_GetErrorMessage, NULL, 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) { JS_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(NULL), size(0), offset(0), reportedOOM(false) { } Sprinter::~Sprinter() { #ifdef DEBUG if (initialized) checkInvariants(); #endif js_free(base); } bool Sprinter::init() { JS_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 { JS_ASSERT(initialized); JS_ASSERT((size_t) offset < size); JS_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 { JS_ASSERT(off >= 0 && (size_t) off < size); return base + off; } char & Sprinter::operator[](size_t off) { JS_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 NULL; } 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(); const jschar *chars = s->getChars(context); if (!chars) return -1; size_t size = length; if (size == (size_t) -1) return -1; ptrdiff_t oldOffset = offset; char *buffer = reserve(size); if (!buffer) return -1; DeflateStringToBuffer(NULL, chars, 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) js_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' }; #define DONT_ESCAPE 0x10000 static char * QuoteString(Sprinter *sp, JSString *str, uint32_t quote) { /* Sample off first for later return value pointer computation. */ bool dontEscape = (quote & DONT_ESCAPE) != 0; jschar qc = (jschar) quote; ptrdiff_t offset = sp->getOffset(); if (qc && Sprint(sp, "%c", (char)qc) < 0) return NULL; const jschar *s = str->getChars(sp->context); if (!s) return NULL; const jschar *z = s + str->length(); /* Loop control variables: z points at end of string sentinel. */ for (const jschar *t = s; t < z; s = ++t) { /* Move t forward from s past un-quote-worthy characters. */ jschar c = *t; while (c < 127 && isprint(c) && c != qc && c != '\\' && c != '\t') { c = *++t; if (t == z) break; } { ptrdiff_t len = t - s; ptrdiff_t base = sp->getOffset(); char *bp = sp->reserve(len); if (!bp) return NULL; for (ptrdiff_t i = 0; i < len; ++i) (*sp)[base + i] = (char) *s++; (*sp)[base + len] = 0; } if (t == z) break; /* Use js_EscapeMap, \u, or \x only if necessary. */ bool ok; const char *e; if (!(c >> 8) && c != 0 && (e = strchr(js_EscapeMap, (int)c)) != NULL) { ok = dontEscape ? Sprint(sp, "%c", (char)c) >= 0 : Sprint(sp, "\\%c", e[1]) >= 0; } 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). */ ok = Sprint(sp, (qc && !(c >> 8)) ? "\\x%02X" : "\\u%04X", c) >= 0; } if (!ok) return NULL; } /* Sprint the closing quote and return the quoted string. */ if (qc && Sprint(sp, "%c", (char)qc) < 0) return NULL; /* * 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 NULL; return sp->stringAt(offset); } JSString * js_QuoteString(ExclusiveContext *cx, JSString *str, jschar quote) { Sprinter sprinter(cx); if (!sprinter.init()) return NULL; char *bytes = QuoteString(&sprinter, str, quote); if (!bytes) return NULL; return js_NewStringCopyZ(cx, bytes); } /************************************************************************/ static int ReconstructPCStack(JSContext*, JSScript*, jsbytecode*, jsbytecode**); static unsigned StackDepth(JSScript *script) { return script->nslots - script->nfixed; } static JSObject * GetBlockChainAtPC(JSContext *cx, JSScript *script, jsbytecode *pc) { jsbytecode *start = script->main(); JS_ASSERT(pc >= start && pc < script->code + script->length); JSObject *blockChain = NULL; for (jsbytecode *p = start; p < pc; p += GetBytecodeLength(p)) { JSOp op = JSOp(*p); switch (op) { case JSOP_ENTERBLOCK: case JSOP_ENTERLET0: case JSOP_ENTERLET1: { JSObject *child = script->getObject(p); JS_ASSERT_IF(blockChain, child->as().stackDepth() >= blockChain->as().stackDepth()); blockChain = child; break; } case JSOP_LEAVEBLOCK: case JSOP_LEAVEBLOCKEXPR: case JSOP_LEAVEFORLETIN: { // Some LEAVEBLOCK instructions are due to early exits via // return/break/etc. from block-scoped loops and functions. We // should ignore these instructions, since they don't really signal // the end of the block. jssrcnote *sn = js_GetSrcNote(cx, script, p); if (!(sn && SN_TYPE(sn) == SRC_HIDDEN)) { JS_ASSERT(blockChain); blockChain = blockChain->as().enclosingBlock(); JS_ASSERT_IF(blockChain, blockChain->is()); } break; } default: break; } } return blockChain; } namespace { class PCStack { jsbytecode **stack; int depth_; public: PCStack() : stack(NULL), depth_(0) {} ~PCStack(); bool init(JSContext *cx, JSScript *script, jsbytecode *pc); int depth() const { return depth_; } jsbytecode *operator[](int i) const; }; } /* anonymous namespace */ PCStack::~PCStack() { js_free(stack); } bool PCStack::init(JSContext *cx, JSScript *script, jsbytecode *pc) { stack = cx->pod_malloc(StackDepth(script)); if (!stack) return false; depth_ = ReconstructPCStack(cx, script, pc, stack); JS_ASSERT(depth_ >= 0); return true; } /* Indexes the pcstack. */ jsbytecode * PCStack::operator[](int i) const { if (i < 0) { i += depth_; JS_ASSERT(i >= 0); } JS_ASSERT(i < depth_); return stack[i]; } /* * 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. Call ReconstructPCStack with the current frame's pc. 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; StackFrame *fp; RootedScript script; RootedFunction fun; BindingVector *localNames; Sprinter sprinter; ExpressionDecompiler(JSContext *cx, JSScript *script, JSFunction *fun) : cx(cx), script(cx, script), fun(cx, fun), localNames(NULL), sprinter(cx) {} ~ExpressionDecompiler(); bool init(); bool decompilePC(jsbytecode *pc); JSAtom *getVar(unsigned slot); JSAtom *getArg(unsigned slot); JSAtom *findLetVar(jsbytecode *pc, unsigned depth); 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::decompilePC(jsbytecode *pc) { JS_ASSERT(script->code <= pc && pc < script->code + script->length); PCStack pcstack; if (!pcstack.init(cx, script, pc)) return false; 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 = js_GetSrcNote(cx, script, pc); if (!sn || SN_TYPE(sn) != SRC_ASSIGNOP) return write("(") && decompilePC(pcstack[-2]) && write(" ") && write(token) && write(" ") && decompilePC(pcstack[-1]) && write(")"); break; } case 1: return write(token) && write("(") && decompilePC(pcstack[-1]) && write(")"); default: break; } } switch (op) { case JSOP_GETGNAME: case JSOP_CALLGNAME: case JSOP_NAME: case JSOP_CALLNAME: return write(loadAtom(pc)); case JSOP_GETARG: case JSOP_CALLARG: { unsigned slot = GET_ARGNO(pc); JSAtom *atom = getArg(slot); return write(atom); } case JSOP_GETLOCAL: case JSOP_CALLLOCAL: { unsigned i = GET_SLOTNO(pc); JSAtom *atom; if (i >= script->nfixed) { i -= script->nfixed; JS_ASSERT(i < unsigned(pcstack.depth())); atom = findLetVar(pc, i); if (!atom) return decompilePC(pcstack[i]); // Destructing temporary } else { atom = getVar(i); } JS_ASSERT(atom); return write(atom); } case JSOP_CALLALIASEDVAR: case JSOP_GETALIASEDVAR: { JSAtom *atom = ScopeCoordinateName(cx, script, pc); JS_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 (!decompilePC(pcstack[-1])) return false; if (IsIdentifier(prop)) { return write(".") && quote(prop, '\0'); } return write("[") && quote(prop, '\'') && write("]"); } case JSOP_GETELEM: case JSOP_CALLELEM: return decompilePC(pcstack[-2]) && write("[") && decompilePC(pcstack[-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_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 decompilePC(pcstack[-int32_t(GET_ARGC(pc) + 2)]) && write("(...)"); case JSOP_SPREADCALL: return decompilePC(pcstack[-int32_t(3)]) && write("(...)"); case JSOP_NEWARRAY: return write("[]"); case JSOP_REGEXP: case JSOP_OBJECT: { JSObject *obj = (op == JSOP_REGEXP) ? script->getRegExp(GET_UINT32_INDEX(pc)) : 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)"); } ExpressionDecompiler::~ExpressionDecompiler() { js_delete(localNames); } bool ExpressionDecompiler::init() { assertSameCompartment(cx, script); if (!sprinter.init()) return false; localNames = cx->new_(cx); if (!localNames) return false; RootedScript script_(cx, script); if (!FillBindingVector(script_, localNames)) 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) >= 0; } JSAtom * ExpressionDecompiler::loadAtom(jsbytecode *pc) { return script->getAtom(GET_UINT32_INDEX(pc)); } JSAtom * ExpressionDecompiler::findLetVar(jsbytecode *pc, unsigned depth) { if (script->hasObjects()) { JSObject *chain = GetBlockChainAtPC(cx, script, pc); if (!chain) return NULL; JS_ASSERT(chain->is()); do { BlockObject &block = chain->as(); uint32_t blockDepth = block.stackDepth(); uint32_t blockCount = block.slotCount(); if (uint32_t(depth - blockDepth) < uint32_t(blockCount)) { for (Shape::Range r(block.lastProperty()); !r.empty(); r.popFront()) { const Shape &shape = r.front(); if (shape.shortid() == int(depth - blockDepth)) return JSID_TO_ATOM(shape.propid()); } } chain = chain->getParent(); } while (chain && chain->is()); } return NULL; } JSAtom * ExpressionDecompiler::getArg(unsigned slot) { JS_ASSERT(fun); JS_ASSERT(slot < script->bindings.count()); return (*localNames)[slot].name(); } JSAtom * ExpressionDecompiler::getVar(unsigned slot) { JS_ASSERT(fun); slot += fun->nargs; JS_ASSERT(slot < script->bindings.count()); return (*localNames)[slot].name(); } 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; } static bool FindStartPC(JSContext *cx, ScriptFrameIter &iter, int spindex, int skipStackHits, Value v, jsbytecode **valuepc) { jsbytecode *current = *valuepc; if (spindex == JSDVG_IGNORE_STACK) return true; /* * Fall back on *valuepc as start pc if this frame is calling .apply and * the methodjit has "splatted" the arguments array, bumping the caller's * stack pointer and skewing it from what static analysis in pcstack.init * would compute. * * FIXME: also fall back if iter.isIon(), since the stack snapshot may be * for the previous pc (see bug 831120). */ if (iter.isIon()) return true; *valuepc = NULL; PCStack pcstack; if (!pcstack.init(cx, iter.script(), current)) return false; if (spindex < 0 && spindex + pcstack.depth() < 0) spindex = JSDVG_SEARCH_STACK; if (spindex == JSDVG_SEARCH_STACK) { size_t index = iter.numFrameSlots(); JS_ASSERT(index >= size_t(pcstack.depth())); // 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 index is out of bounds in pcstack, the blamed value must be one // pushed by the current bytecode, so restore *valuepc. if (index < size_t(pcstack.depth())) *valuepc = pcstack[index]; else *valuepc = current; } else { *valuepc = pcstack[spindex]; } return true; } static bool DecompileExpressionFromStack(JSContext *cx, int spindex, int skipStackHits, HandleValue v, char **res) { JS_ASSERT(spindex < 0 || spindex == JSDVG_IGNORE_STACK || spindex == JSDVG_SEARCH_STACK); *res = NULL; #ifdef JS_MORE_DETERMINISTIC /* * Give up if we need deterministic behavior for differential testing. * IonMonkey doesn't use StackFrames and this ensures we get the same * error messages. */ return true; #endif ScriptFrameIter frameIter(cx); if (frameIter.done()) return true; RootedScript script(cx, frameIter.script()); AutoCompartment ac(cx, &script->global()); jsbytecode *valuepc = frameIter.pc(); RootedFunction fun(cx, frameIter.isFunctionFrame() ? frameIter.callee() : NULL); JS_ASSERT(script->code <= valuepc && valuepc < script->code + script->length); // 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 NULL; 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 NULL; } Rooted linear(cx, fallback->ensureLinear(cx)); if (!linear) return NULL; TwoByteChars tbchars(linear->chars(), linear->length()); return LossyTwoByteCharsToNewLatin1CharsZ(cx, tbchars).c_str(); } static bool DecompileArgumentFromStack(JSContext *cx, int formalIndex, char **res) { JS_ASSERT(formalIndex >= 0); *res = NULL; #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. */ ScriptFrameIter frameIter(cx); JS_ASSERT(!frameIter.done()); /* * Get the second-to-top frame, the caller of the builtin that called the * intrinsic. */ ++frameIter; if (frameIter.done()) return true; RootedScript script(cx, frameIter.script()); AutoCompartment ac(cx, &script->global()); jsbytecode *current = frameIter.pc(); RootedFunction fun(cx, frameIter.isFunctionFrame() ? frameIter.callee() : NULL); JS_ASSERT(script->code <= current && current < script->code + script->length); 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; PCStack pcStack; if (!pcStack.init(cx, script, current)) return false; int formalStackIndex = pcStack.depth() - GET_ARGC(current) + formalIndex; JS_ASSERT(formalStackIndex >= 0); if (formalStackIndex >= pcStack.depth()) return true; ExpressionDecompiler ed(cx, script, fun); if (!ed.init()) return false; if (!ed.decompilePC(pcStack[formalStackIndex])) return false; return ed.getOutput(res); } char * js::DecompileArgument(JSContext *cx, int formalIndex, HandleValue v) { { char *result; if (!DecompileArgumentFromStack(cx, formalIndex, &result)) return NULL; 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 NULL; Rooted linear(cx, fallback->ensureLinear(cx)); if (!linear) return NULL; return LossyTwoByteCharsToNewLatin1CharsZ(cx, linear->range()).c_str(); } unsigned js_ReconstructStackDepth(JSContext *cx, JSScript *script, jsbytecode *pc) { return ReconstructPCStack(cx, script, pc, NULL); } #define LOCAL_ASSERT_CUSTOM(expr, BAD_EXIT) \ JS_BEGIN_MACRO \ JS_ASSERT(expr); \ if (!(expr)) { BAD_EXIT; } \ JS_END_MACRO #define LOCAL_ASSERT_RV(expr, rv) \ LOCAL_ASSERT_CUSTOM(expr, return (rv)) #define LOCAL_ASSERT(expr) LOCAL_ASSERT_RV(expr, -1); static int SimulateOp(JSScript *script, JSOp op, const JSCodeSpec *cs, jsbytecode *pc, jsbytecode **pcstack, unsigned &pcdepth) { unsigned nuses = StackUses(script, pc); unsigned ndefs = StackDefs(script, pc); LOCAL_ASSERT(pcdepth >= nuses); pcdepth -= nuses; LOCAL_ASSERT(pcdepth + ndefs <= StackDepth(script)); /* * Fill the slots that the opcode defines withs its pc unless it just * reshuffles the stack. In the latter case we want to preserve the * opcode that generated the original value. */ switch (op) { default: if (pcstack) { for (unsigned i = 0; i != ndefs; ++i) pcstack[pcdepth + i] = pc; } break; case JSOP_CASE: /* Keep the switch value. */ JS_ASSERT(ndefs == 1); break; case JSOP_DUP: JS_ASSERT(ndefs == 2); if (pcstack) pcstack[pcdepth + 1] = pcstack[pcdepth]; break; case JSOP_DUP2: JS_ASSERT(ndefs == 4); if (pcstack) { pcstack[pcdepth + 2] = pcstack[pcdepth]; pcstack[pcdepth + 3] = pcstack[pcdepth + 1]; } break; case JSOP_SWAP: JS_ASSERT(ndefs == 2); if (pcstack) { jsbytecode *tmp = pcstack[pcdepth + 1]; pcstack[pcdepth + 1] = pcstack[pcdepth]; pcstack[pcdepth] = tmp; } break; } pcdepth += ndefs; return pcdepth; } /* Ensure that script analysis reports the same stack depth. */ static void AssertPCDepth(JSScript *script, jsbytecode *pc, unsigned pcdepth) { /* * If this assertion fails, run the failing test case under gdb and use the * following gdb command to understand the execution path of this function. * * call js_DumpScriptDepth(cx, script, pc) */ JS_ASSERT_IF(script->hasAnalysis() && script->analysis()->maybeCode(pc), script->analysis()->getCode(pc).stackDepth == pcdepth); } static int ReconstructPCStack(JSContext *cx, JSScript *script, jsbytecode *target, jsbytecode **pcstack) { /* * Walk forward from script->code and compute the stack depth and stack of * operand-generating opcode PCs in pcstack. * * Every instruction has a statically computable stack depth before and * after. This function returns the stack depth before the target * instruction. * * The stack depth can be computed from these three common-sense rules... * * - The stack depth before the first instruction of the script is 0. * * - The stack depth after an instruction is always simply the stack * depth before, plus whatever effect of the instruction has. * SimulateOp computes this. * * - Except for three specific cases listed below, the stack depth before * an instruction is simply the stack depth after the preceding one. * * ... and these three less obvious rules: * * - Special case 1: break, continue, or return statements that exit * certain kinds of blocks. * * Rule: The stack depth before the next instruction (following the * GOTO/RETRVAL) equals the stack depth before the first SRC_HIDDEN * instruction. * * - Special case 2: The rethrow at the end of conditional catch blocks. * * Rule: The stack depth before the next rethrow instruction of a * conditional catch block equals the stack depth at the end of the IFEQ * of the catch condition (which is jumping to this re-throw). * * - Special case 3: Conditional expressions (A ? B : C). * * Rule: The code for B and C are pushing a result on the stack, so the * stack depth before C equals the stack depth before B, which is one * less than the stack depth after the GOTO ending B. */ LOCAL_ASSERT(script->code <= target && target < script->code + script->length); /* * Save depth of hidden instructions to recover the depth if a branch is not * taken. */ unsigned hpcdepth = unsigned(-1); /* * Save depth of catch instructions to recover it in the rethrow path, at * the end of a conditional catch. */ unsigned cpcdepth = unsigned(-1); jsbytecode *pc; unsigned pcdepth; ptrdiff_t oplen; for (pc = script->code, pcdepth = 0; ; pc += oplen) { JSOp op = JSOp(*pc); oplen = GetBytecodeLength(pc); const JSCodeSpec *cs = &js_CodeSpec[op]; jssrcnote *sn = js_GetSrcNote(cx, script, pc); /* * *** Special case 1.a *** * * Hidden instructions are used to pop scoped variables (leaveblock) in * early-exit paths and are not supposed to affect the stack depth when * they are not taken. However, when the target is in an early-exit * path, hidden instructions need to be taken into account. * * As we are not following any branch, we need to restore the stack * depth after any instruction which is breaking the flow of the * program. Sequences of hidden instructions are *included* in the * following grammar: * * ExitSequence: * EarlyExit // return / break / continue * | ConditionalCatchExit // conditional catch-block exit * * EarlyExit: * setrval; ExitSegment* retrval; // return stmt * | ExitSegment* goto; // break/continue stmt * * ConditionalCatchExit: * ExitSegment* hidden goto; CatchRethrow * * CatchRethrow: * hidden throw; end-brace nop; // last catch-block * | hidden throw; finally; // followed by a finally * | hidden throwing; hidden leaveblock; catch enterblock; * // followed by a catch-block * * ExitSegment: * hidden leaveblock; // leave let-block or * // leave catch-block * | hidden leavewith; // leave with-block * | hidden gosub; // leave try or * // leave catch-block with finally * | hidden enditer; // leave for-in/of block * | hidden leaveforletin; hidden enditer; hidden popn; * // leave for-let-in/of block * * The following code save the depth of the first hidden * instruction. When we hit either the last instruction of the EarlyExit * (retrval/goto) or the goto of a ConditionalCacthExit, we restore (see * Special case 1.b) the depth after the execution of the instruction. * * In case of a ConditionalCacthExit, we keep the hidden depth after the * hidden goto to again restore (see Special case 1.b) the depth after * the throw/throwing instruction of the CatchRethrow. * * Note: The last instructions of the EarlyExit, such as break, continue * and return are not hidden as they are part of the original sources, * so we need to consider them as-if they were hidden instruction (do * not reset the depth) and restore the depth after their execution. It * is important to restore them after their execution, and not at the * next instruction, because they might be followed by another sequence * of hidden instruction. */ bool exitPath = op == JSOP_GOTO || op == JSOP_RETRVAL || op == JSOP_THROW; bool isConditionalCatchExit = false; if (sn && SN_TYPE(sn) == SRC_HIDDEN) { /* Only ConditionalCatchExit's goto are hidden. */ isConditionalCatchExit = op == JSOP_GOTO; if (hpcdepth == unsigned(-1)) hpcdepth = pcdepth; } else if (!exitPath) { hpcdepth = unsigned(-1); } /* * *** Special case 2 *** * * The catch depth is used to restore the depth expected in case of * rethrow or in case of guard failure. The depth expected by throw and * throwing instructions correspond to the result of enterblock and * exception instructions. * * enterblock depth d <-- depth += d * exception <-- depth += 1 * * This is not a problem, except with conditional catch staements. * Conditional catch statements are looking like: * * ifeq +... <-- jump to the rethrow path. * pop <-- pop the exception value. * * Unfortunately, we iterate linearly over the bytecode, so we cannot * use a stack to save the depth for each catch statement that we * encounter. The trick done here is to save the stack depth before we * unwind the scoped variables with leaveblock, and restore the stack * depth plus one to account for the missing exception value. * * {Catch} leaveblock d <-- save depth * ( gosub ... )? * {Hidden} goto ... * ( {Hidden} throw )? <-- restore depth + 1 * * If there is no rethrow, then we should expect an end-brace nop or a * finally. */ if (op == JSOP_LEAVEBLOCK && sn && SN_TYPE(sn) == SRC_CATCH) { LOCAL_ASSERT(cpcdepth == unsigned(-1)); cpcdepth = pcdepth; } else if (sn && SN_TYPE(sn) == SRC_HIDDEN && (op == JSOP_THROW || op == JSOP_THROWING)) { /* * The current catch block is conditional, we compensate for the pop * of the exception added after the ifeq by adding 1 to the restored * depth. This fake slot might not contain the right value, but * it will be eliminated with the throw or throwing instruction. */ LOCAL_ASSERT(cpcdepth != unsigned(-1)); pcdepth = cpcdepth + 1; cpcdepth = unsigned(-1); } else if (!(op == JSOP_GOTO && sn && SN_TYPE(sn) == SRC_HIDDEN) && !(op == JSOP_GOSUB && cpcdepth != unsigned(-1))) { /* * We need to ignore gosub and goto when we exit a catch block. * Otherwise we might reset the catch depth before hitting the throw * or throwing instruction. * * {Catch} leaveblock d * ( gosub ... )? * {Hidden} goto ... * ( {Hidden} throw )? * finally * * If there is no rethrow, the end-brace nop or the finally * instruction will reset the saved depth. */ if (cpcdepth != unsigned(-1)) LOCAL_ASSERT(op == JSOP_NOP || op == JSOP_FINALLY); cpcdepth = unsigned(-1); } /* At this point, pcdepth is the stack depth *before* the insn at pc. */ AssertPCDepth(script, pc, pcdepth); if (pc >= target) break; /* Simulate the instruction at pc. */ if (SimulateOp(script, op, cs, pc, pcstack, pcdepth) < 0) return -1; /* At this point, pcdepth is the stack depth *after* the insn at pc. */ /* * *** Special case 1.b *** * * Restore the stack depth after the execution of an EarlyExit or the * goto of the ConditionalCatchExit. */ if (exitPath && hpcdepth != unsigned(-1)) { pcdepth = hpcdepth; /* Keep the depth if we are on the ConditionalCatchExit path. */ if (!isConditionalCatchExit) hpcdepth = unsigned(-1); } /* * *** Special case 3 *** * * A (C ? T : E) expression requires skipping T if target is in E or * after the whole expression, because this expression is pushing a * result on the stack and the goto cannot be skipped. * * 09 001 pc : ifeq +11 * 000 pc+ 5: null * 001 pc+ 6: goto +... <-- The stack after the goto * 000 pc+11: ... <-- does not have the same depth. * */ if (sn && SN_TYPE(sn) == SRC_COND) { ptrdiff_t jmplen = GET_JUMP_OFFSET(pc); if (pc + jmplen <= target) { /* Target does not lie in T. */ oplen = jmplen; } } } LOCAL_ASSERT(pc == target); AssertPCDepth(script, pc, pcdepth); return pcdepth; } #undef LOCAL_ASSERT #undef LOCAL_ASSERT_RV 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; } 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.appendInflated(name, strlen(name)); buf.appendInflated("\":", 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, js_GetErrorMessage, NULL, JSMSG_BUFFER_TOO_SMALL); return NULL; } 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 ContextAllocPolicy 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 NULL; buf.append(str); AppendJSONProperty(buf, "line"); NumberValueToStringBuffer(cx, Int32Value(script->lineno), buf); if (script->function()) { JSAtom *atom = script->function()->displayAtom(); if (atom) { AppendJSONProperty(buf, "name"); if (!(str = StringToSource(cx, atom))) return NULL; 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->code + 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_ASSUME_UNREACHABLE("Bad opcode"); } else if (PCCounts::arithOp(op)) { arithTotals[j - PCCounts::BASE_LIMIT] += value; } else { MOZ_ASSUME_UNREACHABLE("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 NULL; 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, NULL, 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->code + script->length; pc += GetBytecodeLength(pc)) { size_t offset = pc - script->code; JSOp op = (JSOp) *pc; if (comma) buf.append(','); comma = true; buf.append('{'); AppendJSONProperty(buf, "id", NO_COMMA); NumberValueToStringBuffer(cx, Int32Value(pc - script->code), 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.appendInflated(name, strlen(name)); buf.append('\"'); } { ExpressionDecompiler ed(cx, script, script->function()); 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); AppendJSONProperty(buf, "instructionBytes"); NumberValueToStringBuffer(cx, Int32Value(block.instructionBytes()), buf); AppendJSONProperty(buf, "spillBytes"); NumberValueToStringBuffer(cx, Int32Value(block.spillBytes()), buf); 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, js_GetErrorMessage, NULL, JSMSG_BUFFER_TOO_SMALL); return NULL; } const ScriptAndCounts &sac = (*rt->scriptAndCountsVector)[index]; JSScript *script = sac.script; StringBuffer buf(cx); if (!script->function() && !script->compileAndGo) return buf.finishString(); { AutoCompartment ac(cx, &script->global()); if (!GetPCCountJSON(cx, sac, buf)) return NULL; } return buf.finishString(); }